Фото в вопросах, Gemini 3.8 Flash по умолчанию, команды /context и /clear
- Бот смотрит фото: подпись с упоминанием, /ask в подписи или ответом на фото, фото в личке - Картинки скачиваются через Bot API и передаются модели байтами (ссылка с токеном наружу не уходит) - OpenRouter по умолчанию — openrouter/google/gemini-3.8-flash; AI_IMAGE_INPUT для явного выбора - Модель без зрения получает пометку, что фото есть, но она его не видит - /context: сколько бот помнит и видит ли всю переписку (privacy mode / админ) - /clear: очистка сохранённой переписки чата с подтверждением Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,13 +14,31 @@ export type { Question } from "./prompts.js";
|
||||
|
||||
interface AssistantServiceDeps {
|
||||
model: string;
|
||||
/** Умеет ли модель смотреть изображения; если нет — фото ей не отправляем. */
|
||||
imageInput: boolean;
|
||||
chatHistoryService: ChatHistoryService;
|
||||
chatMemberService: ChatMemberService;
|
||||
}
|
||||
|
||||
const MAX_AGENT_STEPS = 6;
|
||||
|
||||
export const createAssistantService = ({ model, chatHistoryService, chatMemberService }: AssistantServiceDeps) => {
|
||||
/** Сообщение пользователя для модели: текст вопроса и, если модель умеет, картинки. */
|
||||
const buildUserMessage = (question: Question, imageInput: boolean) => ({
|
||||
role: "user" as const,
|
||||
content: [
|
||||
{ type: "text" as const, text: buildPrompt(question, imageInput) },
|
||||
...(imageInput
|
||||
? question.images.map((image) => ({ type: "image" as const, image: image.data, mimeType: image.mimeType }))
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
export const createAssistantService = ({
|
||||
model,
|
||||
imageInput,
|
||||
chatHistoryService,
|
||||
chatMemberService,
|
||||
}: AssistantServiceDeps) => {
|
||||
const systemRules = readPrompt("system.md");
|
||||
|
||||
const agent = new Agent({
|
||||
@@ -41,7 +59,7 @@ export const createAssistantService = ({ model, chatHistoryService, chatMemberSe
|
||||
|
||||
requestContext.set("chatId", question.chatId);
|
||||
|
||||
const result = await agent.generate(buildPrompt(question), {
|
||||
const result = await agent.generate([buildUserMessage(question, imageInput)], {
|
||||
instructions: buildInstructions(systemRules, question, persona),
|
||||
requestContext,
|
||||
maxSteps: MAX_AGENT_STEPS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChatMember, ChatMessage, Persona } from "../../types.js";
|
||||
import type { ChatMember, ChatMessage, ImageAttachment, Persona } from "../../types.js";
|
||||
import { formatChatMembers } from "./format-chat-members.js";
|
||||
import { formatChatMessages } from "./format-chat-messages.js";
|
||||
|
||||
@@ -14,8 +14,24 @@ export interface Question {
|
||||
mentionedMembers: ChatMember[];
|
||||
/** Упомянутые ники, которых бот в чате не встречал. */
|
||||
unknownUsernames: string[];
|
||||
/** Фото из вопроса и из сообщения, на которое ответили. */
|
||||
images: ImageAttachment[];
|
||||
}
|
||||
|
||||
const IMAGE_SOURCES: Record<ImageAttachment["source"], string> = {
|
||||
question: "из вопроса",
|
||||
replied: "из сообщения, на которое ответили",
|
||||
};
|
||||
|
||||
/** Подсказка модели, откуда картинки — или что посмотреть их она не может. */
|
||||
const imagesNote = (images: ImageAttachment[], canSeeImages: boolean): string | undefined => {
|
||||
if (images.length === 0) return undefined;
|
||||
|
||||
if (!canSeeImages) return "К сообщению приложено изображение, но ты не можешь его просмотреть — честно скажи об этом.";
|
||||
|
||||
return `Приложены изображения (по порядку): ${images.map((image) => IMAGE_SOURCES[image.source]).join(", ")}.`;
|
||||
};
|
||||
|
||||
const mentionsSection = (question: Question): string | undefined => {
|
||||
const known = question.mentionedMembers.length > 0 ? `Упомянуты участники:\n${formatChatMembers(question.mentionedMembers)}` : "";
|
||||
|
||||
@@ -38,7 +54,12 @@ export const buildInstructions = (systemRules: string, question: Question, perso
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
export const buildPrompt = (question: Question): string =>
|
||||
question.repliedTo
|
||||
? `Вопрос задан в ответ на сообщение:\n${formatChatMessages([question.repliedTo])}\n\nВопрос: ${question.text}`
|
||||
: question.text;
|
||||
export const buildPrompt = (question: Question, canSeeImages: boolean): string => {
|
||||
const replied = question.repliedTo
|
||||
? `Вопрос задан в ответ на сообщение:\n${formatChatMessages([question.repliedTo])}`
|
||||
: undefined;
|
||||
|
||||
const text = replied ? `Вопрос: ${question.text}` : question.text;
|
||||
|
||||
return [replied, imagesNote(question.images, canSeeImages), text].filter(Boolean).join("\n\n");
|
||||
};
|
||||
|
||||
@@ -19,7 +19,12 @@ export const createChatHistoryService = ({ messageRepository, historyLimit }: Ch
|
||||
const find = (chatId: number, messageId: number): ChatMessage | undefined =>
|
||||
messageRepository.findById(chatId, messageId);
|
||||
|
||||
return { record, latest, latestByUser, find };
|
||||
const stats = (chatId: number) => ({ ...messageRepository.statsByChat(chatId), limit: historyLimit });
|
||||
|
||||
/** Забыть переписку чата: модель больше не увидит сообщения, сохранённые до этого момента. */
|
||||
const clear = (chatId: number): number => messageRepository.deleteByChat(chatId);
|
||||
|
||||
return { record, latest, latestByUser, find, stats, clear };
|
||||
};
|
||||
|
||||
export type ChatHistoryService = ReturnType<typeof createChatHistoryService>;
|
||||
|
||||
Reference in New Issue
Block a user