This commit is contained in:
2026-05-13 14:58:53 +03:00
parent bd548a9f43
commit a411c6874a
3 changed files with 161 additions and 22 deletions
+153 -14
View File
@@ -5,14 +5,53 @@ import {getOpenAITools} from "./tool-mappers";
import {TelegramStreamMessage} from "./telegram-stream-message"; import {TelegramStreamMessage} from "./telegram-stream-message";
import {ToolRuntimeContext} from "./tools/runtime"; import {ToolRuntimeContext} from "./tools/runtime";
import {OpenAIChatMessage} from "./openai-chat-message"; import {OpenAIChatMessage} from "./openai-chat-message";
import type {ResponseCreateParamsNonStreaming, ResponseCreateParamsStreaming, ResponseInputItem, ResponseStreamEvent} from "openai/resources/responses/responses"; import type {
import type {ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming} from "openai/resources/chat/completions"; ResponseCreateParamsNonStreaming,
ResponseCreateParamsStreaming,
ResponseInputItem,
ResponseStreamEvent
} from "openai/resources/responses/responses";
import type {
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming
} from "openai/resources/chat/completions";
import {createGeminiOpenAiClient, createOpenAiClient} from "./ai-runtime-target"; import {createGeminiOpenAiClient, createOpenAiClient} from "./ai-runtime-target";
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger"; import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
import {AsyncIterableStream, MAX_TOOL_ROUNDS, OPENAI_IMAGE_PARTIALS, OpenAiChatCompletionResponseLike, OpenAiChatToolCallLike, OpenAiCompatibleChatMessage, OpenAiCompatibleContentPart, OpenAiResponseLike, OpenAiResponseOutputItem, RuntimeConfigSnapshot, ToolCallData, StreamingToolCallAccumulator, collectOpenAiResponseFunctionCalls, collectOpenAiResponseImages, collectOpenAiResponseText, executeToolBatch, getOpenAIResponsesToolsWithImage, openAiResponseItemCallId, safeJsonParseObject, showOpenAiGeneratedImage, ToolExecutionMemory, isRecord, roundStatus, OpenAiChatCompletionStreamChunkLike} from "./unified-ai-runner.shared"; import {
AsyncIterableStream,
collectOpenAiResponseFunctionCalls,
collectOpenAiResponseImages,
collectOpenAiResponseText,
executeToolBatch,
getOpenAIResponsesToolsWithImage,
isRecord,
MAX_TOOL_ROUNDS,
OPENAI_IMAGE_PARTIALS,
OpenAiChatCompletionResponseLike,
OpenAiChatCompletionStreamChunkLike,
OpenAiChatToolCallLike,
OpenAiCompatibleChatMessage,
OpenAiCompatibleContentPart,
openAiResponseItemCallId,
OpenAiResponseLike,
OpenAiResponseOutputItem,
roundStatus,
RuntimeConfigSnapshot,
safeJsonParseObject,
showOpenAiGeneratedImage,
StreamingToolCallAccumulator,
ToolCallData,
ToolExecutionMemory
} from "./unified-ai-runner.shared";
import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-file";
import {bot, notesDir} from "../index";
import fs from "node:fs";
import path from "node:path";
import {logError} from "../util/utils";
export async function runOpenAi( export async function runOpenAi(
msg: Message,
messages: OpenAIChatMessage[], messages: OpenAIChatMessage[],
streamMessage: TelegramStreamMessage, streamMessage: TelegramStreamMessage,
signal: AbortSignal, signal: AbortSignal,
@@ -90,6 +129,32 @@ export async function runOpenAi(
argumentsText: call.argumentsText, argumentsText: call.argumentsText,
})); }));
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory); const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
for (const toolResult of toolResults) {
try {
const raw = JSON.parse(toolResult);
const res = GetNoteFileResultSchema.safeParse(raw);
if (res.success && res.data.success) {
successGetNoteFileResult = res.data;
}
} catch {
// Not every tool result is JSON.
}
}
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
await bot.sendDocument({
chat_id: msg.chat.id,
reply_parameters: {
message_id: msg.message_id,
},
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
}).catch(logError);
}
const toolOutputs = calls.map((call, index) => ({ const toolOutputs = calls.map((call, index) => ({
type: "function_call_output" as const, type: "function_call_output" as const,
call_id: call.callId, call_id: call.callId,
@@ -228,6 +293,32 @@ export async function runOpenAi(
argumentsText: call.argumentsText, argumentsText: call.argumentsText,
})); }));
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory); const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
for (const toolResult of toolResults) {
try {
const raw = JSON.parse(toolResult);
const res = GetNoteFileResultSchema.safeParse(raw);
if (res.success && res.data.success) {
successGetNoteFileResult = res.data;
}
} catch {
// Not every tool result is JSON.
}
}
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
await bot.sendDocument({
chat_id: msg.chat.id,
reply_parameters: {
message_id: msg.message_id,
},
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
}).catch(logError);
}
const toolOutputs = calls.map((call, index) => ({ const toolOutputs = calls.map((call, index) => ({
type: "function_call_output", type: "function_call_output",
call_id: call.callId, call_id: call.callId,
@@ -298,6 +389,7 @@ async function appendOpenAiChatToolResults(
} }
export async function runOpenAiCompatibleChat( export async function runOpenAiCompatibleChat(
msg: Message,
messages: OpenAIChatMessage[], messages: OpenAIChatMessage[],
streamMessage: TelegramStreamMessage, streamMessage: TelegramStreamMessage,
signal: AbortSignal, signal: AbortSignal,
@@ -356,7 +448,35 @@ export async function runOpenAiCompatibleChat(
}, },
})), })),
}); });
await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
for (const toolResult of toolResults) {
try {
const raw = JSON.parse(toolResult);
const res = GetNoteFileResultSchema.safeParse(raw);
if (res.success && res.data.success) {
successGetNoteFileResult = res.data;
}
} catch {
// Not every tool result is JSON.
}
}
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
await bot.sendDocument({
chat_id: msg.chat.id,
reply_parameters: {
message_id: msg.message_id,
},
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
}).catch(logError);
}
await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
continue; continue;
} }
@@ -410,15 +530,34 @@ export async function runOpenAiCompatibleChat(
}, },
})), })),
}); });
await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
for (const toolResult of toolResults) {
try {
const raw = JSON.parse(toolResult);
const res = GetNoteFileResultSchema.safeParse(raw);
if (res.success && res.data.success) {
successGetNoteFileResult = res.data;
}
} catch {
// Not every tool result is JSON.
}
}
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
await bot.sendDocument({
chat_id: msg.chat.id,
reply_parameters: {
message_id: msg.message_id,
},
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
}).catch(logError);
}
await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
} }
} }
export class OpenAiProviderRunner {
static run = runOpenAi;
}
export class OpenAiCompatibleProviderRunner {
static run = runOpenAiCompatibleChat;
}
+6 -6
View File
@@ -27,7 +27,7 @@ import {
transcribeSpeechDownloads transcribeSpeechDownloads
} from "./speech-to-text"; } from "./speech-to-text";
import {OpenAIChatMessage} from "./openai-chat-message"; import {OpenAIChatMessage} from "./openai-chat-message";
import type {ResponseInputMessageContentList} from "openai/resources/responses/responses"; import type {ResponseInputContent, ResponseInputMessageContentList} from "openai/resources/responses/responses";
import type {ChatCompletionMessageParam} from "openai/resources/chat/completions"; import type {ChatCompletionMessageParam} from "openai/resources/chat/completions";
import type {GenerateContentParameters} from "@google/genai"; import type {GenerateContentParameters} from "@google/genai";
import {MistralChatMessage} from "./mistral-chat-message"; import {MistralChatMessage} from "./mistral-chat-message";
@@ -536,13 +536,13 @@ export function addMessageAttachmentKinds(msg: Message | undefined, kinds: Set<A
if (msg.video) kinds.add("video"); if (msg.video) kinds.add("video");
} }
export async function collectStoredReplyChainAttachments(msg: Message): Promise<StoredAttachment[]> { export async function collectStoredReplyChainAttachments(msg: Message, limit: number = 1): Promise<StoredAttachment[]> {
const attachments: StoredAttachment[] = []; const attachments: StoredAttachment[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
let current = await MessageStore.get(msg.chat.id, msg.message_id); let current = await MessageStore.get(msg.chat.id, msg.message_id);
for (let i = 0; current && i < 40; i++) { for (let i = 0; current && i < limit; i++) {
for (const attachment of current.attachments ?? []) { for (const attachment of current?.attachments ?? []) {
const key = [ const key = [
attachment.kind, attachment.kind,
attachment.fileUniqueId || attachment.fileId, attachment.fileUniqueId || attachment.fileId,
@@ -796,8 +796,8 @@ export function normalizeOllamaToolCalls(calls: readonly OllamaToolCallLike[] =
} }
export function buildOpenAiResponseMessage(part: MessagePart, getContent: (part: MessagePart) => string): OpenAIChatMessage { export function buildOpenAiResponseMessage(part: MessagePart, getContent: (part: MessagePart) => string): OpenAIChatMessage {
const content: ResponseInputMessageContentList = [{ const content: Array<ResponseInputContent | any> = [{
type: "input_text", type: part.bot ? "output_text" : "input_text",
text: getContent(part), text: getContent(part),
}]; }];
+2 -2
View File
@@ -137,7 +137,7 @@ async function executeUnifiedAiRequest(
switch (options.provider) { switch (options.provider) {
case AiProvider.OPENAI: case AiProvider.OPENAI:
await runOpenAi(chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, options.msg, config, toolContext); await runOpenAi(options.msg, chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, options.msg, config, toolContext);
break; break;
case AiProvider.OLLAMA: case AiProvider.OLLAMA:
const currentModel = config.ollamaChatTarget.model; const currentModel = config.ollamaChatTarget.model;
@@ -154,7 +154,7 @@ async function executeUnifiedAiRequest(
break; break;
case AiProvider.GEMINI: case AiProvider.GEMINI:
if (getGeminiApiMode(config.geminiChatTarget) === "openai") { if (getGeminiApiMode(config.geminiChatTarget) === "openai") {
await runOpenAiCompatibleChat(chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext); await runOpenAiCompatibleChat(options.msg, chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
} else { } else {
await runGemini(chatMessages as GeminiMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext); await runGemini(chatMessages as GeminiMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
} }