Вложения

Сообщение может содержать файлы. Поле ввода сообщений собирает их, приложение сохраняет, список сообщений отображает, а нажатие на вложение извлекает файл из хранилища. Вложения затрагивают компоненты aiChat, aiMessageInput и aiMessageList, поэтому они описаны здесь, а не на странице одного из этих компонентов.

Включение вложений

По умолчанию вложения отключены. Включите их и задайте ограничения в чате или в поле ввода сообщений, если оно используется отдельно:

<aichat:aiChat id="chat"
               dataContainer="messagesDc"
               autoSave="true"
               width="100%"
               height="100%"
               attachmentsEnabled="true"
               maxFiles="3"
               maxFileSize="5242880"
               acceptedFileExtensions=".png,.jpg,.pdf,.txt"
               uploadDropZoneEnabled="true">

Атрибуты maxFiles и maxFileSize ограничивают количество и размер файлов, прикрепляемых к одному сообщению, а acceptedFileExtensions или acceptedMimeTypes ограничивают их тип. Ограничения проверяются и в браузере, и на сервере. Атрибут uploadDropZoneEnabled определяет, можно ли перетаскивать файлы на поле ввода сообщений.

Каждое значение в acceptedFileExtensions должно начинаться с точки: .png, а не png. Значение без точки отклоняется при открытии экрана, и пользователь видит диалог с ошибкой вместо чата.

Выбор файлов

Если зона перетаскивания включена, файлы можно перетащить в любое место поля ввода сообщений. Пока файл находится над полем, оно подсвечивается.

Чтобы файлы можно было выбрать нажатием, добавьте компонент aiUploadTrigger. Это отдельный компонент, потому что браузер открывает диалог выбора файлов только в ответ на реальное действие пользователя. Обычно его добавляют в выпадающее меню:

<aichat:inputPrefix>
    <dropdownButton id="attachMenuButton" icon="PLUS">
        <items>
            <componentItem id="attachItem">
                <aichat:aiUploadTrigger id="menuAttachTrigger"
                                        aiChatId="chat"
                                        icon="PAPERCLIP"
                                        themeNames="menu-item"
                                        text="msg://menuAttachTrigger.text"/>
            </componentItem>
        </items>
    </dropdownButton>
</aichat:inputPrefix>
Открытое меню прикрепления файлов в поле ввода сообщений

Вариант темы menu-item оформляет компонент как строку меню, а не как кнопку внутри меню: он убирает собственный фон, внутренние отступы и видимую границу и выравнивает содержимое по началу. Компонент можно активировать и с клавиатуры. Откройте меню и нажмите Enter — откроется диалог выбора файлов, потому что пункт меню передаёт такое же действие пользователя, как и кнопка.

Компонент внутри componentItem должен указывать свой чат в атрибуте aiChatId, даже если само меню находится внутри этого чата. Автоматический поиск обходит дерево компонентов во время загрузки дескриптора, а дочерние компоненты пункта меню в этот момент ещё не являются частью чата.

Компоненту, размещённому непосредственно в чате, в inputPrefix или inputSuffix, а не внутри меню, атрибут aiChatId вообще не нужен: он сам находит окружающий его чат. Вне чата компонент можно разместить в любом месте экрана, указав идентификатор его чата.

Файлы

Прикреплённые файлы отображаются как «чипы» над строкой ввода: миниатюра (thumbnail) для изображения и значок для любого другого файла. На каждом «чипе» есть кнопка, позволяющая удалить файл перед отправкой. Компонент aiUploadTrigger становится недоступным, если в его чате вложения отключены.

Отклонённые файлы

Файл, нарушающий одно из ограничений, отклоняется до добавления. Чтобы сообщить пользователю причину, обработайте событие отклонения:

@Subscribe("chat")
public void onChatAttachmentRejected(
        final AttachmentRejectedEvent<ChatMessage> event) {
    notifications.show(messageBundle.formatMessage("attachmentRejected.notification",
            event.getFileName(), event.getReason().name()));
}

Причина принимает одно из значений: TOO_MANY_FILES, FILE_TOO_LARGE или INCORRECT_FILE_TYPE.

Сохранение файлов

Компонент передаёт приложению содержимое файлов в виде байтов и позволяет ему решить, где их хранить. В конфигурации без настройки дополнение само записывает их в файловое хранилище по умолчанию, но для чата, связанного с вашей собственной сущностью, нужна фабрика сообщений, которая это делает:

