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 dansplugins/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) ouuser_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
- Morceau de contexte (Context Chunk) : Fournit des métadonnées (ID du sujet, ID de l’utilisateur bot, ID de l’agent).
- 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 :
- Crée un message utilisateur avec la
querybrute. - Diffuse la réponse de l’IA via la réponse par morceaux (chunked).
- Crée le message de réponse final de l’IA avec la réponse accumulée.
- 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_idetpreferred_usernamecomme nouveau nom d’utilisateur souhaité et omettez le champusername. - Si vous souhaitez converser en utilisant un utilisateur existant, passez le
usernamede cet utilisateur et omettez les champsunique_user_idetpreferred_username.