diff --git a/PIPELINE_TODO.md b/PIPELINE_TODO.md index dcd0fec..75e945b 100644 --- a/PIPELINE_TODO.md +++ b/PIPELINE_TODO.md @@ -80,7 +80,7 @@ ## 6. Сделать model_call и tool_loop физически отдельными stages - [ ] Stage `model_call` должен делать только один model request. -- [ ] Stage `model_call` должен возвращать normalized model output. +- [x] Stage `model_call` должен возвращать normalized model output. - [ ] Stage `tool_loop` должен решать, есть ли tool calls. - [ ] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`. - [ ] Stage `tool_loop` должен добавлять tool results в provider adapter. diff --git a/src/ai/response-model-output.ts b/src/ai/response-model-output.ts new file mode 100644 index 0000000..ba1200b --- /dev/null +++ b/src/ai/response-model-output.ts @@ -0,0 +1,19 @@ +import type {TelegramOutputAttachmentRecord, TelegramToolExecutionRecord} from "./telegram-stream-message.js"; + +export type NormalizedModelOutput = { + text: string; + toolExecutions: TelegramToolExecutionRecord[]; + outputAttachments: TelegramOutputAttachmentRecord[]; +}; + +export function summarizeModelOutput(params: { + text: string; + toolExecutions: readonly TelegramToolExecutionRecord[]; + outputAttachments: readonly TelegramOutputAttachmentRecord[]; +}): NormalizedModelOutput { + return { + text: params.text.trim(), + toolExecutions: [...params.toolExecutions], + outputAttachments: [...params.outputAttachments], + }; +} diff --git a/src/ai/unified-ai-response-pipeline.ts b/src/ai/unified-ai-response-pipeline.ts index df71733..cba1363 100644 --- a/src/ai/unified-ai-response-pipeline.ts +++ b/src/ai/unified-ai-response-pipeline.ts @@ -21,6 +21,7 @@ import {runToolRankStage} from "./tool-rank-stage"; import {runOpenAi} from "./unified-ai-runner.openai"; import {runOllama} from "./unified-ai-runner.ollama"; import {runMistral} from "./unified-ai-runner.mistral"; +import {summarizeModelOutput} from "./response-model-output"; import { resolveTextToSpeechProviderForUser, sendSynthesizedSpeech, @@ -254,6 +255,13 @@ export async function runUnifiedAiResponsePipeline(params: { return { stage: "model_call", status: "succeeded", + details: { + modelOutput: summarizeModelOutput({ + text: streamMessage.getText(), + toolExecutions: streamMessage.getToolExecutions(), + outputAttachments: streamMessage.getOutputAttachments(), + }), + }, }; }, }, @@ -266,6 +274,11 @@ export async function runUnifiedAiResponsePipeline(params: { status: executions.length ? "succeeded" : "skipped", 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, diff --git a/test/response-model-output.test.mjs b/test/response-model-output.test.mjs new file mode 100644 index 0000000..14b8069 --- /dev/null +++ b/test/response-model-output.test.mjs @@ -0,0 +1,21 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const {summarizeModelOutput} = await import("../dist/ai/response-model-output.js"); + +test("model output summary trims text and copies attachment records", () => { + const toolExecutions = [{toolName: "read_file", callId: "1", argumentsText: "{}", resultChars: 10}]; + const outputAttachments = [{artifactKind: "generated_file", fileName: "out.txt", sizeBytes: 12}]; + + const summary = summarizeModelOutput({ + text: " hello world ", + toolExecutions, + outputAttachments, + }); + + assert.deepEqual(summary, { + text: "hello world", + toolExecutions, + outputAttachments, + }); +});