Discourse API를 통해 AI 봇 사용

개요

Discourse AI는 원시 청크드 HTTP 응답을 통해 AI 에이전트의 응답을 스트리밍하기 위한 관리자/API 엔드포인트를 제공합니다.

  • 프로토콜: 원시 청크드 HTTP 전송 인코딩 (Server-Sent Events가 아님).
  • 구현 방식: Rack 소켓을 하이재킹하여 줄바꿈으로 구분된 JSON 객체를 스트리밍합니다.
  • 부수 효과: 이는 단순한 완성(completion) 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가 생략된 경우 필수 채팅 PM이 기존 사용자와 연결되어야 하는 경우 사용됩니다.
user_unique_id String username이 생략된 경우 필수 최종 사용자를 식별합니다. 커스텀 필드 ai-stream-conversation-unique-id를 키로 사용하여 스테이징된 사용자를 생성/재사용합니다.
preferred_username String 선택적* 사용자의 사용자 이름 (user_unique_id를 사용하지 않는 경우).
topic_id Integer 선택적 기존 PM 대화를 계속합니다.
custom_instructions String 선택적 에이전트 프롬프트 컨텍스트에 추가됩니다.

참고: username(기존 Discourse 사용자) 또는 user_unique_id 중 하나로 최종 사용자를 식별해야 합니다.


응답 형식

서버는 Transfer-Encoding: chunked가 포함된 200 OK 응답을 반환합니다. 스트림은 줄바꿈으로 구분된 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. 새 PM의 경우, 토픽 제목을 자동으로 지정할 수 있습니다.

사용자 정의 클라이언트 실행 도구

모델이 외부 도구를 호출할 수 있도록 도구 정의를 제공할 수 있습니다. 도구가 호출되면 서버는 스트림을 일시 중지하여 클라이언트가 도구를 실행하고 다시 시작할 수 있게 합니다.

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바이트
  • 최대 도구 결과 콘텐츠 크기: 100KB
  • 재개 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 ='<PM이 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개의 좋아요