Merge reply-chain documents into AI requests
This commit is contained in:
@@ -32,6 +32,7 @@ export type ConversationTurn = {
|
||||
content: string;
|
||||
deletedByBotAt?: number | null;
|
||||
attachments: ConversationAttachment[];
|
||||
documentNames?: string[];
|
||||
};
|
||||
|
||||
export type ConversationSnapshot = {
|
||||
@@ -123,6 +124,13 @@ function attachmentSummary(attachments: ConversationAttachment[]): string {
|
||||
return ["[attachments]:", ...lines].join("\n");
|
||||
}
|
||||
|
||||
function namesSummary(kind: string, names: string[]): string {
|
||||
const filtered = names.map(name => name.trim()).filter(Boolean);
|
||||
if (!filtered.length) return "";
|
||||
|
||||
return [`[${kind}]:`, ...filtered.map(name => `- ${name}`)].join("\n");
|
||||
}
|
||||
|
||||
function supportedAttachmentKinds(provider: AiProvider, bot: boolean): Set<AttachmentKind> {
|
||||
if (bot) return new Set<AttachmentKind>();
|
||||
|
||||
@@ -160,6 +168,10 @@ function renderContentText(
|
||||
parts.push("[message_state]: deleted_by_bot");
|
||||
}
|
||||
|
||||
if (turn.documentNames?.length) {
|
||||
parts.push(namesSummary("documents", turn.documentNames));
|
||||
}
|
||||
|
||||
if (unsupported.length) {
|
||||
parts.push(attachmentSummary(unsupported));
|
||||
}
|
||||
@@ -291,6 +303,7 @@ export async function buildConversationSnapshot(
|
||||
content: part.content,
|
||||
deletedByBotAt: part.deletedByBotAt,
|
||||
attachments: buildConversationAttachments(part),
|
||||
documentNames: part.documentNames,
|
||||
}));
|
||||
|
||||
const imageCount = turns.reduce((sum, turn) => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type {AiDownloadedFile} from "./telegram-attachments.js";
|
||||
|
||||
function downloadKey(download: AiDownloadedFile): string {
|
||||
return [
|
||||
download.kind,
|
||||
download.fileId,
|
||||
download.sha256 ?? "",
|
||||
download.fileName,
|
||||
].join(":");
|
||||
}
|
||||
|
||||
export function mergeReplyChainDownloads(
|
||||
currentDownloads: readonly AiDownloadedFile[],
|
||||
replyChainDownloads: readonly AiDownloadedFile[],
|
||||
): AiDownloadedFile[] {
|
||||
const result: AiDownloadedFile[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const download of [...currentDownloads, ...replyChainDownloads]) {
|
||||
const key = downloadKey(download);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(download);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function shouldPreferCurrentDownloads(text: string, currentDownloads: readonly AiDownloadedFile[]): boolean {
|
||||
if (!currentDownloads.length) return false;
|
||||
|
||||
const normalized = text.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
|
||||
return normalized.includes("this file")
|
||||
|| normalized.includes("this document")
|
||||
|| normalized.includes("этот файл")
|
||||
|| normalized.includes("этот документ");
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import {Environment} from "../common/environment";
|
||||
import {UserRequestPipeline, type UserRequestPipelineState, type UserRequestPipelineStage} from "./user-request-pipeline";
|
||||
import {PipelineFallbackNotifier} from "./user-request-pipeline/fallback-notifier";
|
||||
import {buildToolRankFallbackTargetDetails} from "./user-request-pipeline/fallback-target-details";
|
||||
import type {AiDownloadedFile} from "./telegram-attachments";
|
||||
import {mergeReplyChainDownloads, shouldPreferCurrentDownloads} from "./reply-chain-downloads";
|
||||
import {attachmentsToDownloadedFiles, type AiDownloadedFile} from "./telegram-attachments";
|
||||
import type {TelegramStreamMessage} from "./telegram-stream-message";
|
||||
import type {ChatMessage} from "./chat-messages-types";
|
||||
import type {OpenAIChatMessage} from "./openai-chat-message";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
stripAudioFromRunnerMessages,
|
||||
toolRuntimeContextFromDownloads,
|
||||
transcribeAudioIfNeeded,
|
||||
collectStoredReplyChainAttachments,
|
||||
UnifiedRunOptions,
|
||||
} from "./unified-ai-runner.shared";
|
||||
import {aiLog} from "../logging/ai-logger";
|
||||
@@ -92,6 +94,12 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
controller: AbortController;
|
||||
}): Promise<PreparedUnifiedAiRequest> {
|
||||
const {options, config, downloads, streamMessage, controller} = params;
|
||||
const replyChainDownloads = shouldPreferCurrentDownloads(options.text, downloads)
|
||||
? downloads
|
||||
: mergeReplyChainDownloads(
|
||||
downloads,
|
||||
attachmentsToDownloadedFiles(await collectStoredReplyChainAttachments(options.msg)),
|
||||
);
|
||||
const prepared: MutablePreparedContext = {
|
||||
chatMessages: [],
|
||||
imageCount: 0,
|
||||
@@ -111,7 +119,7 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
details: {
|
||||
phase: "ai_request_prepare",
|
||||
provider: options.provider,
|
||||
downloads: downloads.map(download => ({
|
||||
downloads: replyChainDownloads.map(download => ({
|
||||
kind: download.kind,
|
||||
fileName: download.fileName,
|
||||
mimeType: download.mimeType,
|
||||
@@ -128,15 +136,15 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
options.msg,
|
||||
options.text,
|
||||
options.provider,
|
||||
downloads,
|
||||
replyChainDownloads,
|
||||
config,
|
||||
runtimeTargetFor(options, config),
|
||||
options.responseLanguage ?? DEFAULT_AI_RESPONSE_LANGUAGE,
|
||||
);
|
||||
prepared.chatMessages = collected.chatMessages as typeof prepared.chatMessages;
|
||||
prepared.imageCount = collected.imageCount;
|
||||
prepared.firstRoundStatus = initialStatus(downloads, prepared.imageCount);
|
||||
prepared.toolContext = toolRuntimeContextFromDownloads(downloads);
|
||||
prepared.firstRoundStatus = initialStatus(replyChainDownloads, prepared.imageCount);
|
||||
prepared.toolContext = toolRuntimeContextFromDownloads(replyChainDownloads);
|
||||
|
||||
return {
|
||||
stage: "collect_conversation_context",
|
||||
@@ -171,11 +179,11 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
prepared.transcript = await transcribeAudioIfNeeded(
|
||||
options.provider,
|
||||
options.msg.from?.id,
|
||||
downloads,
|
||||
replyChainDownloads,
|
||||
streamMessage,
|
||||
controller.signal,
|
||||
).catch(error => {
|
||||
if (downloads.some(isTranscribableAudioDownload)) throw error;
|
||||
if (replyChainDownloads.some(isTranscribableAudioDownload)) throw error;
|
||||
return "";
|
||||
});
|
||||
|
||||
@@ -190,7 +198,7 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
const transcriptArtifact = await persistTranscriptArtifactAttachment({
|
||||
provider: options.provider,
|
||||
transcript,
|
||||
downloads,
|
||||
downloads: replyChainDownloads,
|
||||
chatId: options.msg.chat.id,
|
||||
messageId: options.msg.message_id,
|
||||
});
|
||||
@@ -235,7 +243,7 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
|
||||
prepared.preparedDocumentRag = await prepareDocumentRag(
|
||||
options.provider,
|
||||
downloads,
|
||||
replyChainDownloads,
|
||||
prepared.chatMessages,
|
||||
streamMessage,
|
||||
config,
|
||||
@@ -246,7 +254,7 @@ export async function prepareUnifiedAiRequestPipeline(params: {
|
||||
const ragArtifact = await persistRagArtifactAttachment({
|
||||
provider: options.provider,
|
||||
prepared: prepared.preparedDocumentRag,
|
||||
downloads,
|
||||
downloads: replyChainDownloads,
|
||||
chatId: options.msg.chat.id,
|
||||
messageId: options.msg.message_id,
|
||||
details: prepared.preparedDocumentRag?.provider === AiProvider.OPENAI
|
||||
|
||||
@@ -34,7 +34,7 @@ import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../loggi
|
||||
import {buildConversationSnapshot, serializeConversationSnapshot} from "./conversation-pipeline.js";
|
||||
import type {ResponseInputMessageContentList} from "openai/resources/responses/responses";
|
||||
import {persistToolResultArtifactAttachment} from "./tool-result-artifact-store.js";
|
||||
import {filterUserVisibleStoredAttachments} from "../common/attachment-visibility.js";
|
||||
import {filterUserInputStoredAttachments} from "../common/attachment-visibility.js";
|
||||
|
||||
export type {Message} from "typescript-telegram-bot-api";
|
||||
export type {AiRuntimeTarget} from "./ai-runtime-target";
|
||||
@@ -515,13 +515,13 @@ export function addMessageAttachmentKinds(msg: Message | undefined, kinds: Set<A
|
||||
if (msg.video) kinds.add("video");
|
||||
}
|
||||
|
||||
export async function collectStoredReplyChainAttachments(msg: Message, limit: number = 1): Promise<StoredAttachment[]> {
|
||||
export async function collectStoredReplyChainAttachments(msg: Message, limit: number = 40): Promise<StoredAttachment[]> {
|
||||
const attachments: StoredAttachment[] = [];
|
||||
const seen = new Set<string>();
|
||||
let current = await MessageStore.get(msg.chat.id, msg.message_id);
|
||||
|
||||
for (let i = 0; current && i < limit; i++) {
|
||||
for (const attachment of filterUserVisibleStoredAttachments(current?.attachments ?? [])) {
|
||||
for (const attachment of filterUserInputStoredAttachments(current?.attachments ?? [])) {
|
||||
const key = [
|
||||
attachment.kind,
|
||||
attachment.fileUniqueId || attachment.fileId,
|
||||
|
||||
Reference in New Issue
Block a user