Übersicht
Discourse AI stellt einen Admin-/API-Endpunkt bereit, um die Antwort eines KI-Agenten über eine rohe chunked HTTP-Antwort zu streamen.
- Protokoll: Rohe chunked HTTP-Transfer-Encoding (KEINE Server-Sent Events).
- Implementierung: Übernimmt den Rack-Socket, um zeilengetrennte JSON-Objekte zu streamen.
- Nebeneffekte: Dies ist nicht nur eine Completion-API; sie erstellt echte Discourse Private Message (PM)-Beiträge.
Endpunkt-Details
- Standort:
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 - API-Key-Bereich:
ai:stream_completion(registriert inplugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)
Anfrage-Header
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json
Anfrage-Body-Parameter
| Parameter | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
agent_id |
Integer | Optional* | Kennung für den Agenten. |
agent_name |
String | Optional* | Alternative Kennung für den Agenten. |
query |
String | Ja | Der Prompt/die Frage des Nutzers. |
username |
String | Erforderlich, wenn user_unique_id weggelassen wird |
Wird verwendet, falls der Chat-PM mit einem bestehenden Benutzer verknüpft werden muss. |
user_unique_id |
String | Erforderlich, wenn username weggelassen wird |
Identifiziert den Endnutzer. Erstellt/wiederverwendet einen gestuften Benutzer, der über das benutzerdefinierte Feld ai-stream-conversation-unique-id referenziert wird. |
preferred_username |
String | Optional* | Benutzername für den Benutzer (wenn user_unique_id nicht verwendet wird). |
topic_id |
Integer | Optional | Führt eine bestehende PM-Unterhaltung fort. |
custom_instructions |
String | Optional | Wird in den Prompt-Kontext des Agenten eingefügt. |
Hinweis: Sie müssen den Endnutzer entweder über
username(bestehender Discourse-Benutzer) oderuser_unique_ididentifizieren.
Antwortformat
Der Server gibt eine 200 OK-Antwort mit Transfer-Encoding: chunked zurück. Der Stream besteht aus zeilengetrennten JSON-Objekten.
HTTP-Header
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
Struktur der Stream-Payload
- Kontext-Chunk: Stellt Metadaten bereit (Topic-ID, Bot-Benutzer-ID, Agenten-ID).
- Partielle Chunks: Enthält die gestreamten Textfragmente.
Beispiel-Stream:
{"topic_id":42,"bot_user_id":7,"agent_id":123}
{"partial":"Hello"}
{"partial":" there"}
Der Client sollte die partial-Felder zusammenfügen, um die finale Antwort zu erstellen.
Arbeitsablauf & Nebeneffekte
Der Endpunkt führt während der Ausführung die folgenden Aktionen aus:
- Erstellt einen Benutzerbeitrag mit der rohen
query. - Streamt die KI-Antwort über die chunked-Antwort.
- Erstellt den finalen KI-Antwortbeitrag mit der akkumulierten Antwort.
- Kann bei neuen PMs den Topic-Titel automatisch vergeben.
Benutzerdefinierte, clientseitig ausgeführte Tools
Sie können Tool-Definitionen bereitstellen, um dem Modell die Aufruf externer Tools zu ermöglichen. Der Server pausiert den Stream, wenn ein Tool aufgerufen wird, sodass der Client das Tool ausführen und den Stream fortsetzen kann.
1. Initiale Anfrage mit Tools
Fügen Sie custom_tools in den Anfrage-Body ein:
{
"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. Tool-Aufruf-Ereignis
Wenn das Modell ein Tool aufruft, gibt der Stream ein tool_calls-Ereignis aus und stoppt:
{
"event": "tool_calls",
"tool_calls": [
{
"id": "tool_1",
"name": "client_weather",
"parameters": { "city": "Austin" }
}
],
"resume_token": "..."
}
Der Server persistiert an diesem Punkt den Gesprächszustand in Redis.
3. Fortsetzen mit Tool-Ergebnissen
Der Client führt das Tool aus und setzt den Stream fort:
{
"resume_token": "...",
"tool_results": [
{
"tool_call_id": "tool_1",
"content": { "temperature_c": 23 }
}
]
}
Der Server lädt den gespeicherten Prompt-Zustand neu, fügt das Tool-Ergebnis ein, setzt die Generierung fort und streamt weitere partial-Chunks.
Tool-Limits
- Max. benutzerdefinierte Tools: 20
- Max. Tool-Ergebnisse: 20
- Max. Größe der benutzerdefinierten Tool-Definition: 10.000 Bytes
- Max. Größe des Tool-Ergebnis-Contents: 100 KB
- Resume-TTL: 15 Minuten
- Max. Resume-Runden: 10
Implementierungsreferenzen
- Controller:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb - Streamer:
plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb - Custom Tools Session:
plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb
Testbeispiele
Siehe plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb für umfassende Beispiele:
- Neue gestreamte Unterhaltung: Zeilen 1248-1356
- Custom Tools + Resume-Token: Zeilen 1358-1448
- Parallele Tool-Aufrufe: Zeilen 1467-1590
Beispiel-Implementierung
Ruby-Skript
require 'net/http'
require 'json'
require 'uri'
# Konfiguration
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Oder agent_name verwenden
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<leer lassen, wenn der PM an den Benutzer USERNAME gesendet werden soll>'
# Hilfsfunktion zum Erstellen der URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")
# Erstellen der HTTP-Anfrage
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'
# Vorbereiten des Anfrage-Bodys
body = {
agent_id: AGENT_ID,
query: QUERY,
## Die folgende Zeile entkommentieren, wenn ein bestehender Benutzer für die Unterhaltung verwendet werden soll. Außerdem hat `username` Vorrang vor `user_unique_id`, wenn übergeben.
# username: USERNAME,
## Das folgende Feld zusammen mit `user_unique_id` verwenden, um einen neuen gestuften Benutzer zu erstellen. Bei Verwendung dieses Feldes `username` nicht übergeben.
# 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|
# Prüfen, ob die Antwort erfolgreich ist
if response.code == '200'
puts "Stream erfolgreich gestartet."
puts "Antwort-Header: #{response.to_hash}"
puts "Streaming-Inhalt:"
# Chunked-Antwort lesen
response.read_body do |chunk|
# Die Antwort besteht aus zeilengetrennten JSON-Objekten
chunk.each_line do |line|
line = line.strip
next if line.empty?
begin
json = JSON.parse(line)
if json['topic_id']
puts "\n--- Kontext erhalten ---"
puts "Topic-ID: #{json['topic_id']}"
puts "Bot-Benutzer-ID: #{json['bot_user_id']}"
puts "Agenten-ID: #{json['agent_id']}"
elsif json['partial']
# Teilinhalt streamen
print json['partial']
elsif json['event'] == 'tool_calls'
puts "\n--- Tool-Aufruf erhalten ---"
puts JSON.pretty_generate(json)
# Hier würden Sie die Tool-Ausführung und das Fortsetzen behandeln
# Für dieses Beispiel wird es nur ausgegeben
end
# puts "<new word>"
rescue JSON::ParserError => e
puts "\nFehler beim Parsen von JSON: #{e.message}"
puts "Rohzeile: #{line}"
end
end
end
puts "\n--- Stream beendet ---"
else
puts "Fehler: #{response.code} #{response.message}"
puts "Antwort-Body: #{response.body}"
end
end
Hinweise
- Wenn Sie möchten, dass die Unterhaltung im Namen eines neuen gestuften Benutzers geführt wird, übergeben Sie
unique_user_idundpreferred_usernameals den gewünschten neuen Benutzernamen und lassen Sie das Feldusernameweg. - Wenn Sie mit einem bestehenden Benutzer kommunizieren möchten, übergeben Sie den
usernamedieses Benutzers und lassen Sie die Felderunique_user_idundpreferred_usernameweg.