Использование ИИ-бота через API Discourse

Обзор

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

Структура потока данных

  1. Чанк контекста: Предоставляет метаданные (ID темы, ID бота-пользователя, ID агента).
  2. Частичные чанки: Содержат фрагменты текстовой потоковой передачи.

Пример потока:

{"topic_id":42,"bot_user_id":7,"agent_id":123}

{"partial":"Hello"}

{"partial":" there"}

Клиент должен объединять поля partial, чтобы сформировать окончательный ответ.


Рабочий процесс и побочные эффекты

Во время выполнения эндпоинт выполняет следующие действия:

  1. Создает пост пользователя с исходным query.
  2. Потоково передает ответ ИИ через чанковый ответ.
  3. Создает финальный пост ответа ИИ с накопленным ответом.
  4. Для новых 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.
2 лайка