通过 Discourse API 使用 AI 机器人

概述

Discourse AI 提供了一个管理员/API 端点,用于通过原始分块 HTTP 响应流式传输 AI 代理的回复。

  • 协议: 原始分块 HTTP 传输编码( 服务器发送事件 SSE)。
  • 实现: 劫持 Rack 套接字以流式传输由换行符分隔的 JSON 对象。
  • 副作用: 这不仅仅是一个补全 API;它会创建真实的 Discourse 私信(PM)帖子。

端点详情

  • 位置: plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272
  • 路由: POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
  • API 密钥范围: ai:stream_completion(注册于 plugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286

请求头

POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json

请求体参数

参数 类型 必填 描述
agent_id Integer 可选* 代理的标识符。
agent_name String 可选* 代理的替代标识符。
query String 用户的提示/问题。
username String 如果省略 user_unique_id 则必填 用于将聊天私信关联到现有用户的情况。
user_unique_id String 如果省略 username 则必填 标识最终用户。根据自定义字段 ai-stream-conversation-unique-id 创建/重用暂存用户。
preferred_username String 可选* 用户的用户名(如果未使用 user_unique_id)。
topic_id Integer 可选 继续现有的私信对话。
custom_instructions String 可选 追加到代理提示上下文中。

注意: 您必须通过 username(现有 Discourse 用户)或 user_unique_id 来标识最终用户。


响应格式

服务器返回 200 OK 响应,并带有 Transfer-Encoding: chunked。流由换行符分隔的 JSON 对象组成。

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

流负载结构

  1. 上下文块: 提供元数据(主题 ID、机器人用户 ID、代理 ID)。
  2. 部分块: 包含流式传输的文本片段。

流示例:

{"topic_id":42,"bot_user_id":7,"agent_id":123}

{"partial":"Hello"}

{"partial":" there"}

客户端应连接 partial 字段以构建最终答案。


工作流程与副作用

该端点在执行期间执行以下操作:

  1. 使用原始 query 创建用户帖子。
  2. 通过分块响应流式传输 AI 答案。
  3. 使用累积的答案创建最终的 AI 回复帖子。
  4. 对于新的私信,可能会自动为主题命名。

自定义客户端执行工具

您可以提供工具定义,允许模型调用外部工具。当调用工具时,服务器会暂停流,允许客户端执行工具并恢复。

1. 包含工具的初始请求

在请求体中包含 custom_tools

{
  "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_calls 事件并停止:

{
  "event": "tool_calls",
  "tool_calls": [
    {
      "id": "tool_1",
      "name": "client_weather",
      "parameters": { "city": "Austin" }
    }
  ],
  "resume_token": "..."
}

此时服务器在 Redis 中持久化对话状态。

3. 使用工具结果恢复

客户端执行工具并恢复流:

{
  "resume_token": "...",
  "tool_results": [
    {
      "tool_call_id": "tool_1",
      "content": { "temperature_c": 23 }
    }
  ]
}

服务器重新加载保存的提示状态,插入工具结果,继续生成并流式传输更多的 partial 块。

工具限制

  • 最大自定义工具数:20
  • 最大工具结果数:20
  • 最大自定义工具定义大小:10,000 字节
  • 最大工具结果内容大小:100 KB
  • 恢复 TTL:15 分钟
  • 最大恢复轮数:10

实现参考

  • 控制器: plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb
  • 流式传输器: plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb
  • 自定义工具会话: plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb

测试示例

请参阅 plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb 以获取综合示例:

  • 新的流式对话: 第 1248-1356 行
  • 自定义工具 + 恢复令牌: 第 1358-1448 行
  • 并行工具调用: 第 1467-1590 行

示例实现

Ruby 脚本
require 'net/http'
require 'json'
require 'uri'

# 配置
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # 或使用 agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<如果希望将私信发送给 USERNAME 用户,请留空>'


# 创建 URI 的辅助方法
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")

# 创建 HTTP 请求
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'

# 准备请求体
body = {
  agent_id: AGENT_ID,
  query: QUERY,
  ## 如果您希望使用现有用户进行对话,请取消注释下面的一行。另外,如果同时传递,`username` 优先于 `user_unique_id`。
  # username: USERNAME,
  ## 使用以下字段配合 `user_unique_id` 以创建新的暂存用户。使用此功能时,请跳过传递 `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|
    # 检查响应是否成功
    if response.code == '200'
    puts "Stream started successfully."
    puts "Response headers: #{response.to_hash}"
    puts "Streaming content:"   
    
    # 读取分块响应
    response.read_body do |chunk|
        # 响应是由换行符分隔的 JSON 对象
        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']
            # 流式传输部分内容
            print json['partial']
            elsif json['event'] == 'tool_calls'
            puts "\n--- Tool Call Received ---"
            puts JSON.pretty_generate(json)
            # 在这里您可以处理工具执行和恢复
            # 在此示例中,我们只是打印它
            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

注意事项

  • 如果您希望对话以新的暂存用户的名义进行,请传递 unique_user_idpreferred_username 作为新的期望用户名,并跳过 username 字段。
  • 如果您希望使用现有用户进行对话,请传递该用户的 username,并跳过 unique_user_idpreferred_username 字段。
2 个赞