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,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("assistant chat smoke", async ({ page }) => {
|
||||
await page.goto("/assistant");
|
||||
await expect(page.getByRole("heading", { name: "Assistant" })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Message").fill("hello assistant");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
|
||||
await expect(page.getByText("You")).toBeVisible();
|
||||
await expect(page.getByText("hello assistant")).toBeVisible();
|
||||
await expect(page.getByText("Assistant", { exact: true }).nth(1)).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { createMockLlmClient } from "../../src/lib/llm/mock";
|
||||
import { getLlmConfig } from "../../src/lib/llm/config";
|
||||
import { runAgentChat } from "../../src/modules/agent/server/run";
|
||||
|
||||
describe("mock llm provider", () => {
|
||||
it("returns a plain assistant message by default", async () => {
|
||||
const client = createMockLlmClient();
|
||||
const result = await client.chatCompletion({
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
tools: [],
|
||||
});
|
||||
assert.equal(result.message.role, "assistant");
|
||||
assert.ok(result.message.content?.includes("mock provider"));
|
||||
});
|
||||
|
||||
it("requests add_list_item when the user mentions milk", async () => {
|
||||
const client = createMockLlmClient();
|
||||
const result = await client.chatCompletion({
|
||||
messages: [{ role: "user", content: "add milk to shopping list" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "add_list_item",
|
||||
description: "add",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(result.message.tool_calls?.[0]?.function.name, "add_list_item");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLlmConfig", () => {
|
||||
it("uses mock provider when base url is unset", () => {
|
||||
const original = process.env.LLM_BASE_URL;
|
||||
const originalProvider = process.env.LLM_PROVIDER;
|
||||
delete process.env.LLM_BASE_URL;
|
||||
delete process.env.LLM_PROVIDER;
|
||||
|
||||
const config = getLlmConfig();
|
||||
assert.equal(config.provider, "mock");
|
||||
|
||||
if (original === undefined) delete process.env.LLM_BASE_URL;
|
||||
else process.env.LLM_BASE_URL = original;
|
||||
if (originalProvider === undefined) delete process.env.LLM_PROVIDER;
|
||||
else process.env.LLM_PROVIDER = originalProvider;
|
||||
});
|
||||
});
|
||||
|
||||
describe("runAgentChat", () => {
|
||||
it("executes tool calls and returns a final assistant message", async () => {
|
||||
const executed: string[] = [];
|
||||
const result = await runAgentChat({
|
||||
messages: [{ role: "user", content: "add milk to the shopping list" }],
|
||||
request: new Request("http://localhost:3000/api/agent/chat"),
|
||||
llm: createMockLlmClient(),
|
||||
executeTool: async (name) => {
|
||||
executed.push(name);
|
||||
return JSON.stringify({ status: 201, body: { id: "item-1", text: "milk" } });
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(executed, ["add_list_item"]);
|
||||
assert.equal(result.message.role, "assistant");
|
||||
assert.ok(result.message.content.length > 0);
|
||||
assert.equal(result.toolCalls.length, 1);
|
||||
assert.equal(result.toolCalls[0]?.name, "add_list_item");
|
||||
assert.equal(result.toolCalls[0]?.status, 201);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user