Skip to content

Commit

Permalink
feat: implement chain of thoughts for chatbot - WF-88
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Nov 20, 2024
1 parent 77c793e commit aab3d8d
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<template>
<div class="CoreChatbotMessage" :aria-busy="isLoading">
<CoreChatbotAvatar :initials="initials" />
<CoreChatbotAvatar v-if="displayInitials" :initials="initials" />
<div v-else></div>
<div
class="CoreChatbotMessage__content"
:style="{ background: contentBgColor }"
>
<CoreChatbotLoader v-if="displayLoader" />
<CoreChatbotMessageContentThoughtProcess
v-else-if="message.role === 'thought_process'"
:content="message.content"
:pending="pending"
/>
<div v-else class="CoreChatbotMessage__content__text">
<BaseMarkdown v-if="useMarkdown" :raw-text="content">
</BaseMarkdown>
Expand Down Expand Up @@ -44,10 +50,12 @@ export type Action = {
data?: string;
};
type ValueOrArray<T> = T | ValueOrArray<T>[];
type NestedStringArray = ValueOrArray<string>;
export type Message = {
role: string;
pending: boolean;
content: string;
content?: NestedStringArray;
actions?: Action[];
};
</script>
Expand All @@ -56,6 +64,10 @@ export type Message = {
import { computed, defineAsyncComponent, PropType } from "vue";
import CoreChatbotAvatar from "./CoreChatbotAvatar.vue";
const CoreChatbotMessageContentThoughtProcess = defineAsyncComponent(
() => import("./ThoughtProcess/ThoughtProcess.vue"),
);
const BaseMarkdown = defineAsyncComponent(
() => import("../../base/BaseMarkdown.vue"),
);
Expand All @@ -73,6 +85,7 @@ const props = defineProps({
},
assistantRoleColor: { type: String, required: false, default: "" },
isLoading: { type: Boolean, required: false },
pending: { type: Boolean },
});
defineEmits({
Expand All @@ -91,7 +104,22 @@ const role = computed(() => {
return props.message?.role ?? "";
});
const content = computed(() => props.message?.content.trim() ?? "");
const isThoughtProcess = computed(
() => props.message?.role === "thought_process",
);
const displayInitials = computed(() => {
if (!isThoughtProcess.value) return true;
return !!props.pending;
});
const content = computed(() => {
if (typeof props.message?.content === "string")
return props.message.content;
if (Array.isArray(props.message?.content))
return props.message.content.join("\n");
return "";
});
const contentBgColor = computed(() => {
switch (role.value) {
Expand All @@ -110,8 +138,9 @@ const contentBgColor = computed(() => {

<style scoped>
.CoreChatbotMessage {
display: flex;
display: grid;
gap: 8px;
grid-template-columns: 32px auto;
}
.CoreChatbotMessage__content {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<ThoughtProcessStep
v-if="pending"
:content="String(pendingContent)"
pending
/>
<div v-else class="ThoughtProcess">
<div>
<button
class="ThoughtProcess__toggler"
:class="{
'ThoughtProcess__toggler--expanded': isExpanded,
}"
@click="isExpanded = !isExpanded"
>
<i class="icon material-symbols-outlined" aria-hidden="true">{{
togglerIcon
}}</i>
Though process
</button>
</div>
<ThoughtProcessSteps
v-show="isExpanded"
:aria-expanded="isExpanded"
:content="content"
/>
</div>
</template>

<script lang="ts" setup>
import { computed, PropType, ref } from "vue";
import ThoughtProcessStep from "./ThoughtProcessStep.vue";
import ThoughtProcessSteps from "./ThoughtProcessSteps.vue";
import type { Message } from "../CoreChatbotMessage.vue";
const props = defineProps({
content: {
type: [String, Array] as PropType<Message["content"]>,
required: true,
},
pending: { type: Boolean },
});
const isExpanded = ref(false);
const togglerIcon = computed(() =>
isExpanded.value ? "arrow_drop_down" : "arrow_drop_up",
);
function getLastContent(content: Message["content"]): string | undefined {
if (!Array.isArray(content)) return content;
const element = content.at(-1);
return Array.isArray(element) ? getLastContent(element) : element;
}
const pendingContent = computed(() => getLastContent(props.content));
</script>

<style scoped>
.ThoughtProcess__toggler {
display: flex;
align-items: center;
gap: 4px;
border-radius: 16px;
border: none;
height: 24px;
padding: 0 8px;
cursor: pointer;
background-color: transparent;
}
.ThoughtProcess__toggler:hover {
background-color: var(--separatorColor);
}
.ThoughtProcess__toggler--expanded {
background-color: var(--separatorColor);
margin-bottom: 16px;
}
.ThoughtProcess__toggler i {
font-size: 20px;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<ThoughtProcessSteps
v-if="Array.isArray(content)"
:content="content"
style="margin-left: 12px"
/>
<div
v-else
class="ThoughtProcessStep"
:class="{ 'ThoughtProcessStep--pending': pending }"
>
{{ content }}
<CoreChatbotLoader v-if="pending" style="width: auto" />
</div>
</template>

<script lang="ts" setup>
import { defineAsyncComponent, PropType } from "vue";
import type { Message } from "../CoreChatbotMessage.vue";
import ThoughtProcessSteps from "./ThoughtProcessSteps.vue";
const CoreChatbotLoader = defineAsyncComponent(
() => import("../CoreChatbotLoader.vue"),
);
defineProps({
content: {
type: [String, Array] as PropType<Message["content"]>,
required: true,
},
pending: { type: Boolean },
});
</script>

<style scoped>
.ThoughtProcessStep {
position: relative;
padding: 8px 16px;
border: 1px solid var(--separatorColor);
border-radius: 8px;
box-shadow: var(--containerShadow);
background-color: var(--containerBackgroundColor);
}
.ThoughtProcessStep--pending {
display: flex;
align-items: center;
gap: 8px;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<ul class="ThoughtProcessSteps">
<li
v-for="(line, index) of contentAsArray"
:key="index"
class="ThoughtProcessSteps__step"
>
<ThoughtProcessStep :content="line" />
</li>
</ul>
</template>

<script lang="ts" setup>
import { computed, PropType } from "vue";
import type { Message } from "../CoreChatbotMessage.vue";
import ThoughtProcessStep from "./ThoughtProcessStep.vue";
const props = defineProps({
content: {
type: [String, Array] as PropType<Message["content"]>,
required: true,
},
});
const contentAsArray = computed(() =>
Array.isArray(props.content) ? props.content : [props.content],
);
</script>

<style scoped>
.ThoughtProcessSteps {
display: flex;
gap: 8px;
flex-direction: column;
list-style: none;
position: relative;
}
/* vertical line behind the messages */
.ThoughtProcessSteps::before {
content: "";
background-color: var(--separatorColor);
width: 1px;
height: 100%;
position: absolute;
left: 17px;
}
</style>
4 changes: 4 additions & 0 deletions src/ui/src/components/core/content/CoreChatbot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ See the stubs for more details.
:message="message"
:use-markdown="fields.useMarkdown.value == 'yes'"
:assistant-role-color="fields.assistantRoleColor.value"
:pending="
message.role === 'thought_process' &&
messageIndexLoading === messageId
"
:initials="
message.role === 'assistant'
? fields.assistantInitials.value
Expand Down
13 changes: 8 additions & 5 deletions src/writer/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class FunctionTool(Tool):


class PreparedAPIMessage(TypedDict, total=False):
role: Literal["user", "assistant", "system", "tool"]
role: Literal["user", "assistant", "system", "tool", "thought_process"]

content: Union[str, None]

Expand Down Expand Up @@ -876,7 +876,7 @@ class Message(TypedDict, total=False):
:param actions: Optional dictionary containing actions
related to the message.
"""
role: Literal["system", "assistant", "user", "tool"]
role: Literal["system", "assistant", "user", "tool", "thought_process"]
content: str
actions: Optional[dict]
name: Optional[str]
Expand Down Expand Up @@ -907,13 +907,15 @@ def validate_message(cls, message):
if not (
isinstance(message["content"], str)
or
type(message["content"]) in (tuple, list)
or
message["content"] is None
):
raise ValueError(
f"Non-string content in message cannot be added: {message}"
)

if message["role"] not in ["system", "assistant", "user", "tool"]:
if message["role"] not in ["system", "assistant", "user", "tool", "thought_process"]:
raise ValueError(f"Unsupported role in message: {message}")

def __init__(
Expand Down Expand Up @@ -1312,7 +1314,7 @@ def __add__(self, chunk_or_message: Union['Conversation.Message', dict]):
message_to_append = {
"role": message["role"],
"content": message["content"],
"actions": message.get("actions")
"actions": message.get("actions"),
}
if "tool_calls" in message:
message_to_append["tool_calls"] = message["tool_calls"]
Expand Down Expand Up @@ -1355,6 +1357,7 @@ def _send_chat_request(
[
self._prepare_message(message)
for message in self.messages
if message["role"] != "thought_process"
]
)
logging.debug(
Expand Down Expand Up @@ -1837,7 +1840,7 @@ def _serialize_message(self, message: 'Conversation.Message'):
return {
"role": message["role"],
"content": message["content"],
"actions": message["actions"]
"actions": message["actions"],
}

@property
Expand Down

0 comments on commit aab3d8d

Please sign in to comment.