Visão Geral
O Discourse AI expõe um endpoint de administração/API para streaming da resposta de um Agente de IA por meio de uma resposta HTTP bruta em chunks.
- Protocolo: Codificação de transferência HTTP bruta em chunks (NÃO Server-Sent Events).
- Implementação: Sequestra o socket Rack para transmitir objetos JSON separados por nova linha.
- Efeitos Colaterais: Esta não é apenas uma API de conclusão; ela cria posts reais de Mensagem Privada (PM) do Discourse.
Detalhes do Endpoint
- Localização:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272 - Rota:
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json - Escopo da Chave de API:
ai:stream_completion(registrado emplugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)
Cabeçalhos da Requisição
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json
Parâmetros do Corpo da Requisição
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
agent_id |
Integer | Opcional* | Identificador do agente. |
agent_name |
String | Opcional* | Identificador alternativo para o agente. |
query |
String | Sim | O prompt/pergunta do usuário. |
username |
String | Obrigatório se user_unique_id for omitido |
Usado caso a PM do chat precise ser associada a um usuário existente. |
user_unique_id |
String | Obrigatório se username for omitido |
Identifica o usuário final. Cria/reutiliza um usuário em estágio inicial (staged user) com chave no campo personalizado ai-stream-conversation-unique-id. |
preferred_username |
String | Opcional* | Nome de usuário para o usuário (se user_unique_id não for usado). |
topic_id |
Integer | Opcional | Continuar uma conversa de PM existente. |
custom_instructions |
String | Opcional | Adicionado ao contexto do prompt do agente. |
Nota: Você deve identificar o usuário final por meio de
username(usuário existente do Discourse) ouuser_unique_id.
Formato da Resposta
O servidor retorna uma resposta 200 OK com Transfer-Encoding: chunked. O stream consiste em objetos JSON separados por nova linha.
Cabeçalhos 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
Estrutura do Payload do Stream
- Chunk de Contexto: Fornece metadados (ID do tópico, ID do usuário bot, ID do agente).
- Chunks Parciais: Contém os fragmentos de texto transmitidos.
Exemplo de Stream:
{"topic_id":42,"bot_user_id":7,"agent_id":123}
{"partial":"Hello"}
{"partial":" there"}
O cliente deve concatenar os campos partial para construir a resposta final.
Fluxo de Trabalho e Efeitos Colaterais
O endpoint executa as seguintes ações durante a execução:
- Cria um post do usuário com a
querybruta. - Transmite a resposta da IA via resposta em chunks.
- Cria o post final da resposta da IA com a resposta acumulada.
- Para novas PMs, pode definir automaticamente o título do tópico.
Ferramentas Executadas pelo Cliente (Personalizadas)
Você pode fornecer definições de ferramentas para permitir que o modelo chame ferramentas externas. O servidor pausa o stream quando uma ferramenta é chamada, permitindo que o cliente execute a ferramenta e retome.
1. Requisição Inicial com Ferramentas
Inclua custom_tools no corpo da requisição:
{
"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. Evento de Chamada de Ferramenta
Se o modelo chamar uma ferramenta, o stream emite um evento tool_calls e para:
{
"event": "tool_calls",
"tool_calls": [
{
"id": "tool_1",
"name": "client_weather",
"parameters": { "city": "Austin" }
}
],
"resume_token": "..."
}
Neste ponto, o servidor persiste o estado da conversa no Redis.
3. Retomada com Resultados da Ferramenta
O cliente executa a ferramenta e retoma o stream:
{
"resume_token": "...",
"tool_results": [
{
"tool_call_id": "tool_1",
"content": { "temperature_c": 23 }
}
]
}
O servidor recarrega o estado do prompt salvo, insere o resultado da ferramenta, continua a geração e transmite mais chunks partial.
Limites de Ferramentas
- Máximo de ferramentas personalizadas: 20
- Máximo de resultados de ferramentas: 20
- Tamanho máximo da definição de ferramenta personalizada: 10.000 bytes
- Tamanho máximo do conteúdo do resultado da ferramenta: 100 KB
- TTL de retomada: 15 minutos
- Máximo de rodadas de retomada: 10
Referências de Implementação
- Controlador:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb - Streamer:
plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb - Sessão de Ferramentas Personalizadas:
plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb
Exemplos de Teste
Veja plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb para exemplos abrangentes:
- Nova conversa em stream: Linhas 1248-1356
- Ferramentas personalizadas + token de retomada: Linhas 1358-1448
- Chamadas de ferramentas paralelas: Linhas 1467-1590
Implementação de exemplo
Script Ruby
require 'net/http'
require 'json'
require 'uri'
# Configuração
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Ou use agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<deixe vazio se quiser que a PM seja enviada para o usuário USERNAME>'
# Helper para criar o URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")
# Cria a requisição HTTP
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'
# Prepara o corpo da requisição
body = {
agent_id: AGENT_ID,
query: QUERY,
## descomente a linha abaixo se quiser usar um usuário existente para a conversa. Além disso, `username` tem precedência sobre `user_unique_id` se for passado.
# username: USERNAME,
## use o campo abaixo junto com `user_unique_id` para criar um novo usuário em estágio inicial. Ao usar isso, passe de passar o `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|
# Verifica se a resposta foi bem-sucedida
if response.code == '200'
puts "Stream iniciado com sucesso."
puts "Cabeçalhos da resposta: #{response.to_hash}"
puts "Conteúdo em streaming: "
# Lê a resposta em chunks
response.read_body do |chunk|
# A resposta é composta por objetos JSON separados por nova linha
chunk.each_line do |line|
line = line.strip
next if line.empty?
begin
json = JSON.parse(line)
if json['topic_id']
puts "\n--- Contexto Recebido ---"
puts "ID do Tópico: #{json['topic_id']}"
puts "ID do Usuário Bot: #{json['bot_user_id']}"
puts "ID do Agente: #{json['agent_id']}"
elsif json['partial']
# Transmite o conteúdo parcial
print json['partial']
elsif json['event'] == 'tool_calls'
puts "\n--- Chamada de Ferramenta Recebida ---"
puts JSON.pretty_generate(json)
# Aqui você manteria a execução da ferramenta e a retomada
# Neste exemplo, apenas imprimimos
end
# puts "<nova palavra>"
rescue JSON::ParserError => e
puts "\nErro ao analisar JSON: #{e.message}"
puts "Linha bruta: #{line}"
end
end
end
puts "\n--- Stream Finalizado ---"
else
puts "Erro: #{response.code} #{response.message}"
puts "Corpo da resposta: #{response.body}"
end
end
Observações
- Se você quiser que a conversa seja feita em nome de um novo usuário em estágio inicial, passe o
unique_user_ide opreferred_usernamecomo o novo nome de usuário desejado e pule o campousername. - Se você quiser conversar usando um usuário existente, passe o
usernamedesse usuário e pule os camposunique_user_idepreferred_username.