Panoramica
Discourse AI espone un endpoint admin/API per lo streaming della risposta di un Agente IA tramite una risposta HTTP grezza a blocchi (chunked).
- Protocollo: Codifica di trasferimento HTTP grezza a blocchi (NON Server-Sent Events).
- Implementazione: Sequestra il socket Rack per trasmettere oggetti JSON separati da newline.
- Effetti collaterali: Non si tratta solo di un’API di completamento; crea post reali di Messaggi Privati (PM) di Discourse.
Dettagli dell’Endpoint
- Posizione:
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 - Ambito della Chiave API:
ai:stream_completion(registrato inplugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)
Intestazioni della Richiesta
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json
Parametri del Corpo della Richiesta
| Parametro | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
agent_id |
Integer | Facoltativo* | Identificatore per l’agente. |
agent_name |
String | Facoltativo* | Identificatore alternativo per l’agente. |
query |
String | Sì | Il prompt/domanda dell’utente. |
username |
String | Obbligatorio se user_unique_id è omesso |
Utilizzato nel caso in cui il PM della chat debba essere associato a un utente esistente. |
user_unique_id |
String | Obbligatorio se username è omesso |
Identifica l’utente finale. Crea/riutilizza un utente in staging chiaveato dal campo personalizzato ai-stream-conversation-unique-id. |
preferred_username |
String | Facoltativo* | Nome utente per l’utente (se user_unique_id non viene utilizzato). |
topic_id |
Integer | Facoltativo | Continua una conversazione PM esistente. |
custom_instructions |
String | Facoltativo | Aggiunto al contesto del prompt dell’agente. |
Nota: È necessario identificare l’utente finale tramite
username(utente Discourse esistente) oppureuser_unique_id.
Formato della Risposta
Il server restituisce una risposta 200 OK con Transfer-Encoding: chunked. Lo stream consiste in oggetti JSON separati da newline.
Intestazioni 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
Struttura del Payload dello Stream
- Chunk di Contesto: Fornisce metadati (ID topic, ID utente bot, ID agente).
- Chunk Parziali: Contiene i frammenti di testo trasmessi in streaming.
Esempio di Stream:
{"topic_id":42,"bot_user_id":7,"agent_id":123}
{"partial":"Hello"}
{"partial":" there"}
Il client dovrebbe concatenare i campi partial per costruire la risposta finale.
Flusso di Lavoro ed Effetti Collaterali
L’endpoint esegue le seguenti azioni durante l’esecuzione:
- Crea un post utente con la
querygrezza. - Trasmette in streaming la risposta IA tramite la risposta a blocchi.
- Crea il post di risposta IA finale con la risposta accumulata.
- Per i nuovi PM, potrebbe impostare automaticamente il titolo del topic.
Strumenti Personalizzati Eseguiti dal Client
È possibile fornire definizioni di strumenti per consentire al modello di chiamare strumenti esterni. Il server mette in pausa lo stream quando viene chiamato uno strumento, consentendo al client di eseguire lo strumento e riprendere.
1. Richiesta Iniziale con Strumenti
Includi custom_tools nel corpo della richiesta:
{
"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 di Chiamata Strumento
Se il modello chiama uno strumento, lo stream emette un evento tool_calls e si ferma:
{
"event": "tool_calls",
"tool_calls": [
{
"id": "tool_1",
"name": "client_weather",
"parameters": { "city": "Austin" }
}
],
"resume_token": "..."
}
In questo punto il server persiste lo stato della conversazione in Redis.
3. Ripresa con Risultati degli Strumenti
Il client esegue lo strumento e riprende lo stream:
{
"resume_token": "...",
"tool_results": [
{
"tool_call_id": "tool_1",
"content": { "temperature_c": 23 }
}
]
}
Il server ricarica lo stato del prompt salvato, inserisce il risultato dello strumento, continua la generazione e trasmette altri chunk partial.
Limiti degli Strumenti
- Numero massimo di strumenti personalizzati: 20
- Numero massimo di risultati degli strumenti: 20
- Dimensione massima della definizione di strumento personalizzato: 10.000 byte
- Dimensione massima del contenuto del risultato dello strumento: 100 KB
- TTL di ripresa: 15 minuti
- Numero massimo di round di ripresa: 10
Riferimenti Implementativi
- Controller:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb - Streamer:
plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb - Sessione Strumenti Personalizzati:
plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb
Esempi di Test
Consulta plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb per esempi completi:
- Nuova conversazione in streaming: Righe 1248-1356
- Strumenti personalizzati + token di ripresa: Righe 1358-1448
- Chiamate di strumenti parallele: Righe 1467-1590
Implementazione di esempio
Script Ruby
require 'net/http'
require 'json'
require 'uri'
# Configurazione
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Oppure usa agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<lascia vuoto se vuoi che il PM venga inviato all'utente USERNAME>'
# Helper per creare l'URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")
# Crea la richiesta HTTP
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'
# Prepara il corpo della richiesta
body = {
agent_id: AGENT_ID,
query: QUERY,
## discommenta la riga seguente se vuoi utilizzare un utente esistente per la conversazione. Inoltre, `username` ha la precedenza su `user_unique_id` se passato.
# username: USERNAME,
## usa il campo seguente insieme a `user_unique_id` per creare un nuovo utente in staging. Quando si usa questo, omettere il passaggio di `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|
# Controlla se la risposta è riuscita
if response.code == '200'
puts "Stream avviato con successo."
puts "Intestazioni di risposta: #{response.to_hash}"
puts "Contenuto in streaming:"
# Legge la risposta a blocchi
response.read_body do |chunk|
# La risposta è costituita da oggetti JSON separati da newline
chunk.each_line do |line|
line = line.strip
next if line.empty?
begin
json = JSON.parse(line)
if json['topic_id']
puts "\n--- Contesto Ricevuto ---"
puts "ID Topic: #{json['topic_id']}"
puts "ID Utente Bot: #{json['bot_user_id']}"
puts "ID Agente: #{json['agent_id']}"
elsif json['partial']
# Trasmette il contenuto parziale
print json['partial']
elsif json['event'] == 'tool_calls'
puts "\n--- Chiamata Strumento Ricevuta ---"
puts JSON.pretty_generate(json)
# Qui si gestirebbe l'esecuzione dello strumento e la ripresa
# Per questo esempio, lo stampiamo solo
end
# puts "<nuova parola>"
rescue JSON::ParserError => e
puts "\nErrore nell'analisi JSON: #{e.message}"
puts "Riga grezza: #{line}"
end
end
end
puts "\n--- Stream Terminato ---"
else
puts "Errore: #{response.code} #{response.message}"
puts "Corpo della risposta: #{response.body}"
end
end
Note
- Se si desidera che la conversazione sia a nome di un nuovo utente in staging, passare
unique_user_idepreferred_usernamecome nuovo nome utente desiderato e omettere il campousername. - Se si desidera conversare utilizzando un utente esistente, passare il
usernamedi quell’utente e omettere i campiunique_user_idepreferred_username.