Utilizzo del bot AI tramite l'API di Discourse

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 in plugins/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 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) oppure user_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

  1. Chunk di Contesto: Fornisce metadati (ID topic, ID utente bot, ID agente).
  2. 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:

  1. Crea un post utente con la query grezza.
  2. Trasmette in streaming la risposta IA tramite la risposta a blocchi.
  3. Crea il post di risposta IA finale con la risposta accumulata.
  4. 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_id e preferred_username come nuovo nome utente desiderato e omettere il campo username.
  • Se si desidera conversare utilizzando un utente esistente, passare il username di quell’utente e omettere i campi unique_user_id e preferred_username.
2 Mi Piace