@Install(to = "chat", subject = "messageFactory")
private ChatMessage messageFactory(final AiChatMessageContext context) {
    ChatMessage message = dataManager.create(ChatMessage.class);
    message.setRole(context.getRole());
    message.setContent(context.getContent());

    FileStorage fileStorage = fileStorageLocator.getDefault();
    List<FileRef> refs = new ArrayList<>();
    for (AIAttachment attachment : context.getAttachments()) {
        refs.add(fileStorage.saveStream(attachment.name(),
                new ByteArrayInputStream(attachment.data())));
    }
    message.setAttachments(refs);

    return message;
}

Сущность хранит результаты в виде списка FileRef. Описание самой сущности см. в разделе Сообщения и данные.

Укажите вложения в фетч-плане:

<fetchPlan extends="_base">
    <property name="attachments"/>
</fetchPlan>

Коллекция вложений не входит в _base. Без этого файлы сохраняются правильно, но при повторной загрузке диалога нигде не отображаются. Содержимое файлов находится в хранилище, строки — в базе данных, но список сообщений не показывает вложений, и в журнал ничего не записывается.

Отправка файлов модели

Сохранение файла не делает его доступным модели. Чат передаёт вложения провайдеру, а пересылка их модели — задача провайдера.

Модель должна принимать файлы. Модель, которая работает только с текстом, отклоняет весь запрос, поэтому чат показывает баннер ошибки, и ответ не формируется.

Это относится к любому вложению, а не только к изображениям: Spring AI отправляет любое вложение как мультимодальные данные независимо от его типа, поэтому обычный текстовый файл вызывает ошибку на модели, работающей только с текстом, точно так же, как фотография. Чтобы попробовать пример ниже, подключите приложение к мультимодальной модели.

Запрос содержит вложения наряду с текстом сообщения. Провайдер в примере ниже пересылает их модели и задаёт текст для случая, когда сообщение содержит файлы и совсем не содержит текста:

@Install(to = "chat", subject = "llmProvider")
private Flux<String> llmProvider(final LLMProvider.LLMRequest request) {
    String userMessage = request.userMessage();
    if (userMessage == null || userMessage.isBlank()) { (1)
        userMessage = "Describe the attached files.";
    }
    String text = userMessage;

    Media[] media = request.attachments().stream() (2)
            .map(attachment -> Media.builder()
                    .name(attachment.name())
                    .mimeType(MimeTypeUtils.parseMimeType(attachment.mimeType()))
                    .data(new ByteArrayResource(attachment.data()))
                    .build())
            .toArray(Media[]::new);

    return chatClient.prompt()
            .user(spec -> spec.text(text).media(media))
            .stream()
            .content();
}
1 Сообщение с вложением и без текста допустимо, но ChatClient.user(String) отклоняет пустой текст и завершается ошибкой ещё до обращения к модели. Задайте текст для этого случая.
2 Вложения передаются в запросе наряду с текстом. Преобразование их в Media отправляет их модели.

Скачивание из диалога

Вложения остаются в списке сообщений после повторной загрузки: изображения — в виде миниатюр, остальные файлы — в виде «чипов». Нажатие на вложение сообщает приложению, какой файл был выбран, а остальное делает загрузчик файлов Jmix:

@Subscribe("chat")
public void onChatAttachmentClick(
        final AttachmentClickEvent<ChatMessage> event) {
    FileRef fileRef = event.getFileRef();
    if (fileRef != null) {
        downloader.download(fileRef);
    }
}

aiUploadTrigger

XML-элемент

aiUploadTrigger

Java-класс

AiUploadTrigger

aiUploadTrigger имеет следующие уникальные атрибуты:

Имя Описание По умолчанию

aiChatId

Указывает чат, к которому компонент прикрепляет файлы. Обязателен, если компонент не размещён непосредственно внутри чата. См. Выбор файлов.

—

icon

Задаёт значок компонента.

—

text

Задаёт надпись компонента.

—

themeNames

Применяет варианты темы. menu-item оформляет компонент как строку выпадающего меню, а не как кнопку внутри меню.

—

aiUploadTrigger имеет следующие общие атрибуты:

Компонент становится недоступным, если в его чате вложения отключены, независимо от значения атрибута enabled.

Смотрите также