Summarize tool loop output
This commit is contained in:
+2
-2
@@ -81,12 +81,12 @@
|
|||||||
|
|
||||||
- [ ] Stage `model_call` должен делать только один model request.
|
- [ ] Stage `model_call` должен делать только один model request.
|
||||||
- [x] Stage `model_call` должен возвращать normalized model output.
|
- [x] Stage `model_call` должен возвращать normalized model output.
|
||||||
- [ ] Stage `tool_loop` должен решать, есть ли tool calls.
|
- [x] Stage `tool_loop` должен решать, есть ли tool calls.
|
||||||
- [ ] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`.
|
- [ ] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`.
|
||||||
- [ ] Stage `tool_loop` должен добавлять tool results в provider adapter.
|
- [ ] Stage `tool_loop` должен добавлять tool results в provider adapter.
|
||||||
- [ ] Stage `tool_loop` должен управлять max rounds.
|
- [ ] Stage `tool_loop` должен управлять max rounds.
|
||||||
- [ ] Stage `tool_loop` должен сохранять tool result artifacts.
|
- [ ] Stage `tool_loop` должен сохранять tool result artifacts.
|
||||||
- [ ] Stage `tool_loop` должен уметь завершаться без tools как `skipped`.
|
- [x] Stage `tool_loop` должен уметь завершаться без tools как `skipped`.
|
||||||
- [ ] Убрать tool loop из `runOpenAi`.
|
- [ ] Убрать tool loop из `runOpenAi`.
|
||||||
- [ ] Убрать tool loop из `runMistral`.
|
- [ ] Убрать tool loop из `runMistral`.
|
||||||
- [ ] Убрать tool loop из `runOllama`.
|
- [ ] Убрать tool loop из `runOllama`.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type {PipelineArtifact} from "./user-request-pipeline/types.js";
|
||||||
|
import type {TelegramOutputAttachmentRecord, TelegramToolExecutionRecord} from "./telegram-stream-message.js";
|
||||||
|
import {summarizeModelOutput} from "./response-model-output.js";
|
||||||
|
|
||||||
|
export type ToolLoopSummary = {
|
||||||
|
status: "succeeded" | "skipped";
|
||||||
|
fallbackAction?: "continue_without_stage";
|
||||||
|
details: {
|
||||||
|
modelOutput: ReturnType<typeof summarizeModelOutput>;
|
||||||
|
count: number;
|
||||||
|
tools: Array<{
|
||||||
|
toolName: string;
|
||||||
|
callId: string;
|
||||||
|
resultChars: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
artifacts?: PipelineArtifact[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function summarizeToolLoop(params: {
|
||||||
|
text: string;
|
||||||
|
executions: readonly TelegramToolExecutionRecord[];
|
||||||
|
outputAttachments: readonly TelegramOutputAttachmentRecord[];
|
||||||
|
}): ToolLoopSummary {
|
||||||
|
const count = params.executions.length;
|
||||||
|
const tools = params.executions.map(execution => ({
|
||||||
|
toolName: execution.toolName,
|
||||||
|
callId: execution.callId,
|
||||||
|
resultChars: execution.resultChars,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: count ? "succeeded" : "skipped",
|
||||||
|
fallbackAction: count ? undefined : "continue_without_stage",
|
||||||
|
details: {
|
||||||
|
modelOutput: summarizeModelOutput({
|
||||||
|
text: params.text,
|
||||||
|
toolExecutions: params.executions,
|
||||||
|
outputAttachments: params.outputAttachments,
|
||||||
|
}),
|
||||||
|
count,
|
||||||
|
tools,
|
||||||
|
},
|
||||||
|
artifacts: count ? [{
|
||||||
|
kind: "tool_result",
|
||||||
|
stage: "tool_loop",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
toolName: "summary",
|
||||||
|
callId: "tool_loop_summary",
|
||||||
|
resultText: JSON.stringify({
|
||||||
|
count,
|
||||||
|
tools,
|
||||||
|
}),
|
||||||
|
}] : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import {runOpenAi} from "./unified-ai-runner.openai";
|
|||||||
import {runOllama} from "./unified-ai-runner.ollama";
|
import {runOllama} from "./unified-ai-runner.ollama";
|
||||||
import {runMistral} from "./unified-ai-runner.mistral";
|
import {runMistral} from "./unified-ai-runner.mistral";
|
||||||
import {summarizeModelOutput} from "./response-model-output";
|
import {summarizeModelOutput} from "./response-model-output";
|
||||||
|
import {summarizeToolLoop} from "./tool-loop-summary";
|
||||||
import {
|
import {
|
||||||
resolveTextToSpeechProviderForUser,
|
resolveTextToSpeechProviderForUser,
|
||||||
sendSynthesizedSpeech,
|
sendSynthesizedSpeech,
|
||||||
@@ -269,38 +270,15 @@ export async function runUnifiedAiResponsePipeline(params: {
|
|||||||
name: "tool_loop",
|
name: "tool_loop",
|
||||||
async run() {
|
async run() {
|
||||||
const executions = streamMessage.getToolExecutions();
|
const executions = streamMessage.getToolExecutions();
|
||||||
|
const summary = summarizeToolLoop({
|
||||||
|
text: streamMessage.getText(),
|
||||||
|
executions,
|
||||||
|
outputAttachments: streamMessage.getOutputAttachments(),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stage: "tool_loop",
|
stage: "tool_loop",
|
||||||
status: executions.length ? "succeeded" : "skipped",
|
...summary,
|
||||||
fallbackAction: executions.length ? undefined : "continue_without_stage",
|
|
||||||
details: {
|
|
||||||
modelOutput: summarizeModelOutput({
|
|
||||||
text: streamMessage.getText(),
|
|
||||||
toolExecutions: executions,
|
|
||||||
outputAttachments: streamMessage.getOutputAttachments(),
|
|
||||||
}),
|
|
||||||
count: executions.length,
|
|
||||||
tools: executions.map(execution => ({
|
|
||||||
toolName: execution.toolName,
|
|
||||||
callId: execution.callId,
|
|
||||||
resultChars: execution.resultChars,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
artifacts: executions.length ? [{
|
|
||||||
kind: "tool_result",
|
|
||||||
stage: "tool_loop",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
toolName: "summary",
|
|
||||||
callId: "tool_loop_summary",
|
|
||||||
resultText: JSON.stringify({
|
|
||||||
count: executions.length,
|
|
||||||
tools: executions.map(execution => ({
|
|
||||||
toolName: execution.toolName,
|
|
||||||
callId: execution.callId,
|
|
||||||
resultChars: execution.resultChars,
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
}] : undefined,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
const {summarizeToolLoop} = await import("../dist/ai/tool-loop-summary.js");
|
||||||
|
|
||||||
|
test("tool loop summary skips empty tool execution batches", () => {
|
||||||
|
const summary = summarizeToolLoop({
|
||||||
|
text: "answer",
|
||||||
|
executions: [],
|
||||||
|
outputAttachments: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(summary.status, "skipped");
|
||||||
|
assert.equal(summary.fallbackAction, "continue_without_stage");
|
||||||
|
assert.equal(summary.details.count, 0);
|
||||||
|
assert.deepEqual(summary.details.tools, []);
|
||||||
|
assert.deepEqual(summary.details.modelOutput, {
|
||||||
|
text: "answer",
|
||||||
|
toolExecutions: [],
|
||||||
|
outputAttachments: [],
|
||||||
|
});
|
||||||
|
assert.equal(summary.artifacts, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tool loop summary reports executions and summary artifact", () => {
|
||||||
|
const summary = summarizeToolLoop({
|
||||||
|
text: "answer",
|
||||||
|
executions: [{
|
||||||
|
toolName: "read_file",
|
||||||
|
callId: "call-1",
|
||||||
|
argumentsText: "{}",
|
||||||
|
resultChars: 12,
|
||||||
|
}],
|
||||||
|
outputAttachments: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(summary.status, "succeeded");
|
||||||
|
assert.equal(summary.fallbackAction, undefined);
|
||||||
|
assert.equal(summary.details.count, 1);
|
||||||
|
assert.deepEqual(summary.details.tools, [{
|
||||||
|
toolName: "read_file",
|
||||||
|
callId: "call-1",
|
||||||
|
resultChars: 12,
|
||||||
|
}]);
|
||||||
|
assert.equal(summary.artifacts?.[0]?.kind, "tool_result");
|
||||||
|
assert.equal(summary.artifacts?.[0]?.stage, "tool_loop");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user