KI-Bot über die Discourse-API verwenden

Ü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 in plugins/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) oder user_unique_id identifizieren.


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

  1. Kontext-Chunk: Stellt Metadaten bereit (Topic-ID, Bot-Benutzer-ID, Agenten-ID).
  2. 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:

  1. Erstellt einen Benutzerbeitrag mit der rohen query.
  2. Streamt die KI-Antwort über die chunked-Antwort.
  3. Erstellt den finalen KI-Antwortbeitrag mit der akkumulierten Antwort.
  4. 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_id und preferred_username als den gewünschten neuen Benutzernamen und lassen Sie das Feld username weg.
  • Wenn Sie mit einem bestehenden Benutzer kommunizieren möchten, übergeben Sie den username dieses Benutzers und lassen Sie die Felder unique_user_id und preferred_username weg.
2 „Gefällt mir“