shitton
This commit is contained in:
+163
-40
@@ -1,9 +1,9 @@
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import fs, {openAsBlob} from "node:fs";
|
||||
import fs, {createReadStream, openAsBlob} from "node:fs";
|
||||
import path from "node:path";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {Environment} from "../common/environment";
|
||||
import {bot, photoGenDir} from "../index";
|
||||
import {bot, notesDir, photoGenDir} from "../index";
|
||||
import {clamp, collectReplyChainText, delay, ifTrue, logError, replyToMessage} from "../util/utils";
|
||||
import {MessageStore} from "../common/message-store";
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {AiDownloadedFile, attachmentsToDownloadedFiles, cleanupDownloads} from "
|
||||
import {getModelCapabilities, getRuntimeCapabilities} from "./provider-model-runtime";
|
||||
import {StoredAttachment} from "../model/stored-attachment";
|
||||
import {AiChatMessage, ChatMessage} from "./chat-messages-types";
|
||||
import {ChatRequest, ListResponse, Tool} from "ollama";
|
||||
import {ChatRequest, ListResponse, Ollama, Tool} from "ollama";
|
||||
import {executeToolCall, ToolRuntimeContext} from "./tools/runtime";
|
||||
import {MessageImagePart, MessagePart} from "../common/message-part";
|
||||
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
|
||||
@@ -29,7 +29,7 @@ import {getCurrentDateTimeTool} from "./tools/datetime";
|
||||
import {getMarketRatesTool} from "./tools/market-rates";
|
||||
import {getWeatherTool} from "./tools/weather";
|
||||
import {aiProviderRequestQueue, type AiRequestQueueTarget} from "./provider-request-queue";
|
||||
import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils";
|
||||
import {loadOllamaModel, RouterPlanSchema, unloadAllOllamaModels} from "./tools/utils";
|
||||
import {prepareOllamaDocumentRag} from "./ollama-rag";
|
||||
import {PYTHON_INTERPRETER_TOOL_NAME, pythonInterpreterToolPrompt} from "./tools/python-interpretator";
|
||||
import {
|
||||
@@ -47,12 +47,6 @@ import {
|
||||
resolveSpeechToTextProviderForUser,
|
||||
transcribeSpeechDownloads,
|
||||
} from "./speech-to-text";
|
||||
import {
|
||||
isTextToSpeechConfigured,
|
||||
resolveTextToSpeechProviderForUser,
|
||||
sendSynthesizedSpeech,
|
||||
synthesizeSpeech,
|
||||
} from "./text-to-speech";
|
||||
import {OpenAIChatMessage} from "./openai-chat-message";
|
||||
import {ResponseInputMessageContentList} from "openai/resources/responses/responses.js";
|
||||
import {MistralChatMessage} from "./mistral-chat-message";
|
||||
@@ -70,6 +64,7 @@ import {
|
||||
getGeminiApiMode,
|
||||
resolveAiRuntimeTarget,
|
||||
} from "./ai-runtime-target";
|
||||
import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-file";
|
||||
|
||||
const TELEGRAM_LIMIT = 4096;
|
||||
const MAX_TOOL_ROUNDS = 20;
|
||||
@@ -135,8 +130,10 @@ type RuntimeConfigSnapshot = {
|
||||
useNamesInPrompt: boolean;
|
||||
useSystemPrompt: boolean;
|
||||
systemPrompt?: string;
|
||||
rankerToolPrompt?: string;
|
||||
|
||||
ollamaChatTarget: AiRuntimeTarget;
|
||||
ollamaToolRankerTarget?: AiRuntimeTarget;
|
||||
ollamaVisionTarget: AiRuntimeTarget;
|
||||
ollamaThinkingTarget: AiRuntimeTarget;
|
||||
ollamaAudioTarget: AiRuntimeTarget;
|
||||
@@ -165,8 +162,15 @@ export function snapshotRuntimeConfig(): RuntimeConfigSnapshot {
|
||||
useSystemPrompt: Environment.USE_SYSTEM_PROMPT,
|
||||
|
||||
systemPrompt: Environment.SYSTEM_PROMPT,
|
||||
rankerToolPrompt: Environment.RANKER_TOOL_PROMPT,
|
||||
|
||||
ollamaChatTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat"),
|
||||
ollamaToolRankerTarget: {
|
||||
provider: AiProvider.OLLAMA,
|
||||
purpose: "tools",
|
||||
model: "gemma4:e2b",
|
||||
baseUrl: "http://meloda-zen.lan:11434"
|
||||
},
|
||||
ollamaVisionTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "vision"),
|
||||
ollamaThinkingTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "thinking"),
|
||||
ollamaAudioTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "audio"),
|
||||
@@ -379,8 +383,8 @@ export function ollamaModelNames(response: ListResponse): string[] {
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0);
|
||||
}
|
||||
|
||||
async function isOllamaModelActive(target: AiRuntimeTarget): Promise<boolean> {
|
||||
const active = await createOllamaClient(target).ps();
|
||||
async function isOllamaModelActive(ollama: Ollama, target: AiRuntimeTarget): Promise<boolean> {
|
||||
const active = await ollama.ps();
|
||||
return ollamaModelNames(active).includes(target.model);
|
||||
}
|
||||
|
||||
@@ -916,23 +920,21 @@ function appendTranscriptToChatMessages(chatMessages: AiChatMessage[], provider:
|
||||
}
|
||||
}
|
||||
|
||||
async function sendVoiceResponseIfNeeded(options: UnifiedRunOptions, downloads: AiDownloadedFile[], text: string): Promise<void> {
|
||||
if (!downloads.some(isTranscribableAudioDownload)) return;
|
||||
if (!options.msg.from?.id) return;
|
||||
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
try {
|
||||
const provider = isTextToSpeechConfigured(options.provider)
|
||||
? options.provider
|
||||
: (await resolveTextToSpeechProviderForUser(options.msg.from.id)).provider;
|
||||
const speech = await synthesizeSpeech({provider, text: trimmed});
|
||||
await sendSynthesizedSpeech(options.msg, speech);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
// async function sendVoiceResponseIfNeeded(options: UnifiedRunOptions, downloads: AiDownloadedFile[], text: string): Promise<void> {
|
||||
// if (!downloads.some(isTranscribableAudioDownload)) return;
|
||||
// if (!options.msg.from?.id) return;
|
||||
//
|
||||
// const trimmed = text.trim();
|
||||
// if (!trimmed) return;
|
||||
//
|
||||
// try {
|
||||
// const provider = (await resolveTextToSpeechProviderForUser(options.msg.from.id)).provider;
|
||||
// const speech = await synthesizeSpeech({provider, text: trimmed});
|
||||
// await sendSynthesizedSpeech(options.msg, speech);
|
||||
// } catch (e) {
|
||||
// logError(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
async function deleteMistralLibrary(libraryId: string | undefined, target: AiRuntimeTarget): Promise<void> {
|
||||
if (!libraryId) return;
|
||||
@@ -1429,9 +1431,24 @@ async function runOllama(
|
||||
const maxContextLength = contextKey ? <number>modelInfo?.model_info?.[contextKey] : DEFAULT_OLLAMA_CONTEXT_SIZE;
|
||||
const context = clamp(contextSize ?? contextSize === -1 ? MAX_OLLAMA_CONTEXT_SIZE : DEFAULT_OLLAMA_CONTEXT_SIZE, MIN_OLLAMA_CONTEXT_SIZE, maxContextLength ?? DEFAULT_OLLAMA_CONTEXT_SIZE);
|
||||
|
||||
await unloadAllOllamaModels(ollama, [model, config.ollamaDocumentsTarget.model]);
|
||||
const modelsToLoad = [model];
|
||||
|
||||
if (!(await isOllamaModelActive(target))) {
|
||||
try {
|
||||
const activeModels = (await ollama.ps()).models.map(m => m.model);
|
||||
const oldSet = new Set(activeModels);
|
||||
const newSet = new Set(modelsToLoad);
|
||||
|
||||
const added = modelsToLoad.filter(m => !oldSet.has(m));
|
||||
const removed = activeModels.filter(m => !newSet.has(m));
|
||||
const diff = [...added, ...removed];
|
||||
if (diff.length) {
|
||||
await unloadAllOllamaModels(ollama, modelsToLoad);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
if (!(await isOllamaModelActive(ollama, target))) {
|
||||
const currentStatus = streamMessage.getStatus();
|
||||
streamMessage.setStatus(Environment.getLoadingModelText(model));
|
||||
await streamMessage.flush();
|
||||
@@ -1468,27 +1485,97 @@ async function runOllama(
|
||||
|
||||
try {
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
console.log("CONTENT_LENGTH", context);
|
||||
console.log("CONTEXT_LENGTH", context);
|
||||
|
||||
const request: ChatRequest = {
|
||||
model: model,
|
||||
messages: messages,
|
||||
think: audioCount ? false : think,
|
||||
options: {
|
||||
temperature: messages.length <= 2 ? 0 : 0.6,
|
||||
temperature: messages.length <= 2 ? 0.6 : 0.6,
|
||||
num_ctx: context,
|
||||
}
|
||||
};
|
||||
|
||||
if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) {
|
||||
if (fromId !== Environment.CREATOR_ID) {
|
||||
request.tools = [
|
||||
const ollamaTools: Tool[] = fromId !== Environment.CREATOR_ID ?
|
||||
[
|
||||
getCurrentDateTimeTool,
|
||||
getMarketRatesTool,
|
||||
getWeatherTool
|
||||
];
|
||||
] : getOllamaTools() as Tool[];
|
||||
|
||||
if (!config.ollamaToolRankerTarget) {
|
||||
request.tools = ollamaTools;
|
||||
} else {
|
||||
request.tools = getOllamaTools() as Tool[];
|
||||
try {
|
||||
const toolRankerPrompt = config.rankerToolPrompt
|
||||
?.replace(
|
||||
"{{user_request}}",
|
||||
messages.reverse().find(m => m.role === "user")?.content ?? ""
|
||||
)
|
||||
?.replace(
|
||||
"{{tools}}",
|
||||
ollamaTools.map(t => JSON.stringify(t)).join("\n")
|
||||
);
|
||||
|
||||
streamMessage.setStatus("🦽 Подбираю лучшие инструменты...");
|
||||
await streamMessage.flush();
|
||||
|
||||
const client = createOllamaClient(config.ollamaToolRankerTarget);
|
||||
|
||||
const modelsToLoad = [config.ollamaToolRankerTarget.model];
|
||||
|
||||
const activeModels = (await client.ps()).models.map(m => m.model);
|
||||
const oldSet = new Set(activeModels);
|
||||
const newSet = new Set(modelsToLoad);
|
||||
|
||||
const added = modelsToLoad.filter(m => !oldSet.has(m));
|
||||
const removed = activeModels.filter(m => !newSet.has(m));
|
||||
const diff = [...added, ...removed];
|
||||
if (diff.length) {
|
||||
await unloadAllOllamaModels(client, modelsToLoad);
|
||||
}
|
||||
|
||||
const result = await client.generate({
|
||||
model: config.ollamaToolRankerTarget.model,
|
||||
stream: false,
|
||||
prompt: toolRankerPrompt ?? ""
|
||||
});
|
||||
|
||||
console.log("TOOL_RANKER_RESULT: ", result.response);
|
||||
|
||||
const raw = JSON.parse(result.response);
|
||||
const res = RouterPlanSchema.safeParse(raw);
|
||||
if (res.success) {
|
||||
const toolNames = res.data.s.flatMap(s => s.t);
|
||||
const tools = toolNames.map(n => ollamaTools.find(t => t.function.name === n) as Tool);
|
||||
// const tools = ollamaTools.filter(t => toolNames.includes(t.function.name ?? ""));
|
||||
request.tools = tools;
|
||||
|
||||
|
||||
const m = messages.reverse().find(m => m.role === "user");
|
||||
let content = m?.content ?? "";
|
||||
content += "\n" + JSON.stringify({
|
||||
toolPlan: {
|
||||
steps: res.data.s.map(s => {
|
||||
return {
|
||||
tool_name: s.t,
|
||||
hint: s.h,
|
||||
from: s.from,
|
||||
};
|
||||
}),
|
||||
missing_info: res.data.m
|
||||
}
|
||||
});
|
||||
|
||||
if (m) m.content = content;
|
||||
// messages.reverse().find(m => m.role === "user")?.content += "";
|
||||
// messages[messages.length - 1].content = content;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1616,7 +1703,42 @@ async function runOllama(
|
||||
}))
|
||||
});
|
||||
|
||||
appendOllamaToolResults(messages, calls, await executeToolBatch(calls, streamMessage, toolContext));
|
||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext);
|
||||
|
||||
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 (e: unknown) {
|
||||
await replyToMessage({message: msg, text: e + ""}).catch(logError);
|
||||
}
|
||||
}
|
||||
|
||||
if (successGetNoteFileResult) {
|
||||
await bot.sendDocument({
|
||||
chat_id: msg.chat.id,
|
||||
reply_parameters: {
|
||||
message_id: msg.message_id
|
||||
},
|
||||
document: createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath))
|
||||
}).catch(logError);
|
||||
}
|
||||
|
||||
const reversedMessages = messages.reverse();
|
||||
let lastUserMessageIndex = reversedMessages.findIndex(m => m.role === "user");
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
lastUserMessageIndex = messages.indexOf(reversedMessages[lastUserMessageIndex]);
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
messages[lastUserMessageIndex].content += "\n\nLast tool_call result: " + JSON.stringify(toolResults);
|
||||
}
|
||||
}
|
||||
|
||||
appendOllamaToolResults(messages, calls, toolResults);
|
||||
}
|
||||
} finally {
|
||||
if (interval) clearInterval(interval);
|
||||
@@ -1990,7 +2112,8 @@ async function executeUnifiedAiRequest(
|
||||
|
||||
if (transcript.trim()) {
|
||||
if (options.voiceMode === AI_VOICE_MODE_TRANSCRIPT) {
|
||||
streamMessage.replaceText(transcript.trim());
|
||||
// TODO: 12.05.2026, Danil Nikolaev: extract to string
|
||||
streamMessage.replaceText(`[Расшифровка]\n${transcript.trim()}`);
|
||||
await streamMessage.finish();
|
||||
return {mistralLibraryId};
|
||||
}
|
||||
@@ -2057,7 +2180,7 @@ async function executeUnifiedAiRequest(
|
||||
streamMessage.replaceText(streamMessage.getText().slice(0, TELEGRAM_LIMIT - 3) + "...");
|
||||
}
|
||||
await streamMessage.finish();
|
||||
await sendVoiceResponseIfNeeded(options, downloads, streamMessage.getText());
|
||||
// await sendVoiceResponseIfNeeded(options, downloads, streamMessage.getText());
|
||||
|
||||
return {mistralLibraryId};
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user