Utiliser le bot IA via l'API Discourse

Vue d’ensemble

Discourse AI expose un point d’accès admin/API pour diffuser en continu la réponse d’un Agent IA via une réponse HTTP brute par morceaux (chunked).

  • Protocole : Encodage de transfert HTTP brut par morceaux (chunked) (ET NON des Server-Sent Events).
  • Implémentation : Prend le contrôle du socket Rack pour diffuser des objets JSON séparés par des sauts de ligne.
  • Effets secondaires : Ce n’est pas seulement une API de complétion ; elle crée de véritables messages privés (PM) Discourse.

Détails du point d’accès

  • Emplacement : plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272
  • Route : POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
  • Portée de la clé API : ai:stream_completion (enregistrée dans plugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)

En-têtes de requête

POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json

Paramètres du corps de requête

Paramètre Type Obligatoire Description
agent_id Entier Optionnel* Identifiant de l’agent.
agent_name Chaîne de caractères Optionnel* Identifiant alternatif pour l’agent.
query Chaîne de caractères Oui Le prompt ou la question de l’utilisateur.
username Chaîne de caractères Obligatoire si user_unique_id est omis Utilisé au cas où le PM de chat doit être associé à un utilisateur existant.
user_unique_id Chaîne de caractères Obligatoire si username est omis Identifie l’utilisateur final. Crée/réutilise un utilisateur en attente (staged user) basé sur le champ personnalisé ai-stream-conversation-unique-id.
preferred_username Chaîne de caractères Optionnel* Nom d’utilisateur pour l’utilisateur (si user_unique_id n’est pas utilisé).
topic_id Entier Optionnel Poursuivre une conversation PM existante.
custom_instructions Chaîne de caractères Optionnel Ajouté au contexte du prompt de l’agent.

Remarque : Vous devez identifier l’utilisateur final par username (utilisateur Discourse existant) ou user_unique_id.


Format de réponse

Le serveur renvoie une réponse 200 OK avec Transfer-Encoding: chunked. Le flux se compose d’objets JSON séparés par des sauts de ligne.

En-têtes 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

Structure de la charge utile du flux

  1. Morceau de contexte (Context Chunk) : Fournit des métadonnées (ID du sujet, ID de l’utilisateur bot, ID de l’agent).
  2. Morceaux partiels (Partial Chunks) : Contient les fragments de texte diffusés.

Exemple de flux :

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

{"partial":"Hello"}

{"partial":" there"}

Le client doit concaténer les champs partial pour construire la réponse finale.


Flux de travail et effets secondaires

Le point d’accès effectue les actions suivantes pendant l’exécution :

  1. Crée un message utilisateur avec la query brute.
  2. Diffuse la réponse de l’IA via la réponse par morceaux (chunked).
  3. Crée le message de réponse final de l’IA avec la réponse accumulée.
  4. Pour les nouveaux PM, peut titrer automatiquement le sujet.

Outils exécutés par le client (Custom Client-Executed Tools)

Vous pouvez fournir des définitions d’outils pour permettre au modèle d’appeler des outils externes. Le serveur met le flux en pause lorsqu’un outil est appelé, permettant au client d’exécuter l’outil et de reprendre.

1. Requête initiale avec outils

Incluez custom_tools dans le corps de la requête :

{
  "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. Événement d’appel d’outil (Tool Call Event)

Si le modèle appelle un outil, le flux émet un événement tool_calls et s’arrête :

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

Le serveur persiste l’état de la conversation dans Redis à ce moment-là.

3. Reprendre avec les résultats des outils

Le client exécute l’outil et reprend le flux :

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

Le serveur recharge l’état du prompt enregistré, insère le résultat de l’outil, poursuit la génération et diffuse plus de morceaux partial.

Limites des outils

  • Nombre maximal d’outils personnalisés : 20
  • Nombre maximal de résultats d’outils : 20
  • Taille maximale de la définition d’un outil personnalisé : 10 000 octets
  • Taille maximale du contenu du résultat d’un outil : 100 Ko
  • Durée de vie (TTL) de la reprise : 15 minutes
  • Nombre maximal de rondes de reprise : 10

Références d’implémentation

  • Contrôleur : plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb
  • Diffuseur (Streamer) : plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb
  • Session des outils personnalisés : plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb

Exemples de tests

Consultez plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb pour des exemples complets :

  • Nouvelle conversation diffusée : Lignes 1248-1356
  • Outils personnalisés + jeton de reprise : Lignes 1358-1448
  • Appels d’outils parallèles : Lignes 1467-1590

Implémentation d’exemple

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

# Configuration
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Or use agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<leave empty if want the PM to be sent to the USERNAME user>'


# Helper to create the URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")

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

# Prepare the request body
body = {
  agent_id: AGENT_ID,
  query: QUERY,
  ## uncomment the below line if you want to use an existing user for the conversation. Also, `username` takes precedence over `user_unique_id` if passed.
  # username: USERNAME,
  ## use the below field along with `user_unique_id` in order to create a new staged user. When using this, skip passing the `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|
    # Check if the response is successful
    if response.code == '200'
    puts "Stream started successfully."
    puts "Response headers: #{response.to_hash}"
    puts "Streaming content:"   
    
    # Read the chunked response
    response.read_body do |chunk|
        # The response is newline-separated JSON objects
        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']
            # Stream the partial content
            print json['partial']
            elsif json['event'] == 'tool_calls'
            puts "\n--- Tool Call Received ---"
            puts JSON.pretty_generate(json)
            # Here you would handle tool execution and resume
            # For this example, we just print it
            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

Notes

  • Si vous souhaitez que la conversation soit au nom d’un nouvel utilisateur en attente (staged user), passez unique_user_id et preferred_username comme nouveau nom d’utilisateur souhaité et omettez le champ username.
  • Si vous souhaitez converser en utilisant un utilisateur existant, passez le username de cet utilisateur et omettez les champs unique_user_id et preferred_username.
2 « J'aime »