Uso del bot de IA a través de la API de Discourse

Resumen

Discourse AI expone un punto de administración/API para transmitir la respuesta de un Agente de IA mediante una respuesta HTTP cruda con codificación por bloques (chunked).

  • Protocolo: Codificación de transferencia HTTP cruda por bloques (NO Server-Sent Events).
  • Implementación: Toma el control del socket de Rack para transmitir objetos JSON separados por saltos de línea.
  • Efectos secundarios: Esto no es solo una API de completado; crea publicaciones reales de mensajes privados (PM) de Discourse.

Detalles del punto de acceso

  • Ubicación: plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272
  • Ruta: POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
  • Alcance de la clave de API: ai:stream_completion (registrado en plugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)

Encabezados de solicitud

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 del cuerpo de la solicitud

Parámetro Tipo Obligatorio Descripción
agent_id Entero Opcional* Identificador del agente.
agent_name Cadena Opcional* Identificador alternativo del agente.
query Cadena El prompt o pregunta del usuario.
username Cadena Obligatorio si se omite user_unique_id Se usa en caso de que el PM del chat necesite asociarse con un usuario existente.
user_unique_id Cadena Obligatorio si se omite username Identifica al usuario final. Crea/reutiliza un usuario en espera (staged user) claveado por el campo personalizado ai-stream-conversation-unique-id.
preferred_username Cadena Opcional* Nombre de usuario para el usuario (si no se usa user_unique_id).
topic_id Entero Opcional Continuar una conversación de PM existente.
custom_instructions Cadena Opcional Se agrega al contexto del prompt del agente.

Nota: Debe identificar al usuario final mediante username (usuario existente de Discourse) o user_unique_id.


Formato de respuesta

El servidor devuelve una respuesta 200 OK con Transfer-Encoding: chunked. El flujo consiste en objetos JSON separados por saltos de línea.

Encabezados 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

Estructura de la carga del flujo

  1. Bloque de contexto: Proporciona metadatos (ID del tema, ID del usuario bot, ID del agente).
  2. Bloques parciales: Contiene los fragmentos de texto transmitidos.

Ejemplo de flujo:

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

{"partial":"Hello"}

{"partial":" there"}

El cliente debe concatenar los campos partial para construir la respuesta final.


Flujo de trabajo y efectos secundarios

El punto de acceso realiza las siguientes acciones durante la ejecución:

  1. Crea una publicación de usuario con la query cruda.
  2. Transmite la respuesta de la IA mediante la respuesta por bloques.
  3. Crea la publicación de respuesta final de la IA con la respuesta acumulada.
  4. Para nuevos PM, puede titular automáticamente el tema.

Herramientas ejecutadas por el cliente personalizadas

Puede proporcionar definiciones de herramientas para permitir que el modelo llame a herramientas externas. El servidor pausa el flujo cuando se llama a una herramienta, permitiendo que el cliente ejecute la herramienta y reanude.

1. Solicitud inicial con herramientas

Incluya custom_tools en el cuerpo de la solicitud:

{
  "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 llamada a herramienta

Si el modelo llama a una herramienta, el flujo emite un evento tool_calls y se detiene:

{
  "event": "tool_calls",
  "tool_calls": [
    {
      "id": "tool_1",
      "name": "client_weather",
      "parameters": { "city": "Austin" }
    }
  ],
  "resume_token": "..."
}

El servidor persiste el estado de la conversación en Redis en este punto.

3. Reanudar con resultados de herramientas

El cliente ejecuta la herramienta y reanuda el flujo:

{
  "resume_token": "...",
  "tool_results": [
    {
      "tool_call_id": "tool_1",
      "content": { "temperature_c": 23 }
    }
  ]
}

El servidor recarga el estado del prompt guardado, inserta el resultado de la herramienta, continúa la generación y transmite más bloques partial.

Límites de herramientas

  • Máximo de herramientas personalizadas: 20
  • Máximo de resultados de herramientas: 20
  • Tamaño máximo de la definición de herramienta personalizada: 10,000 bytes
  • Tamaño máximo del contenido del resultado de la herramienta: 100 KB
  • TTL de reanudación: 15 minutos
  • Máximo de rondas de reanudación: 10

Referencias de implementación

  • Controlador: plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb
  • Transmisor: plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb
  • Sesión de herramientas personalizadas: plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb

Ejemplos de prueba

Vea plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb para ejemplos exhaustivos:

  • Nueva conversación transmitida: Líneas 1248-1356
  • Herramientas personalizadas + token de reanudación: Líneas 1358-1448
  • Llamadas a herramientas en paralelo: Líneas 1467-1590

Implementación de ejemplo

Script de Ruby
require 'net/http'
require 'json'
require 'uri'

# Configuración
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # O use agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<dejar vacío si se desea que el PM se envíe al usuario USERNAME>'


# Helper para crear la URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")

# Crear la solicitud HTTP
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'

# Preparar el cuerpo de la solicitud
body = {
  agent_id: AGENT_ID,
  query: QUERY,
  ## descomente la línea siguiente si desea usar un usuario existente para la conversación. Además, `username` tiene precedencia sobre `user_unique_id` si se pasa.
  # username: USERNAME,
  ## use el siguiente campo junto con `user_unique_id` para crear un nuevo usuario en espera. Al usar esto, omita pasar `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|
    # Verificar si la respuesta es exitosa
    if response.code == '200'
    puts "Stream started successfully."
    puts "Response headers: #{response.to_hash}"
    puts "Streaming content:"   
    
    # Leer la respuesta por bloques
    response.read_body do |chunk|
        # La respuesta son objetos JSON separados por saltos de línea
        chunk.each_line do |line|
        line = line.strip
        next if line.empty?
        
        begin
            json = JSON.parse(line)
            
            if json['topic_id']
            puts "\n--- Context Received ---"
            puts "Topic ID: #{json['topic_id']}"
            puts "Bot User ID: #{json['bot_user_id']}"
            puts "Agent ID: #{json['agent_id']}"
            elsif json['partial']
            # Transmitir el contenido parcial
            print json['partial']
            elsif json['event'] == 'tool_calls'
            puts "\n--- Tool Call Received ---"
            puts JSON.pretty_generate(json)
            # Aquí se manejaría la ejecución de la herramienta y la reanudación
            # Para este ejemplo, solo lo imprimimos
            end

            # puts "<new word>"
        rescue JSON::ParserError => e
            puts "\nError parsing JSON: #{e.message}"
            puts "Raw line: #{line}"
        end
        end
    end
    puts "\n--- Stream Finished ---"

    else
    puts "Error: #{response.code} #{response.message}"
    puts "Response body: #{response.body}"
    end
end

Notas

  • Si desea que la conversación esté a nombre de un nuevo usuario en espera, pase unique_user_id y preferred_username como el nuevo nombre de usuario deseado y omita el campo username.
  • Si desea conversar usando un usuario existente, pase el username de ese usuario y omita los campos unique_user_id y preferred_username.
2 Me gusta