lib/assistant.ts (292 lines of code) (raw):
import { DEVELOPER_PROMPT } from "@/config/constants";
import { parse } from "partial-json";
import { handleTool } from "@/lib/tools/tools-handling";
import useConversationStore from "@/stores/useConversationStore";
import { tools } from "@/lib/tools/tools";
import { Annotation } from "@/components/Annotations";
import { functionsMap } from "@/config/functions";
import useDataStore from "@/stores/useDataStore";
import { agentTools } from "@/config/tools-list";
export interface ContentItem {
type: "input_text" | "output_text" | "refusal" | "output_audio";
annotations?: Annotation[];
text?: string;
}
// Message items for storing conversation history matching API shape
export interface MessageItem {
type: "message";
role: "user" | "assistant" | "system";
id?: string;
content: ContentItem[];
}
// Chat messages to display in chat
export interface ChatMessage {
type: "message";
role: "user" | "agent";
id?: string;
content: ContentItem[];
}
// Custom items to display in chat
export interface ToolCallItem {
type: "tool_call";
tool_type: "file_search_call" | "web_search_call" | "function_call";
status: "in_progress" | "completed" | "failed" | "searching";
id: string;
name?: string | null;
call_id?: string;
arguments?: string;
parsedArguments?: any;
output?: string | null;
}
export type Item = ChatMessage | ToolCallItem;
export const handleTurn = async (
messages: any[],
onMessage: (data: any) => void
) => {
try {
// Get response from the API (defined in app/api/turn_response/route.ts)
const response = await fetch("/api/turn_response", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: messages,
tools: tools,
}),
});
if (!response.ok) {
console.error(`Error: ${response.status} - ${response.statusText}`);
return;
}
// Reader for streaming data
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let done = false;
let buffer = "";
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
buffer += chunkValue;
const lines = buffer.split("\n\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const dataStr = line.slice(6);
if (dataStr === "[DONE]") {
done = true;
break;
}
const data = JSON.parse(dataStr);
onMessage(data);
}
}
}
// Handle any remaining data in buffer
if (buffer && buffer.startsWith("data: ")) {
const dataStr = buffer.slice(6);
if (dataStr !== "[DONE]") {
const data = JSON.parse(dataStr);
onMessage(data);
}
}
} catch (error) {
console.error("Error handling turn:", error);
}
};
export const processMessages = async () => {
const {
chatMessages,
conversationItems,
recommendedActions,
setChatMessages,
setConversationItems,
setRecommendedActions,
setSuggestedMessage,
setAgentTyping,
setSuggestedMessageDone,
} = useConversationStore.getState();
const { setRelevantArticlesLoading, setFAQExtracts } =
useDataStore.getState();
const allConversationItems = [
// Adding developer prompt as first item in the conversation
{
role: "developer",
content: DEVELOPER_PROMPT,
},
...conversationItems,
];
let assistantMessageContent = "";
let functionArguments = "";
await handleTurn(allConversationItems, async ({ event, data }) => {
switch (event) {
case "response.output_text.delta":
case "response.output_text.annotation.added": {
const { delta, item_id, annotation } = data;
setAgentTyping(true);
let partial = "";
if (typeof delta === "string") {
partial = delta;
}
assistantMessageContent += partial;
const message = {
type: "message",
role: "agent",
id: item_id,
content: [
{
type: "output_text",
text: assistantMessageContent,
annotations: annotation ? [annotation] : undefined,
},
],
} as ChatMessage;
if (annotation) {
message.content[0].annotations = [
...(message.content[0].annotations ?? []),
annotation,
];
}
setSuggestedMessage(message);
break;
}
case "response.output_text.done": {
setSuggestedMessageDone(true);
break;
}
case "response.output_item.added": {
const { item } = data || {};
// New item coming in
if (!item || !item.type) {
break;
}
// Handle differently depending on the item type
switch (item.type) {
case "function_call": {
functionArguments += item.arguments || "";
chatMessages.push({
type: "tool_call",
tool_type: "function_call",
status: "in_progress",
id: item.id,
name: item.name, // function name,e.g. "get_weather"
call_id: item.call_id,
arguments: item.arguments || "",
parsedArguments: {},
output: null,
});
setChatMessages([...chatMessages]);
break;
}
case "web_search_call": {
chatMessages.push({
type: "tool_call",
tool_type: "web_search_call",
status: item.status || "in_progress",
id: item.id,
});
setChatMessages([...chatMessages]);
break;
}
case "file_search_call": {
setRelevantArticlesLoading(true);
chatMessages.push({
type: "tool_call",
tool_type: "file_search_call",
status: item.status || "in_progress",
id: item.id,
});
setChatMessages([...chatMessages]);
break;
}
}
break;
}
case "response.function_call_arguments.delta": {
// Streaming arguments delta to show in the chat
functionArguments += data.delta || "";
let parsedFunctionArguments = {};
if (functionArguments.length > 0) {
parsedFunctionArguments = parse(functionArguments);
}
const toolCallMessage = chatMessages.find((m) => m.id === data.item_id);
if (toolCallMessage && toolCallMessage.type === "tool_call") {
toolCallMessage.arguments = functionArguments;
try {
toolCallMessage.parsedArguments = parsedFunctionArguments;
} catch {
// partial JSON can fail parse; ignore
}
setChatMessages([...chatMessages]);
}
break;
}
case "response.function_call_arguments.done": {
// This has the full final arguments string
const { item_id, arguments: finalArgs } = data;
functionArguments = finalArgs;
// Mark the tool_call as "completed" and parse the final JSON
const toolCallMessage = chatMessages.find((m) => m.id === item_id);
if (toolCallMessage && toolCallMessage.type === "tool_call") {
toolCallMessage.arguments = finalArgs;
toolCallMessage.parsedArguments = parse(finalArgs);
toolCallMessage.status = "completed";
setChatMessages([...chatMessages]);
if (
toolCallMessage.name &&
agentTools.includes(toolCallMessage.name)
) {
setRecommendedActions([
...recommendedActions.filter(
(action) => action.name !== toolCallMessage.name
),
{
name: toolCallMessage.name as keyof typeof functionsMap,
parameters: toolCallMessage.parsedArguments,
},
]);
}
}
break;
}
case "response.web_search_call.completed": {
const { item_id, output } = data;
const toolCallMessage = chatMessages.find((m) => m.id === item_id);
if (toolCallMessage && toolCallMessage.type === "tool_call") {
toolCallMessage.output = output;
toolCallMessage.status = "completed";
setChatMessages([...chatMessages]);
}
break;
}
case "response.file_search_call.completed": {
const { item_id } = data;
console.log("file search call completed", data);
const toolCallMessage = chatMessages.find((m) => m.id === item_id);
if (toolCallMessage && toolCallMessage.type === "tool_call") {
toolCallMessage.status = "completed";
setChatMessages([...chatMessages]);
}
break;
}
case "response.output_item.done": {
// After output item is done, adding tool call ID
const { item } = data || {};
conversationItems.push({
...item,
results: undefined,
});
if (item.type === "function_call") {
const toolCallMessage = chatMessages.find((m) => m.id === item.id);
if (toolCallMessage && toolCallMessage.type === "tool_call") {
// Handle tool call (execute function)
const toolResult = await handleTool(
toolCallMessage.name as keyof typeof functionsMap,
toolCallMessage.parsedArguments
);
toolCallMessage.call_id = item.call_id;
// Record tool output
toolCallMessage.output = JSON.stringify(toolResult);
setChatMessages([...chatMessages]);
conversationItems.push({
type: "function_call_output",
call_id: toolCallMessage.call_id,
status: "completed",
output: JSON.stringify(toolResult),
});
// Create another turn after tool output has been added
await processMessages();
}
}
if (item.type === "file_search_call") {
setFAQExtracts(item.results);
setRelevantArticlesLoading(false);
}
setConversationItems([...conversationItems]);
break;
}
// Handle other events as needed
}
});
};