Files
tg-chat-bot/src/ai/unified-ai-runner.mistral.ts
T
2026-05-18 13:31:37 +03:00

192 lines
7.8 KiB
TypeScript

import {Environment} from "../common/environment";
import {getMistralTools} from "./tool-mappers";
import {TelegramStreamMessage} from "./telegram-stream-message";
import {ToolRuntimeContext} from "./tools/runtime";
import {MistralChatMessage} from "./mistral-chat-message";
import {createMistralClient} from "./ai-runtime-target";
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
import {AiProvider} from "../model/ai-provider";
import {ToolRanker} from "./unified-ai-runner.tool-ranker";
import {
contentFromMistralDelta,
executeToolBatch,
MAX_TOOL_ROUNDS,
MistralDeltaLike,
MistralDocumentReference,
mistralToolCalls,
normalizeMistralToolCalls,
roundStatus,
RuntimeConfigSnapshot,
StreamingToolCallAccumulator,
ToolCallData,
ToolExecutionMemory
} from "./unified-ai-runner.shared";
import {Message} from "typescript-telegram-bot-api";
import {filterRankedTools, latestUserTextFromMessages} from "./tool-ranker-pipeline";
import {storeToolRankAudit} from "./tool-rank-audit";
export async function runMistral(
msg: Message,
messages: MistralChatMessage[],
documents: MistralDocumentReference[],
streamMessage: TelegramStreamMessage,
signal: AbortSignal,
stream: boolean,
firstRoundStatus: string,
config: RuntimeConfigSnapshot,
toolContext: ToolRuntimeContext,
): Promise<void> {
const runnerStartedAt = Date.now();
const mistralAi = createMistralClient(config.mistralChatTarget);
const toolRanker = new ToolRanker(config);
const availableTools = getMistralTools(msg.from?.id === Environment.CREATOR_ID);
aiLog("info", "mistral.run.start", {
stream,
target: aiLogProviderTarget(config.mistralChatTarget),
inputMessages: messages.length,
documents: documents.length,
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
});
const toolMemory: ToolExecutionMemory = new Map();
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const roundStartedAt = Date.now();
aiLog("debug", "mistral.round.start", {round, messages: messages.length, stream});
if (signal.aborted) throw new Error("Aborted");
streamMessage.setStatus(Environment.getSelectingToolsText());
await streamMessage.flush();
const toolRankStartedAt = Date.now();
const toolRankStartedAtIso = new Date().toISOString();
const rankerSelection = await toolRanker.selectTools({
provider: AiProvider.MISTRAL,
userQuery: latestUserTextFromMessages(messages),
availableTools,
round,
signal,
})
.catch(async error => {
streamMessage.clearStatus();
await streamMessage.flush();
await storeToolRankAudit({
streamMessage,
provider: AiProvider.MISTRAL,
model: config.mistralChatTarget.model,
round,
startedAt: toolRankStartedAt,
startedAtIso: toolRankStartedAtIso,
error,
});
throw error;
});
streamMessage.clearStatus();
await streamMessage.flush();
await storeToolRankAudit({
streamMessage,
provider: AiProvider.MISTRAL,
model: config.mistralChatTarget.model,
round,
startedAt: toolRankStartedAt,
startedAtIso: toolRankStartedAtIso,
selectedTools: rankerSelection.toolNames,
});
const filteredTools = filterRankedTools(availableTools, rankerSelection.toolNames);
const requestTools = filteredTools.length ? filteredTools : undefined;
streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
await streamMessage.flush();
if (!stream) {
const request = {
model: config.mistralChatTarget.model,
messages,
tools: requestTools,
documents: documents
} as Parameters<typeof mistralAi.chat.complete>[0];
const response = await mistralAi.chat.complete(request, {signal});
const message = response.choices?.[0]?.message;
const text = typeof message?.content === "string" ? message.content : JSON.stringify(message?.content ?? "");
streamMessage.append(text);
const calls = normalizeMistralToolCalls(mistralToolCalls(message));
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
round,
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
textChars: text.length,
calls: calls.map(aiLogToolCall),
});
if (!calls.length) return;
messages.push({
role: "assistant",
content: text,
toolCalls: calls.map(call => ({
id: call.id,
function: {name: call.name, arguments: call.argumentsText},
})),
});
const toolResults = await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory);
for (const [index, call] of calls.entries()) {
messages.push({
role: "tool",
name: call.name,
toolCallId: call.id,
content: toolResults[index] ?? "",
});
}
continue;
}
const request = {
model: config.mistralChatTarget.model,
messages,
tools: requestTools,
documents: documents
} as Parameters<typeof mistralAi.chat.stream>[0];
const streamResponse = await mistralAi.chat.stream(request, {signal});
aiLog("debug", "mistral.stream.open", {round});
let calls: ToolCallData[] = [];
const roundTextStart = streamMessage.getText().length;
const toolCallAccumulator = new StreamingToolCallAccumulator("mistral_stream", round);
for await (const event of streamResponse) {
if (signal.aborted) throw new Error("Aborted");
const choice = event.data?.choices?.[0];
const delta = choice?.delta;
const mistralDelta = delta as MistralDeltaLike;
streamMessage.append(contentFromMistralDelta(mistralDelta));
const rawDeltaCalls = mistralToolCalls(mistralDelta);
if (rawDeltaCalls.length) {
calls = toolCallAccumulator.add(rawDeltaCalls);
streamMessage.setStatus(Environment.getUseToolText(calls));
await streamMessage.flush();
}
}
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
round,
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
textChars: streamMessage.getText().slice(roundTextStart).length,
calls: calls.map(aiLogToolCall),
});
if (!calls.length) return;
const roundText = streamMessage.getText().slice(roundTextStart);
messages.push({
role: "assistant",
content: roundText,
toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}}))
});
const toolResults = await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory);
for (const [index, call] of calls.entries()) {
messages.push({
role: "tool",
name: call.name,
toolCallId: call.id,
content: toolResults[index] ?? "",
});
}
}
}