Обзор
Discourse AI предоставляет административный API-эндпоинт для потоковой передачи ответа ИИ-агента через «сырой» чанковый HTTP-ответ.
- Протокол: «Сырая» чанковая кодировка передачи HTTP (НЕ Server-Sent Events).
- Реализация: Перехват сокета Rack для потоковой передачи JSON-объектов, разделенных символами новой строки.
- Побочные эффекты: Это не просто API для завершения (completion); он создает реальные посты в личных сообщениях (PM) Discourse.
Детали эндпоинта
- Расположение:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272 - Маршрут:
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json - Область действия API-ключа:
ai:stream_completion(зарегистрировано вplugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)
Заголовки запроса
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json
Параметры тела запроса
| Параметр | Тип | Обязательность | Описание |
|---|---|---|---|
agent_id |
Integer | Необязательно* | Идентификатор агента. |
agent_name |
String | Необязательно* | Альтернативный идентификатор агента. |
query |
String | Да | Запрос/вопрос пользователя. |
username |
String | Обязательно, если user_unique_id не указан |
Используется, если чат PM нужно связать с существующим пользователем. |
user_unique_id |
String | Обязательно, если username не указан |
Идентифицирует конечного пользователя. Создает/использует «стагированного» (staged) пользователя, ключ которого хранится в пользовательском поле ai-stream-conversation-unique-id. |
preferred_username |
String | Необязательно* | Имя пользователя для данного пользователя (если user_unique_id не используется). |
topic_id |
Integer | Необязательно | Продолжить существующую переписку в PM. |
custom_instructions |
String | Необязательно | Добавляется в контекст промпта агента. |
Примечание: Вы должны идентифицировать конечного пользователя с помощью либо
username(существующий пользователь Discourse), либоuser_unique_id.
Формат ответа
Сервер возвращает ответ 200 OK с заголовком Transfer-Encoding: chunked. Поток состоит из JSON-объектов, разделенных символами новой строки.
HTTP-заголовки
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-cache, no-store, must-revalidate
Connection: close
X-Accel-Buffering: no
X-Content-Type-Options: nosniff
Структура потока данных
- Чанк контекста: Предоставляет метаданные (ID темы, ID бота-пользователя, ID агента).
- Частичные чанки: Содержат фрагменты текстовой потоковой передачи.
Пример потока:
{"topic_id":42,"bot_user_id":7,"agent_id":123}
{"partial":"Hello"}
{"partial":" there"}
Клиент должен объединять поля partial, чтобы сформировать окончательный ответ.
Рабочий процесс и побочные эффекты
Во время выполнения эндпоинт выполняет следующие действия:
- Создает пост пользователя с исходным
query. - Потоково передает ответ ИИ через чанковый ответ.
- Создает финальный пост ответа ИИ с накопленным ответом.
- Для новых PM может автоматически назначить теме заголовок.
Пользовательские инструменты, выполняемые клиентом
Вы можете предоставить определения инструментов, чтобы позволить модели вызывать внешние инструменты. Сервер приостанавливает поток при вызове инструмента, позволяя клиенту выполнить инструмент и возобновить работу.
1. Начальный запрос с инструментами
Включите custom_tools в тело запроса:
{
"agent_id": 123,
"query": "What's the weather?",
"user_unique_id": "external-user-42",
"custom_tools": [
{
"name": "client_weather",
"description": "Gets weather from the client runtime",
"parameters": [
{
"name": "city",
"description": "City to fetch weather for",
"type": "string",
"required": true
}
]
}
]
}
2. Событие вызова инструмента
Если модель вызывает инструмент, поток издает событие tool_calls и останавливается:
{
"event": "tool_calls",
"tool_calls": [
{
"id": "tool_1",
"name": "client_weather",
"parameters": { "city": "Austin" }
}
],
"resume_token": "..."
}
В этот момент сервер сохраняет состояние разговора в Redis.
3. Возобновление с результатами инструментов
Клиент выполняет инструмент и возобновляет поток:
{
"resume_token": "...",
"tool_results": [
{
"tool_call_id": "tool_1",
"content": { "temperature_c": 23 }
}
]
}
Сервер загружает сохраненное состояние промпта, вставляет результат инструмента, продолжает генерацию и потоково передает дополнительные чанки partial.
Ограничения инструментов
- Макс. количество пользовательских инструментов: 20
- Макс. количество результатов инструментов: 20
- Макс. размер определения пользовательского инструмента: 10 000 байт
- Макс. размер содержимого результата инструмента: 100 КБ
- TTL возобновления: 15 минут
- Макс. количество раундов возобновления: 10
Ссылки на реализацию
- Контроллер:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb - Стимер:
plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb - Сессия пользовательских инструментов:
plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb
Примеры тестов
См. plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb для комплексных примеров:
- Новый потоковый разговор: Строки 1248-1356
- Пользовательские инструменты + токен возобновления: Строки 1358-1448
- Параллельные вызовы инструментов: Строки 1467-1590
Пример реализации
Скрипт на Ruby
require 'net/http'
require 'json'
require 'uri'
# Конфигурация
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Или используйте agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<оставьте пустым, если хотите, чтобы PM был отправлен пользователю USERNAME>'
# Вспомогательная функция для создания URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")
# Создание HTTP-запроса
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'
# Подготовка тела запроса
body = {
agent_id: AGENT_ID,
query: QUERY,
## раскомментируйте следующую строку, если вы хотите использовать существующего пользователя для разговора. Также, если передан `username`, он имеет приоритет над `user_unique_id`.
# username: USERNAME,
## используйте следующее поле вместе с `user_unique_id`, чтобы создать нового стегированного пользователя. При использовании этого пропустите передачу `username`.
# preferred_username: USER_UNIQUE_ID
}
body[:user_unique_id] = USER_UNIQUE_ID unless USER_UNIQUE_ID.empty?
body = body.to_json
request.body = body
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.request(request) do |response|
# Проверка успешности ответа
if response.code == '200'
puts "Поток успешно запущен."
puts "Заголовки ответа: #{response.to_hash}"
puts "Потоковое содержимое:"
# Чтение чанкового ответа
response.read_body do |chunk|
# Ответ представляет собой JSON-объекты, разделенные символами новой строки
chunk.each_line do |line|
line = line.strip
next if line.empty?
begin
json = JSON.parse(line)
if json['topic_id']
puts "\n--- Получен контекст ---"
puts "ID темы: #{json['topic_id']}"
puts "ID пользователя-бота: #{json['bot_user_id']}"
puts "ID агента: #{json['agent_id']}"
elsif json['partial']
# Потоковая передача частичного содержимого
print json['partial']
elsif json['event'] == 'tool_calls'
puts "\n--- Получен вызов инструмента ---"
puts JSON.pretty_generate(json)
# Здесь вы должны обработать выполнение инструмента и возобновление
# В этом примере мы просто выводим его
end
# puts "<новое слово>"
rescue JSON::ParserError => e
puts "\nОшибка разбора JSON: #{e.message}"
puts "Исходная строка: #{line}"
end
end
end
puts "\n--- Поток завершён ---"
else
puts "Ошибка: #{response.code} #{response.message}"
puts "Тело ответа: #{response.body}"
end
end
Примечания
- Если вы хотите, чтобы разговор велся от имени нового стегированного пользователя, передайте
unique_user_idиpreferred_usernameкак новое желаемое имя пользователя и пропустите полеusername. - Если вы хотите вести разговор от имени существующего пользователя, передайте
usernameэтого пользователя и пропустите поляunique_user_idиpreferred_username.