aiChat
Компонент aiChat представляет собой готовый чат: список сообщений, поле ввода сообщений, пустое состояние и цикл генерации, который связывает их с языковой моделью.
XML-элемент |
|
|---|---|
Java-класс |
|
Он состоит из компонентов aiMessageList и aiMessageInput и настраивает их автоматически, поэтому большая часть того, что описано на страницах этих компонентов, доступна и отсюда.
Основы
Для чата нужны элемент и провайдер. Элемент:
<aichat:aiChat id="chat" width="100%" height="100%"/>
Провайдер — единственная функция, которую компонент запрашивает у приложения. Он описан в разделе Подключение модели:
@Autowired
private ChatClient.Builder chatClientBuilder;
private ChatClient chatClient;
@Subscribe
public void onInit(final InitEvent event) {
chatClient = chatClientBuilder.build();
}
@Install(to = "chat", subject = "llmProvider")
private Flux<String> llmProvider(final LLMProvider.LLMRequest request) {
return chatClient.prompt()
.user(request.userMessage())
.stream()
.content();
}
Этого достаточно для работающего чата. Он хранит диалог в памяти и теряет его при закрытии экрана. Свяжите чат с данными, чтобы диалог сохранялся после закрытия экрана.
Системный промпт
Системный промпт сообщает модели, кто она: ее роль, язык ответов, на что она должна отвечать отказом. Объявите его как вложенный элемент, который принимает многострочное значение без изменений:
<aichat:systemPrompt><![CDATA[You are the support assistant for an order management application.
Answer only questions about orders, customers and deliveries. If a question is
about anything else, say so and suggest the user contact their account manager.
Always answer in the language the question was asked in. Keep answers short:
two or three sentences unless the user asks for detail. When you refer to an
order, always include its number.
Never invent an order number, a delivery date or a price. If the answer is not
in the conversation, say that you do not have it.]]></aichat:systemPrompt>
Атрибут systemPrompt вместо этого принимает однострочное значение и поддерживает ключ локализованного сообщения.
|
Объявленный системный промпт сам по себе ничего не делает. Чат добавляет его в запрос, а провайдер решает, что отправить. Провайдер, который читает только сообщение пользователя, отбрасывает промпт, и модель отвечает так, как будто промпта нет. Передайте его дальше:
|
То же самое относится к вложениям, которые передаются в запросе вместе с промптом и сообщением. См. раздел Отправка файлов модели.
Сообщения и данные
Сообщение — это данные. Чат читает его через контракт AiChatMessage: роль, текст, необязательное время и вложения. Чат никогда не изменяет сообщение напрямую.
По умолчанию чат хранит в памяти список встроенных сообщений. Чтобы сохранять диалог, создайте собственную сущность, которая реализует этот контракт:
@JmixEntity
@Table(name = "CHAT_MESSAGE")
@Entity
public class ChatMessage implements AiChatMessage {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@Version
@Column(name = "VERSION", nullable = false)
private Integer version;
@Column(name = "ROLE", nullable = false, length = 50)
private String role;
@Column(name = "CONTENT")
@Lob
private String content;
@CreatedDate
@Column(name = "CREATED_DATE")
private OffsetDateTime createdDate;
@ElementCollection
@CollectionTable(name = "CHAT_MESSAGE_ATTACHMENTS",
joinColumns = @JoinColumn(name = "CHAT_MESSAGE_ID"))
@OrderColumn(name = "ATTACHMENTS_ORDER")
@Convert(converter = FileRefConverter.class)
@Column(name = "ATTACHMENTS", length = 1024)
private List<FileRef> attachments;
@Override
public AiMessageRole getRole() {
return role == null ? null : AiMessageRole.fromId(role);
}
public void setRole(AiMessageRole role) {
this.role = role == null ? null : role.getId();
}
@Override
public String getContent() {
return content == null ? "" : content;
}
public void setContent(@Nullable String content) {
this.content = content;
}
@Override
public @Nullable Instant getTime() {
return createdDate == null ? null : createdDate.toInstant();
}
@Override
public List<FileRef> getAttachments() {
return attachments == null ? List.of() : attachments; (1)
}
| 1 | Контракт требует список и никогда не допускает null, а пустая коллекция элементов приходит из базы данных как null. |
Затем загрузите сущность в контейнер коллекции и укажите его для чата:
<collection id="messagesDc" class="com.company.demo.entity.ChatMessage">
<fetchPlan extends="_base"/>
<loader id="messagesDl">
<query>
<![CDATA[select e from ChatMessage e order by e.createdDate]]>
</query>
</loader>
</collection>
<aichat:aiChat id="chat"
dataContainer="messagesDc"
autoSave="true"
width="100%"
height="100%">
<aichat:actions>
<aichat:action id="clearAction"
text="msg://clearAction.text"
icon="TRASH"/>
</aichat:actions>
<aichat:systemPrompt><![CDATA[You are the support assistant for an order management application.
Answer only questions about orders, customers and deliveries. If a question is
about anything else, say so and suggest the user contact their account manager.
Always answer in the language the question was asked in. Keep answers short:
two or three sentences unless the user asks for detail. When you refer to an
order, always include its number.
Never invent an order number, a delivery date or a price. If the answer is not
in the conversation, say that you do not have it.]]></aichat:systemPrompt>
<aichat:inputHeader>
<span id="contextSpan" text="msg://contextSpan.text"/>
</aichat:inputHeader>
<aichat:inputFooter>
<span text="msg://disclaimer.text"/>
</aichat:inputFooter>
<aichat:emptyStateHeader>
<h3 text="msg://emptyStateHeader.text"/>
</aichat:emptyStateHeader>
<aichat:emptyStateFooter>
<span text="msg://emptyStateFooter.text"/>
</aichat:emptyStateFooter>
</aichat:aiChat>
|
Сортируйте результат запроса по хранимому атрибуту, как в примере выше. Поле |
Компонент не может самостоятельно создать вашу сущность, поэтому установите фабрику, которая ее создает:
@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());
return message;
}
Фабрика вызывается один раз для каждого зафиксированного сообщения: для сообщения пользователя — при его отправке, для ответа — при его завершении.
Сохранение
Атрибут autoSave="true", показанный в примере выше, заставляет чат сохранять сообщения через DataContext экрана: каждое зафиксированное сообщение объединяется с контекстом и сохраняется, а сообщение, удаленное при откате диалога, удаляется.
|
|
Без autoSave сохраняйте сообщения сами, установив два делегата. saveDelegate вызывается для каждого зафиксированного сообщения до того, как оно попадет в диалог, и чат использует экземпляр, который возвращает делегат. Слой хранения обычно возвращает другой объект, чем тот, который получил, и именно этот объект далее отслеживают контейнер и DataContext. removeDelegate вызывается для каждого сообщения, которое удаляется при откате диалога, начиная с самого нового.
Приложение, которому нужно сохранять сообщения по-своему, может не устанавливать ни один из делегатов и слушать событие MessageChangeEvent, которое сообщает о каждом добавленном или удаленном сообщении.
Пустое состояние
До первого сообщения чат размещает поле ввода сообщений в центре и предоставляет вокруг него два слота для приветствия, подсказок или предупреждения:
<aichat:emptyStateHeader>
<h3 text="msg://emptyStateHeader.text"/>
</aichat:emptyStateHeader>
<aichat:emptyStateFooter>
<span text="msg://emptyStateFooter.text"/>
</aichat:emptyStateFooter>
Как только появляется первое сообщение, пустое состояние сменяется обычной компоновкой, в которой поле ввода закреплено внизу.
Полосы поля ввода сообщений
inputHeader и inputFooter размещают содержимое приложения внутри поля ввода сообщений, но вне его рамки, например строку контекста над полем или предупреждение под ним:
<aichat:inputHeader>
<span id="contextSpan" text="msg://contextSpan.text"/>
</aichat:inputHeader>
<aichat:inputFooter>
<span text="msg://disclaimer.text"/>
</aichat:inputFooter>
com.company.demo.view.chat/disclaimer.text=AI can make mistakes. Check important information.
|
Пока чат пуст, обе полосы скрыты, потому что для этого места у пустого состояния есть свои слоты. После отправки сообщения полоса появляется. Чтобы полоса была видна и в пустом чате, примените вариант темы |
Генерация
Чат переходит между тремя состояниями (ожидание, генерация и ошибка) и сообщает о каждом переходе:
@Subscribe("chat")
public void onChatGenerationStateChange(
final GenerationStateChangeEvent<SimpleAiChatMessage> event) {
stateSpan.setText(event.getNewState().name());
stopButton.setEnabled(event.getNewState() == GenerationState.GENERATING);
}
Остановка и повторная генерация доступны пользователю без дополнительного кода: во время генерации ответа кнопка отправки в поле ввода сообщений становится кнопкой остановки, а на баннере ошибки появляется кнопка Retry. Частичный ответ остановленной генерации остается на экране, но никогда не фиксируется.
Те же операции доступны из приложения, если чат должен предлагать их и в другом месте, например на панели инструментов над диалогом, в меню или по сочетанию клавиш. В примере ниже подключены две такие кнопки. Для работы встроенных элементов управления ничего из этого не нужно:
@Subscribe("stopButton")
public void onStopButtonClick(final ClickEvent<JmixButton> event) {
chat.stop();
}
@Subscribe("regenerateButton")
public void onRegenerateButtonClick(final ClickEvent<JmixButton> event) {
chat.regenerate();
}
Чтобы пользователь мог повторно сгенерировать любой ответ, а не только последний, объявите встроенное действие сообщения. Оно откатывает диалог к этому сообщению и запускает генерацию заново:
<aichat:assistantActions>
<aichat:action id="assistantRegenerate"
type="aichat_messageRegenerate"/>
</aichat:assistantActions>
При ошибке генерации список сообщений показывает баннер ошибки с кнопкой Retry, а приложение получает уведомление с ошибкой и текстом, который пришел до нее:
@Subscribe("chat")
public void onChatGenerationFailed(
final GenerationFailedEvent<SimpleAiChatMessage> event) {
notifications.show(messageBundle.formatMessage("generationFailed.notification",
event.getError().getMessage()));
}
| Ошибка сохранения и ошибка генерации используют одно и то же состояние ошибки, и кнопка Retry заново генерирует ответ, а не повторяет сохранение. |
Действия чата
Чат содержит обычные действия Jmix, которые можно связать с любым компонентом экрана:
<aichat:actions>
<aichat:action id="clearAction"
text="msg://clearAction.text"
icon="TRASH"/>
</aichat:actions>
<button id="clearButton" action="chat.clearAction"/>
Это собственные действия чата, а не действия отдельных сообщений, описанные в разделе Действия над сообщениями.
Настройка составных частей
Все возможности списка сообщений и поля ввода сообщений доступны через чат. Атрибуты maxRows, enterAction и атрибуты вложений настраивают поле ввода сообщений; announceMessages настраивает список сообщений; элементы thinkingIndicator, thinkingStages, userActions и assistantActions объявляются в чате и применяются к списку сообщений; а inputPrefix и inputSuffix заполняют слоты поля ввода сообщений. Назначение каждого из них описано на страницах aiMessageList и aiMessageInput.
Стилизация
У aiChat нет собственного пользовательского элемента. Он отображается как обычный div, который содержит список сообщений и поле ввода сообщений. Поэтому он не предоставляет shadow parts, и его нельзя выбрать по имени тега в CSS или найти по нему в инспекторе браузера. Задавайте стили через класс или id, которые вы ему назначили, а к его частям обращайтесь через компоненты внутри него:
<aichat:aiChat id="chat"
classNames="branded-chat"
themeNames="fixed-toolbar"
width="100%"
height="100%"/>
.branded-chat jmix-ai-message-input::part(input-container) {
border-radius: 0;
border-color: var(--vaadin-border-color-secondary);
}
Следующие свойства стилей можно использовать в таблицах стилей CSS для настройки внешнего вида компонента.
| Имя | Описание |
|---|---|
|
Ширина колонки диалога. Значение передается в список сообщений и поле ввода сообщений, поэтому оно одновременно меняет размер диалога, поля ввода и пустого состояния. |
Все остальное стилизуется в составляющих его компонентах. См. aiMessageList, aiMessageInput и aiCodeBlock.
Варианты темы
| Вариант | Описание |
|---|---|
|
Всегда размещает элементы управления поля ввода сообщений в отдельной строке. Передается в поле ввода сообщений. |
|
Оставляет полосу |
|
Оставляет полосу |
|
Убирает промежуток между полосами и рамкой поля ввода. Передается в поле ввода сообщений. |
Локализация
Собственные объявления чата берутся из пакета сообщений дополнения по ключам aiChat.i18n.*. Его вложенный элемент i18n повторяет дерево компонентов: атрибуты самого элемента меняют тексты чата, а вложенные элементы messageList, messageInput и codeBlock относятся к частям, из которых он состоит. См. Локализация.
Вложения
Чат поддерживает вложения на всем пути: поле ввода сообщений собирает их, фабрика сообщений сохраняет их, а список сообщений отображает их. См. Вложения.
Атрибуты
aiChat имеет следующие уникальные атрибуты:
| Имя | Описание | По умолчанию |
|---|---|---|
Разрешает вложения только с указанными расширениями файлов. См. Вложения. |
— |
|
Разрешает вложения только с указанными типами медиа. См. Вложения. |
— |
|
Определяет, объявляются ли новые сообщения программам чтения с экрана. См. Доступность. |
|
|
Определяет, принимает ли поле ввода сообщений вложения. См. Вложения. |
|
|
Сохраняет зафиксированные сообщения через |
|
|
Связывает чат с контейнером коллекции сообщений. См. Сообщения и данные. |
— |
|
Определяет, что делает клавиша Enter в поле ввода сообщений. См. Текстовое поле. |
|
|
Задает максимальный размер одного вложения в байтах. См. Вложения. |
|
|
Задает, сколько файлов можно прикрепить к одному сообщению. См. Вложения. |
|
|
Задает, до скольких строк увеличивается поле ввода сообщений, прежде чем появится прокрутка. См. Текстовое поле. |
|
|
Задает системный промпт, который отправляется с каждым запросом. Для многострочного значения его также можно объявить как вложенный элемент. |
— |
|
Применяет варианты темы, в том числе |
— |
|
Определяет, можно ли перетаскивать файлы в поле ввода сообщений. См. Вложения. |
|
aiChat имеет следующие общие атрибуты:
Обработчики
aiChat имеет следующие уникальные обработчики:
| Имя | Описание |
|---|---|
Срабатывает, когда пользователь нажимает на вложение в сообщении. См. Скачивание из диалога. |
|
Срабатывает, когда файл отклонен одним из ограничений на вложения. См. Отклонённые файлы. |
|
Срабатывает при ошибке генерации. Содержит ошибку и текст, полученный до нее. См. Генерация. |
|
Срабатывает при каждом переходе между состояниями ожидания, генерации и ошибки. См. Генерация. |
|
Срабатывает, когда сообщение зафиксировано или удалено. См. Сохранение. |
|
Возвращает ответ на запрос в виде потока текста. Обязателен. См. Основы. |
|
Создает экземпляр сообщения для связанного источника. Обязателен, если чат связан с вашей собственной сущностью. См. Сообщения и данные. |
|
Удаляет сообщение, которое было убрано из диалога при откате. См. Сохранение. |
|
Сохраняет зафиксированное сообщение и возвращает экземпляр, который чат будет использовать дальше. См. Сохранение. |
aiChat поддерживает следующие общие обработчики:
Элементы
aiChat может содержать следующие вложенные элементы: actions, systemPrompt, inputPrefix, inputSuffix, inputHeader, inputFooter, thinkingIndicator, thinkingStages, userActions, assistantActions, emptyStateHeader, emptyStateFooter и i18n.
actions
Элемент actions содержит собственные элементы action чата. См. Действия чата.
systemPrompt
Элемент systemPrompt содержит многострочный системный промпт и используется вместо одноименного атрибута.
inputPrefix, inputSuffix, inputHeader, inputFooter
Эти элементы заполняют слоты поля ввода сообщений: первые два — внутри его рамки, последние два — как полосы вне ее. См. Полосы поля ввода сообщений и Префикс и суффикс.
emptyStateHeader, emptyStateFooter
Эти элементы содержат контент, который отображается над полем ввода сообщений и под ним, пока чат пуст. См. Пустое состояние.
thinkingIndicator, thinkingStages, userActions, assistantActions
Эти элементы настраивают список сообщений и работают так, как описано для aiMessageList.
i18n
Элемент i18n меняет тексты этого чата, а через вложенные элементы messageList, messageInput и codeBlock — тексты частей, из которых он состоит. См. Локализация.