feat(agent): add llm assistant chat with api tools (task 88)
OpenAI-compatible client with mock fallback, tool-calling loop, and /assistant UI. Tools map to /api/v1/ endpoints per ADR 0006 direct-tools decision.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { createLlmClient, type ChatMessage, type LlmClient } from "@/lib/llm";
|
||||
import { AGENT_SYSTEM_PROMPT, AGENT_TOOLS } from "../tools";
|
||||
import { createApiToolExecutor, type ToolExecutor } from "../tool-executor";
|
||||
|
||||
const MAX_TOOL_ROUNDS = 8;
|
||||
|
||||
export type ClientChatMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type AgentToolCallSummary = {
|
||||
name: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
export type AgentChatResult = {
|
||||
message: ClientChatMessage;
|
||||
toolCalls: AgentToolCallSummary[];
|
||||
};
|
||||
|
||||
export async function runAgentChat(options: {
|
||||
messages: ClientChatMessage[];
|
||||
request: Request;
|
||||
llm?: LlmClient;
|
||||
executeTool?: ToolExecutor;
|
||||
}): Promise<AgentChatResult> {
|
||||
const llm = options.llm ?? createLlmClient();
|
||||
const executeTool = options.executeTool ?? createApiToolExecutor(options.request);
|
||||
|
||||
const transcript: ChatMessage[] = [
|
||||
{ role: "system", content: AGENT_SYSTEM_PROMPT },
|
||||
...options.messages.map(
|
||||
(message): ChatMessage => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
const toolCalls: AgentToolCallSummary[] = [];
|
||||
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
||||
const completion = await llm.chatCompletion({
|
||||
messages: transcript,
|
||||
tools: AGENT_TOOLS,
|
||||
});
|
||||
|
||||
const assistantMessage = completion.message;
|
||||
transcript.push(assistantMessage);
|
||||
|
||||
if (!assistantMessage.tool_calls?.length) {
|
||||
return {
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: assistantMessage.content?.trim() || "I couldn't generate a response.",
|
||||
},
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
|
||||
for (const toolCall of assistantMessage.tool_calls) {
|
||||
let result: string;
|
||||
let status: number;
|
||||
|
||||
try {
|
||||
result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
||||
const parsed = JSON.parse(result) as { status?: number };
|
||||
status = typeof parsed.status === "number" ? parsed.status : 200;
|
||||
} catch (err) {
|
||||
status = 500;
|
||||
result = JSON.stringify({
|
||||
status: 500,
|
||||
body: { error: err instanceof Error ? err.message : "Tool execution failed" },
|
||||
});
|
||||
}
|
||||
|
||||
toolCalls.push({ name: toolCall.function.name, status });
|
||||
|
||||
transcript.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
content: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "I hit the tool-call limit for this request. Please try a simpler question.",
|
||||
},
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user