Usando o bot de IA via API do Discourse

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 em plugins/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) ou user_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

  1. Chunk de Contexto: Fornece metadados (ID do tópico, ID do usuário bot, ID do agente).
  2. 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:

  1. Cria um post do usuário com a query bruta.
  2. Transmite a resposta da IA via resposta em chunks.
  3. Cria o post final da resposta da IA com a resposta acumulada.
  4. 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_id e o preferred_username como o novo nome de usuário desejado e pule o campo username.
  • Se você quiser conversar usando um usuário existente, passe o username desse usuário e pule os campos unique_user_id e preferred_username.
2 curtidas