154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
buildOpenApiInstructionsWithTools,
|
|
runToolEnabledOpenApiChatLoop,
|
|
} from "../src/openapi-chat-tool-loop.js";
|
|
|
|
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const openApiChatSource = readFileSync(resolve(desktopRoot, "src/openapi-chat.ts"), "utf8");
|
|
const openApiChatPromptFlowsSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-prompt-flows.ts"), "utf8");
|
|
const openApiChatToolLoopSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-tool-loop.ts"), "utf8");
|
|
|
|
assert.match(openApiChatSource, /from "\.\/openapi-chat-prompt-flows(?:\.js)?"/, "openapi-chat must import the extracted prompt-flow seam.");
|
|
assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-tool-loop(?:\.js)?"/, "openapi-chat prompt flows must import the extracted tool loop seam.");
|
|
assert.match(openApiChatToolLoopSource, /export async function runToolEnabledOpenApiChatLoop/, "openapi-chat tool loop seam must export the async tool runner.");
|
|
assert.match(openApiChatToolLoopSource, /export function buildOpenApiInstructionsWithTools/, "openapi-chat tool loop seam must export tool-aware instruction shaping.");
|
|
|
|
assert.equal(
|
|
buildOpenApiInstructionsWithTools("Base instructions", [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "memory.save",
|
|
description: "Persist a note",
|
|
parameters: { type: "object" },
|
|
},
|
|
},
|
|
]),
|
|
"Base instructions\n\nYou have access to the following tools: memory.save. When you need to use a tool, respond with a tool call. The system will execute it and return the result. Do not make up tool results.",
|
|
"tool-aware instructions must append the available tool names and execution rule.",
|
|
);
|
|
|
|
const requestBodies: Array<Record<string, unknown>> = [];
|
|
const toolCalls: Array<{ name: string; args: unknown }> = [];
|
|
const responses = [
|
|
new Response(JSON.stringify({
|
|
id: "chatcmpl_tool",
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: "Checking memory",
|
|
tool_calls: [
|
|
{
|
|
id: "tool_1",
|
|
type: "function",
|
|
function: {
|
|
name: "memory.save",
|
|
arguments: "{\"note\":\"Remember the milk\"}",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})),
|
|
new Response(JSON.stringify({
|
|
id: "chatcmpl_final",
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: "Saved it for later.",
|
|
},
|
|
},
|
|
],
|
|
})),
|
|
];
|
|
|
|
const result = await runToolEnabledOpenApiChatLoop({
|
|
endpoint: "https://provider.example/v1/responses",
|
|
credential: "sk-test",
|
|
model: "gpt-test",
|
|
prompt: "Remember the milk",
|
|
systemContent: "You are FamiliarOS.",
|
|
history: [],
|
|
maxHistoryEntries: 24,
|
|
maxToolCallIterations: 5,
|
|
maxResponseOutputTokens: 256,
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "memory.save",
|
|
description: "Persist a note",
|
|
parameters: { type: "object" },
|
|
},
|
|
},
|
|
],
|
|
manager: {
|
|
async callTool(name: string, args: unknown) {
|
|
toolCalls.push({ name, args });
|
|
return "stored";
|
|
},
|
|
},
|
|
async fetchImpl(_input, init) {
|
|
assert.ok(init?.body, "tool loop must send a request body.");
|
|
requestBodies.push(JSON.parse(String(init.body)) as Record<string, unknown>);
|
|
const next = responses.shift();
|
|
assert.ok(next, "tool loop test ran out of stubbed responses.");
|
|
return next;
|
|
},
|
|
});
|
|
|
|
assert.equal(result.finalText, "Saved it for later.");
|
|
assert.equal(result.responseId, "chatcmpl_final");
|
|
assert.equal(result.iterationCount, 2);
|
|
assert.equal(result.endpoint, "https://provider.example/v1/chat/completions");
|
|
assert.equal(toolCalls.length, 1);
|
|
assert.deepEqual(toolCalls[0], {
|
|
name: "memory.save",
|
|
args: { note: "Remember the milk" },
|
|
});
|
|
assert.equal(requestBodies.length, 2);
|
|
assert.equal(requestBodies[0].tool_choice, "auto");
|
|
assert.equal(
|
|
(requestBodies[0].tools as Array<{ function: { name: string } }>)[0]?.function.name,
|
|
"memory.save",
|
|
"tool loop must forward the OpenAI tool definition payload.",
|
|
);
|
|
assert.deepEqual(
|
|
result.history,
|
|
[
|
|
{ role: "system", content: "You are FamiliarOS." },
|
|
{ role: "user", content: "Remember the milk" },
|
|
{
|
|
role: "assistant",
|
|
content: "Checking memory",
|
|
tool_calls: [
|
|
{
|
|
id: "tool_1",
|
|
type: "function",
|
|
function: {
|
|
name: "memory.save",
|
|
arguments: "{\"note\":\"Remember the milk\"}",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
tool_call_id: "tool_1",
|
|
content: "stored",
|
|
},
|
|
{
|
|
role: "assistant",
|
|
content: "Saved it for later.",
|
|
},
|
|
],
|
|
"tool loop must preserve the system/user/tool-call/tool-result/final-assistant history sequence.",
|
|
);
|
|
|
|
console.error("OpenAPI chat tool loop validation passed.");
|