@ykurilov/agent-chat-attachments
v0.75.0
Published
Вложения к сообщению: картинки, PDF и текстовые файлы. Отдельное расширение: приложению, которому вложения не нужны, они не нужны и в сборке. Без пакета чат остаётся текстовым, договор остальных пакетов не меняется.
Downloads
618
Readme
@ykurilov/agent-chat-attachments
Вложения к сообщению: картинки, PDF и текстовые файлы. Отдельное расширение: приложению, которому вложения не нужны, они не нужны и в сборке. Без пакета чат остаётся текстовым, договор остальных пакетов не меняется.
Серверу без экрана React не нужен: react и @ykurilov/agent-chat-react — необязательные зависимости (peer). Для @ykurilov/agent-chat-attachments/react
оба должны стоять в приложении, как и для самого чата на экране.
// сервер
import { createAttachments, createDiskAttachmentStorage } from "@ykurilov/agent-chat-attachments";
const attachments = createAttachments({ storage: createDiskAttachmentStorage({ directory: "./data/attachments" }) });
createChatHandler({
…,
attachments,
agent: { instructions: appInstructions },
});// браузер
import { attachmentsClient } from "@ykurilov/agent-chat-attachments/react";
<AgentChat api="/api/chat" chatId={chatId} skin={tandemSkin} attachments={attachmentsClient} />;Что умеет
Типы: картинки (PNG, JPEG, WebP, GIF), PDF, текстовые файлы (
.txt,.md,.csv,.jsonи подобные). Тип определяется по содержимому, а не по имени и не по тому, что назвал браузер. Всё остальное отклоняется понятным отказом.Типы можно сузить:
createAttachments({ acceptedMediaTypes: ["image/png", "image/jpeg", "image/webp"] }). Файл другого типа отклоняется отказом 415unsupported_typeдо ужатия, разбора и записи (и при загрузке, и вingestFile, и при импорте). Файл, загруженный раньше, при отправке сообщения тоже отклоняется: «подойдут только PNG, JPEG, WebP». Поле ввода получает список изGET …/attachments/settingsи файл чужого типа (в том числе вставленный из буфера) отклоняет сразу, без загрузки. Тип, которого SDK не распознаёт (напримерimage/jpg), — ошибка при запуске.Картинки уходят только моделям, которые их видят у выбранного провайдера (
modelAcceptsImages(profile, providerId)из ядра:acceptsImagesByProviderпрофиля, иначеacceptsImages). Если модель не видит, человек узнаёт об этом до отправки, а модель получает в запросе пометку «картинка не показана».Модель без зрения смотрит картинку инструментом (
imagesForBlindModels, по умолчанию выключено). Вместо картинки в сообщении — постоянная плашка с номером вложения, а инструмент приложения читает картинку этого чата и отдаёт её зрячей модели:const attachments = createAttachments({ storage, acceptedMediaTypes: ["image/png", "image/jpeg", "image/webp"], imagesForBlindModels: { toolName: "look_at_attachment" }, // свой текст: marker: ({ id, name, mediaType, authorId }) => `[Картинка от Юрия «${name}», вложение ${id}. Посмотреть: look_at_attachment.]` }); // инструмент приложения (agent.tools — функция от выбранной модели, в ней есть chatId): const image = await attachments.readChatImage({ chatId: model.chatId, attachmentId }); if (!image) return { error: "В этом чате нет такой картинки" }; // чужой чат, не картинка, неверный номер // image: { id, name, mediaType, authorId, bytes }Плашка по умолчанию:
[Картинка «shot.png», вложение <id>. Посмотреть: look_at_attachment.]. Она собирается только из неизменной записи вложения, поэтому одинакова байт в байт на каждом ходе и после смены модели: кэш модели не сбивается. Свойmarkerтоже должен зависеть только от входа (без дат и счётчиков). Зрячей паре картинка уходит как раньше;always: true— плашка всем моделям. К инструкции вложений дописывается постоянная строка про плашку и инструмент. Поле ввода в этом режиме не предупреждает «модель не видит картинки». Сам инструмент (какая модель смотрит, сколько стоит) пишет приложение.readChatImageпроверяет только чат: картинку отдаст, кто бы в этом чате её ни загрузил. Если в чате несколько людей и кому-то нельзя смотреть чужие картинки, проверку автора (image.authorId) делает приложение в своём инструменте. Безalwaysсмена пары «модель + провайдер» между зрячей и слепой меняет историю (плашка ↔ картинка) и сбрасывает кэш модели; нужен ровный кэш при смене моделей — ставьтеalways: true.Только нужные типы (
acceptedMediaTypes). Решает тип, с которым файл пришёл: ужатый в JPEG большой PNG при["image/png"]проходит. Он записан в полеsourceMediaTypeзаписи вложения — своё хранилище должно его сохранять. Файл чужого типа, загруженный до сужения списка, при отправке получает отказunsupported_typeс текстом «Такой файл прикрепить нельзя: подойдут только PNG, JPEG, WebP.» (список — по настройке). Импорт чата (importChat) вместо картинки неподходящего типа пишет текст «[картинка: имя]».isAttachmentId(value)— номер вложения ли это (UUID): проверка до похода в хранилище.Большие фото уменьшаются при загрузке: картинка поворачивается по EXIF и ужимается до 2000×2000 пикселей и ~4,5 МБ в base64 (запас ниже 5 МБ строгих провайдеров). Хранится и показывается уменьшенная копия, оригинал не сохраняется. Картинки, загруженные до этого, ужимаются при сборке запроса (последние 20 решений запоминаются, повторно не перекодируются). Нечитаемая картинка и картинка больше 60 млн пикселей при загрузке — отказ 422
unreadable, в старой истории — пометка «картинка пропущена». У большой анимации (GIF, WebP) остаётся первый кадр в PNG или JPEG; имя файла не меняется. Обработку делает sharp: готовые сборки для macOS, Linux (glibc и Alpine/musl) и Windows ставятся сами, компилятор не нужен. В Docker ставьте зависимости внутри образа (pnpm install), а не копируйтеnode_modulesс Mac: сборка sharp у каждой системы своя.PDF и текст: текст достаётся на сервере при загрузке один раз и идёт в запрос блоком
<attachment name="…" type="…">…</attachment>любой модели. PDF без текста (скан) отклоняется с объяснением.Лимиты (
createAttachments({ limits })): 10 файлов на сообщение, 20 МБ на файл, 25 000 токенов текста на файл (лишнее обрезается с пометкой), 100 000 токенов текста на сообщение. На человека: не больше 30 неотправленных файлов и 200 МБ (maxUnsentFiles,maxUnsentBytes, ответ 429too_many_unsent); одновременных загрузок не больше 8 на сервер и 3 на человека (maxParallelUploads,maxParallelUploadsPerUser, ответ 429busy). Файл с ошибкой в лимит не входит. Поле ввода грузит по два файла сразу, остальные ждут в очереди; наbusyповторяет само. Значения по умолчанию —defaultAttachmentLimits.Ужатие при загрузке идёт по очереди: разом не больше половины ядер процессора (
maxParallelImageFits), остальные ждут.Старые большие картинки (загруженные до того, как сервер стал их уменьшать) уменьшаются один раз и держатся в памяти: до 128 МБ уменьшенных копий (
imageCacheBytes,0— не хранить), сверх — уходят давно не нужные.Разбор PDF идёт в отдельном потоке с потолком памяти и сроком: зависший или «раздутый» файл обрывается, сервер живёт дальше. Сбор текста останавливается, как только набран потолок символов. Текстовые логи с цветовыми кодами терминала принимаются как текст.
Хранилище файлов заменяемое (
AttachmentStorage): из коробкиcreateDiskAttachmentStorageиcreateMemoryAttachmentStorage. Своё (например, S3) проверяется общим наборомdescribeAttachmentStorageContractиз@ykurilov/agent-chat-attachments/storage-contract. Схема базы чата не меняется, миграции не нужны. МетодremoveAllOfChat(chatId)удаляет все файлы чата: его вызывает удаление чата из списка чатов.Безопасность: файл привязан к чату и владельцу, чужой и несуществующий файл отвечают одинаково (404). Адрес в хранилище — серверный UUID, имя файла в путь не попадает. Файлы отдаются с
nosniffиContent-Security-Policy: sandbox. Ключи провайдеров в браузер не уходят.Кэш модели: вложения — часть сообщения человека, извлечённый текст не меняется от хода к ходу, поэтому начало запроса остаётся постоянным.
Сжатие истории: сводка называет прикреплённые файлы по имени, оригиналы остаются в хранилище.
Загруженные, но не отправленные файлы убираются сами через 24 часа (проверка раз в 15 минут), вместе с недописанными остатками на диске. Файл, который пролежал дольше и не был отправлен, возвращается в поле с пометкой «загрузите снова». Файл считается прикреплённым только после того, как сообщение записано; если отправка отклонена, текст и файлы остаются в поле.
Импорт старой переписки:
attachments.importFile({ ownerId, chatId, bytes, mediaType, filename? })сохраняет картинку из старой системы; его зовётservice.importChatядра сам, если вложения подключены к чату.Инструкция агенту про вложения (
attachmentsInstructions) ядро добавляет само, вручную дописывать не нужно (дубль не появится).Служебный формат
<attachment …>(attachments.serviceBlocks): если модель повторит текст файла этим блоком в ответе, ядро его вырежет до экрана и записи.
Адреса
Под адресом чата (/api/chat): GET /attachments/settings (лимиты и допустимые типы для браузера),
POST /:chatId/attachments (поле формы file), GET и DELETE /:chatId/attachments/:id (удалить можно только неотправленный файл).
Со своим скином
Composer получает необязательное attachments (ComposerAttachments): скрепка, перетаскивание и вставка файлов, превью с удалением,
notice с готовой надписью. UserMessage получает необязательное attachments (MessageAttachmentsView): миниатюры картинок и плашки файлов.
Нет свойства — вложений нет, ничего про них не рисовать. Надписи пакета — russianAttachmentsLabels, английские — englishAttachmentsLabels, свои передаются в
createAttachmentsClient(labels).
