Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

230 lines
6.4 KiB
TypeScript

import type { Route } from "./+types/api.chat";
import OpenAI from "openai";
type ChatCompletionMessageParam =
OpenAI.Chat.Completions.ChatCompletionMessageParam;
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// Define available tools
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city name, e.g. San Francisco",
},
},
required: ["location"],
},
},
},
];
// Simple tool execution
async function executeTool(
name: string,
args: Record<string, unknown>,
): Promise<string> {
if (name === "get_weather") {
const location = args.location as string;
// Simulate weather API call
const temp = Math.floor(Math.random() * 30) + 10;
const conditions = ["sunny", "cloudy", "rainy", "partly cloudy"];
const condition = conditions[Math.floor(Math.random() * conditions.length)];
return JSON.stringify({
location,
temperature: temp,
unit: "celsius",
condition,
});
}
return JSON.stringify({ error: "Unknown tool" });
}
export async function action({ request }: Route.ActionArgs) {
const { messages } = await request.json();
const openaiMessages: ChatCompletionMessageParam[] = messages.map(
(m: { role: string; content: string }) => ({
role: m.role as "user" | "assistant",
content: m.content,
}),
);
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
let continueLoop = true;
while (continueLoop) {
const response = await openai.chat.completions.create({
model: "gpt-5.4-nano",
messages: openaiMessages,
tools,
stream: true,
});
let currentToolCalls: {
id: string;
name: string;
arguments: string;
}[] = [];
let assistantContent = "";
for await (const chunk of response) {
const delta = chunk.choices[0]?.delta;
// Handle text content
if (delta?.content) {
assistantContent += delta.content;
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ type: "text", content: delta.content })}\n\n`,
),
);
}
// Handle tool calls
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
const index = toolCall.index;
if (!currentToolCalls[index]) {
currentToolCalls[index] = {
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: "",
};
}
if (toolCall.id) {
currentToolCalls[index].id = toolCall.id;
}
if (toolCall.function?.name) {
currentToolCalls[index].name = toolCall.function.name;
}
if (toolCall.function?.arguments) {
currentToolCalls[index].arguments +=
toolCall.function.arguments;
}
}
}
}
// If there are tool calls, execute them and continue the loop
if (currentToolCalls.length > 0) {
// Add assistant message with tool calls
openaiMessages.push({
role: "assistant",
content: assistantContent || null,
tool_calls: currentToolCalls.map((tc) => ({
id: tc.id,
type: "function" as const,
function: {
name: tc.name,
arguments: tc.arguments,
},
})),
});
// Execute each tool and add results
for (const toolCall of currentToolCalls) {
// Send tool call event to client
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: "tool_call",
id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
})}\n\n`,
),
);
let parsedArgs: unknown;
try {
parsedArgs = JSON.parse(toolCall.arguments);
} catch (parseError) {
const message =
parseError instanceof Error
? parseError.message
: "Invalid tool arguments";
// Surface parsing failure to the client and continue gracefully
const errorResult = JSON.stringify({
error: message,
});
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: "tool_result",
id: toolCall.id,
result: errorResult,
})}\n\n`,
),
);
openaiMessages.push({
role: "tool",
tool_call_id: toolCall.id,
content: errorResult,
});
continue;
}
const args = parsedArgs as Record<string, unknown>;
const result = await executeTool(toolCall.name, args);
// Send tool result event to client
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
type: "tool_result",
id: toolCall.id,
result,
})}\n\n`,
),
);
// Add tool result to messages
openaiMessages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});
}
// Reset for next iteration
currentToolCalls = [];
} else {
// No tool calls, we're done
continueLoop = false;
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}