Skip to content

Latest commit

ย 

History

73 Commits

Folders and files

Repository files navigation

๐Ÿ“˜ ProcessGPT Agent SDK โ€“ README

1. ์ด๊ฒŒ ๋ญํ•˜๋Š” ๊ฑด๊ฐ€์š”?

์ด SDK๋Š” ProcessGPT ์—์ด์ „ํŠธ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ๋•Œ ํ•„์š”ํ•œ ๊ณตํ†ต ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • DB์—์„œ ์ž‘์—…(todo) ํด๋ง โ†’ ์ฒ˜๋ฆฌํ•  ์ผ๊ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ์ปจํ…์ŠคํŠธ ์ค€๋น„ (์‚ฌ์šฉ์ž ์ •๋ณด, ํผ ์ •์˜, MCP ์„ค์ • ๋“ฑ ์ž๋™์œผ๋กœ ์กฐํšŒ)
  • ๋‹ค์–‘ํ•œ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜(A2A) ๊ณผ ํ˜ธํ™˜
  • ์ด๋ฒคํŠธ(Event) ์ „์†ก ๊ทœ๊ฒฉ ํ†ต์ผํ™” โ†’ ๊ฒฐ๊ณผ๋ฅผ DB์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ
  • ์ฑ„ํŒ…(SSE) ์ „์†ก ๊ณ„์ธต โ†’ ํ•˜ํŠธ๋น„ํŠธ ยท ์žฌ์ ‘์†(attach) ยท ์ค‘์ง€(stop) ๋ฅผ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ œ๊ณต
  • ํ…Œ๋„ŒํŠธ ์ธ์ฆ โ†’ ์š”์ฒญ์ด ๋ณด๋‚ธ tenant_id ๋ฅผ ์š”์ฒญ์ž์˜ JWT ๋กœ ๊ฒ€์ฆ
  • ์—์ด์ „ํŠธ ์‚ฐ์ถœ๋ฌผ โ†’ ๋งŒ๋“  ํŒŒ์ผ์„ ๋น„๊ณต๊ฐœ ๋ฒ„ํ‚ท์— ๋ณด๊ด€ํ•˜๊ณ  ๋งŒ๋ฃŒ๋˜๋Š” ์„œ๋ช… ์ฃผ์†Œ๋กœ ๋‚ด์คŒ

๐Ÿ‘‰ ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด: ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ AI ์—์ด์ „ํŠธ๋ฅผ ๊ฐ™์€ ๊ทœ์น™์œผ๋กœ ์‹คํ–‰/์ €์žฅ/ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ†ตํ•ฉ SDK ์ž…๋‹ˆ๋‹ค.

0.8.0 ์—์„œ ๋‹ฌ๋ผ์ง„ ์ 
์—์ด์ „ํŠธ๊ฐ€ ๋งŒ๋“  ํŒŒ์ผ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋‚ด์ฃผ๋Š” ์ผ์ด SDK ๋กœ ์˜ฌ๋ผ์™”์Šต๋‹ˆ๋‹ค. ๊ทธ๋™์•ˆ ์—์ด์ „ํŠธ๋งˆ๋‹ค ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ์“ฐ๋˜ ์ˆ˜์ง‘ยท๋ณด๊ด€ยท๋ณธ๋ฌธ ๋งํฌ ์น˜ํ™˜์„ processgpt_agent_sdk.artifacts ํ•˜๋‚˜๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค. ์‚ฐ์ถœ๋ฌผ์€ ๋น„๊ณต๊ฐœ ๋ฒ„ํ‚ท์— ๋“ค์–ด๊ฐ€๊ณ  ์ฃผ์†Œ๋Š” ํ•œ ์‹œ๊ฐ„์งœ๋ฆฌ ์„œ๋ช… ์ฃผ์†Œ์ด๋ฉฐ, ๋งŒ๋ฃŒ๋˜๋ฉด file_id ๋กœ ๋‹ค์‹œ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ 7 ์„ ๋ณด์„ธ์š”. 0.8.1 ๋ถ€ํ„ฐ๋Š” ์‚ฐ์ถœ๋ฌผ์ด SSE done ์—๋„ ์‹ค๋ ค ๋‚˜๊ฐ‘๋‹ˆ๋‹ค(7.5).

0.5.0 ์—์„œ ๋‹ฌ๋ผ์ง„ ์ 
์ฑ„ํŒ… SSE ์ „์†ก ๊ณ„์ธต๊ณผ ํ…Œ๋„ŒํŠธ ์ธ์ฆ์ด SDK ๋กœ ์˜ฌ๋ผ์™”์Šต๋‹ˆ๋‹ค. ๊ทธ๋™์•ˆ ๊ฐ ์—์ด์ „ํŠธ ์ €์žฅ์†Œ๊ฐ€ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ์“ฐ๋˜ ํ•˜ํŠธ๋น„ํŠธยท์žฌ์ ‘์†ยท์ค‘์ง€ยท์ธ์ฆ์„ mount_chat_routes() ํ•œ ๋ฒˆ์œผ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ 4.5 ยท 4.6 ์„ ๋ณด์„ธ์š”. ๊ธฐ์กด mount_chat_sse() ๋‹จ๋… ํ˜ธ์ถœ์€ ๋™์ž‘์ด ๊ทธ๋Œ€๋กœ๋ผ ๊ณง๋ฐ”๋กœ ์˜ฌ๋ ค๋„ ๊นจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


2. ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ

flowchart TD
    subgraph DB["Postgres / Supabase"]
        T["todolist"]:::db
        E["events"]:::db
        CH["chats"]:::db
    end

    subgraph SDK["SDK โ€” ํ”„๋กœ์„ธ์Šค(ํด๋ง)"]
        P["Polling<br/>(fetch_pending_task)"] --> C["Context ์ค€๋น„<br/>(fetch_context_bundle ๋“ฑ)"]
        C --> X["Executor<br/>(MinimalExecutor)"]
    end

    subgraph CHAT["SDK โ€” ์ฑ„ํŒ…(SSE)"]
        G["ํ…Œ๋„ŒํŠธ ๊ฐ€๋“œ<br/>(JWT ๊ฒ€์ฆ)"] --> S["POST /chat/stream"]
        R["๋Ÿฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ"] --> A["POST /chat/stream/attach"]
        K["POST /chat/stop"]
        ST["POST /chat/steer"] --> Q["์ˆ˜์ • ์ง€์‹œ ๋Œ€๊ธฐ์—ด"]
    end

    S --> X
    K -->|์ทจ์†Œ| X
    Q -->|์•ˆ์ „ ์ง€์ ์—์„œ ๋ฐ˜์˜| X
    X -->|TaskStatusUpdateEvent| E
    X -->|TaskArtifactUpdateEvent| T
    X -->|ํ† ํฐยทdone| R
    R --> CH

    classDef db fill:#f2f2f2,stroke:#333,stroke-width:1px;
Loading
  • todolist: ๊ฐ ์ž‘์—…(Task)์˜ ์ง„ํ–‰ ์ƒํƒœ, ๊ฒฐ๊ณผ๋ฌผ ์ €์žฅ
  • events: ์‹คํ–‰ ์ค‘๊ฐ„์— ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ ๋กœ๊ทธ ์ €์žฅ
  • chats: ์ฑ„ํŒ… ํ„ด์˜ ์ตœ์ข… ์‘๋‹ต ์ €์žฅ
  • SDK๋Š” ์„ธ ํ…Œ์ด๋ธ”์„ ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐํ•ด ์ค๋‹ˆ๋‹ค.
  • ์ฑ„ํŒ… ๊ฒฝ๋กœ๋Š” ์š”์ฒญ์ด Executor ์— ๋‹ฟ๊ธฐ ์ „์— ํ…Œ๋„ŒํŠธ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , ํ„ด์ด ๋‚ด๋ณด๋‚ด๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋Ÿฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋‚จ๊ฒจ ์žฌ์ ‘์†ยท์ค‘์ง€๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

3. A2A ํƒ€์ž…๊ณผ ์ด๋ฒคํŠธ ์ข…๋ฅ˜

A2A ํƒ€์ž… (2๊ฐ€์ง€)

A2A ํƒ€์ž… ์„ค๋ช… ๋งค์นญ ํ…Œ์ด๋ธ”
TaskStatusUpdateEvent ์ž‘์—… ์ƒํƒœ ์—…๋ฐ์ดํŠธ events ํ…Œ์ด๋ธ”
TaskArtifactUpdateEvent ์ž‘์—… ๊ฒฐ๊ณผ๋ฌผ ์—…๋ฐ์ดํŠธ todolist ํ…Œ์ด๋ธ”

(v1.0) Enum ๋ณ€๊ฒฝ์‚ฌํ•ญ: snake_case โ†’ SCREAMING_SNAKE_CASE

a2a-sdk v1.0๋ถ€ํ„ฐ A2A ์ŠคํŽ™(ProtoJSON) ์ •ํ•ฉ์„ฑ์„ ์œ„ํ•ด ๋ชจ๋“  enum ๊ฐ’์ด ๋Œ€๋ฌธ์ž ์Šค๋„ค์ดํฌ ์ผ€์ด์Šค๋กœ ํ‘œ์ค€ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • TaskState

    • TaskState.submitted โ†’ TaskState.TASK_STATE_SUBMITTED
    • TaskState.working โ†’ TaskState.TASK_STATE_WORKING
    • TaskState.completed โ†’ TaskState.TASK_STATE_COMPLETED
    • TaskState.failed โ†’ TaskState.TASK_STATE_FAILED
    • TaskState.canceled โ†’ TaskState.TASK_STATE_CANCELED
    • TaskState.input_required โ†’ TaskState.TASK_STATE_INPUT_REQUIRED
    • TaskState.auth_required โ†’ TaskState.TASK_STATE_AUTH_REQUIRED
    • TaskState.rejected โ†’ TaskState.TASK_STATE_REJECTED
    • (์ถ”๊ฐ€) TaskState.TASK_STATE_UNSPECIFIED
  • Role

    • Role.user โ†’ Role.ROLE_USER
    • Role.agent โ†’ Role.ROLE_AGENT
    • (์ถ”๊ฐ€) Role.ROLE_UNSPECIFIED

events.event_type enum ๋งคํ•‘

DB ์˜ events.event_type ์ปฌ๋Ÿผ์€ enum ์ž…๋‹ˆ๋‹ค. Executor ๊ฐ€ emitํ•œ TaskStatusUpdateEvent ๊ฐ€ events ํ…Œ์ด๋ธ”์— ์ €์žฅ๋  ๋•Œ ์–ด๋–ค enum ๊ฐ’์œผ๋กœ ๋“ค์–ด๊ฐ€๋Š”์ง€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.

event_type (DB enum) ๋ฐœํ–‰ ์ฃผ์ฒด A2A ์ด๋ฒคํŠธ ํ˜•ํƒœ ๋งคํ•‘ ๋ฐฉ์‹
task_started Executor TaskStatusUpdateEvent(state=SUBMITTED) ์ž๋™ (state ๊ธฐ๋ฐ˜)
task_completed Executor TaskStatusUpdateEvent(state=COMPLETED) ์ž๋™ (state ๊ธฐ๋ฐ˜)
error Executor TaskStatusUpdateEvent(state=FAILED) ์ž๋™ (state ๊ธฐ๋ฐ˜)
human_asked Executor TaskStatusUpdateEvent(state=INPUT_REQUIRED) ์ž๋™ (state ๊ธฐ๋ฐ˜)
task_working Executor TaskStatusUpdateEvent(state=WORKING) + metadata["event_type"]="task_working" ๋ช…์‹œ
tool_usage_started / tool_usage_finished Executor TaskStatusUpdateEvent(state=WORKING) + metadata["event_type"]="tool_usage_*" ๋ช…์‹œ (sub-event, ์•„๋ž˜ ์ฐธ์กฐ)
crew_completed SDK (Executor ๊ฐ€ emit X) ์ž๋™ โ€” TaskArtifactUpdateEvent(last_chunk=True) ์ฒ˜๋ฆฌ ์‹œ์ ์— SDK ๊ฐ€ ๋ฐœํ–‰ (์•ˆ์ „๋ง: framework ์˜ task_done())

์ž๋™ ๋งคํ•‘ ๊ทœ์น™: SDK ๋Š” ๋‹ค์Œ lifecycle state ๋ฅผ ์ž๋™์œผ๋กœ enum ๊ฐ’์œผ๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค.

  • TASK_STATE_SUBMITTED โ†’ task_started
  • TASK_STATE_COMPLETED โ†’ task_completed
  • TASK_STATE_FAILED โ†’ error
  • TASK_STATE_INPUT_REQUIRED โ†’ human_asked

TASK_STATE_WORKING ์€ ์˜๋„์ ์œผ๋กœ ์ž๋™ ๋งคํ•‘ ๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. WORKING ์€ ๋„ˆ๋ฌด ๊ด‘๋ฒ”์œ„ํ•˜๊ณ  ๋„๋ฉ”์ธ sub-event(tool_usage_* ๋“ฑ) ์˜ ๋ฒ ์ด์Šค๋กœ๋„ ์žฌ์‚ฌ์šฉ๋˜๋ฏ€๋กœ, sub-event ์˜๋ฏธ์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก NULL ๋กœ ๋‘๊ฑฐ๋‚˜ metadata["event_type"] ์œผ๋กœ ๋ช…์‹œํ•˜์„ธ์š”. metadata ๊ฐ€ ์—†์œผ๋ฉด event_type ์ปฌ๋Ÿผ์€ NULL ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค(ํ—ˆ์šฉ๋จ).

๋ช…์‹œ vs ์ž๋™ ์šฐ์„ ์ˆœ์œ„: metadata["event_type"] ๊ฐ€ ์žˆ์œผ๋ฉด ์ž๋™ ๋งคํ•‘๋ณด๋‹ค ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค (explicit > implicit). ์˜ˆ: state=WORKING + metadata["event_type"]="tool_usage_started" โ†’ tool_usage_started ๋กœ ์ €์žฅ.

์‚ฌ๋žŒ์—๊ฒŒ ๋ฌป๊ณ  ๋๋‚œ ์‹คํ–‰ (INPUT_REQUIRED): ์‹คํ–‰ ์ค‘ INPUT_REQUIRED ๋ฅผ ๋‚ธ ๋’ค (WORKING/COMPLETED ๋กœ ์ด์–ด์ง€์ง€ ์•Š๊ณ ) ๋๋‚˜๋ฉด SDK ๋Š” ๊ทธ ์‹คํ–‰์„ ์™„๋ฃŒ๋กœ ๋ณด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋’ค๋”ฐ๋ฅด๋Š” TaskArtifactUpdateEvent(last_chunk=True) ๋Š” ์งˆ๋ฌธ ๋ณธ๋ฌธ์œผ๋กœ ๋ณด๊ณ  todolist ๊ฒฐ๊ณผ(output/draft)์— ์ €์žฅํ•˜์ง€ ์•Š์œผ๋ฉฐ, crew_completed ๋„ ๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ์ž‘์—…์„ draft_status='HUMAN_ASKED' ๋กœ ๋‘๊ณ  ์ ์œ (consumer, lease)๋ฅผ ํ’‰๋‹ˆ๋‹ค โ€” status ๋Š” IN_PROGRESS ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ตํ•˜๋ฉด ํ™”๋ฉด์ด FB_REQUESTED ๋กœ ๋ฐ”๊พธ๊ณ  ์›Œ์ปค๊ฐ€ ๋‹ค์‹œ ์ง‘์Šต๋‹ˆ๋‹ค. ์•„ํ‹ฐํŒฉํŠธ ์—†์ด ์ƒํƒœ๋งŒ ๋‚ด๊ณ  ๋๋‚˜๋„ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด ๊ทœ์น™์ด ์—†์œผ๋ฉด COMPLETE ๋ชจ๋“œ์—์„œ ์งˆ๋ฌธ์ด ์‚ฐ์ถœ๋ฌผ๋กœ SUBMITTED ๋˜์–ด ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค.

task_completed vs TaskArtifactUpdateEvent: ๋‘˜์€ ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค. task_completed ๋Š” events ํ…Œ์ด๋ธ”์˜ lifecycle ํ‘œ์‹œ์ด๊ณ , ์‹ค์ œ ๊ฒฐ๊ณผ๋ฌผ ์ €์žฅ์€ TaskArtifactUpdateEvent(last_chunk=True) ๊ฐ€ todolist ํ…Œ์ด๋ธ”์— ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

A2A ํƒ€์ž… = ๋ผ์šฐํŒ… ํ‚ค (SDK ๋Š” dumb transport)

์›์น™: Executor ๋Š” A2A ํ‘œ์ค€ ์ด๋ฒคํŠธ์™€ ํ‘œ์ค€ ํ•„๋“œ๋งŒ emit. SDK ๋Š” ๋งค์ง ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—†์ด A2A ์ด๋ฒคํŠธ ํƒ€์ž… ์ž์ฒด๋ฅผ ๋ผ์šฐํŒ… ํ‚ค๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋ง์€ Executor ์ฑ…์ž„์ด๊ณ  SDK ๋Š” ๋ฐ›์€ ๋Œ€๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.

๋ผ์šฐํŒ… ๋งคํŠธ๋ฆญ์Šค:

A2A ์ด๋ฒคํŠธ ํƒ€์ž… ChatEventQueue ProcessEventQueue
Task (๋ผ์ดํ”„์‚ฌ์ดํด ๋งˆ์ปค) silently ignore silently ignore
Message SSE {"type":"token","content":...} (๋ฐ˜๋ณต ํ—ˆ์šฉ = ํ† ํฐ ์ŠคํŠธ๋ฆฌ๋ฐ) silently ignore
TaskStatusUpdateEvent silently ignore events ํ…Œ์ด๋ธ” ์ €์žฅ (state/text ๊ทธ๋Œ€๋กœ)
TaskArtifactUpdateEvent(last_chunk=True) SSE done + chats ์ €์žฅ todolist ์ €์žฅ (is_final=True)

Executor ์˜ ํ‘œ์ค€ ํ๋ฆ„ (LLM ์ŠคํŠธ๋ฆฌ๋ฐ ์˜ˆ์‹œ):

  1. Task (state=SUBMITTED) โ€” ๋ผ์ดํ”„์‚ฌ์ดํด ์‹œ์ž‘
  2. ํ† ํฐ๋งˆ๋‹ค:
    • Message(text=token) โ€” ์ฑ„ํŒ…์šฉ
    • TaskStatusUpdateEvent(state=WORKING, text=token) โ€” ํ”„๋กœ์„ธ์Šค์šฉ (ํ•„์š” ์‹œ JSON payload)
  3. TaskStatusUpdateEvent(state=COMPLETED, text=full) โ€” ์ข…๋ฃŒ ์•Œ๋ฆผ (์„ ํƒ)
  4. TaskArtifactUpdateEvent(last_chunk=True, text=full) โ€” ์ตœ์ข… ๊ฒฐ๊ณผ

๋ถ€ํ•˜ ์šฐ๋ ค๊ฐ€ ์žˆ๋‹ค๋ฉด (์˜ˆ: events ํ…Œ์ด๋ธ”์— ํ† ํฐ row ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์ด ์Œ“์ผ ๊ฒฝ์šฐ) Executor ๊ฐ€ ์ง์ ‘ ํ•„ํ„ฐ๋ง/์ง‘๊ณ„ํ•˜์„ธ์š”. SDK ๋Š” ์ •์ฑ…์„ ๊ฐ•์ œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋„๋ฉ”์ธ sub-event ๊ธฐ๋ก (๋„๊ตฌ ํ˜ธ์ถœ ๋“ฑ)

tool_usage_started, tool_usage_finished ๊ฐ™์€ ๋„๋ฉ”์ธ sub-event ๋Š” A2A TaskState ์— ์ง์ ‘ ๋งคํ•‘๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค โ€” TaskState ๋Š” ์ž‘์—… ์ „์ฒด์˜ lifecycle (SUBMITTED โ†’ WORKING โ†’ COMPLETED/FAILED) ์ถ”์ƒํ™”์ด๊ณ , ๋„๊ตฌ ํ˜ธ์ถœ์€ ๊ทธ ์•ˆ์—์„œ ์ผ์–ด๋‚˜๋Š” ์„ธ๋ถ€ ์‚ฌ๊ฑด์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฐ sub-event ๋Š” state=TASK_STATE_WORKING ๊ทธ๋Œ€๋กœ ๋‘๊ณ , metadata["event_type"] ๋กœ enum ๊ฐ’์„ ๋ช…์‹œํ•˜์„ธ์š”. SDK ๊ฐ€ ๊ทธ ๊ฐ’์„ events.event_type ์ปฌ๋Ÿผ์— ๊ทธ๋Œ€๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. data ์ปฌ๋Ÿผ์—๋Š” text ๋กœ ์‹ค์€ JSON payload (๋„๊ตฌ ์ด๋ฆ„, ์ธ์ž, ๊ฒฐ๊ณผ ๋“ฑ) ๊ฐ€ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

import json
from a2a.helpers import new_text_status_update_event
from a2a.types import TaskState

# ๋„๊ตฌ ํ˜ธ์ถœ ์‹œ์ž‘
evt_start = new_text_status_update_event(
    task_id=task_id, context_id=context_id,
    state=TaskState.TASK_STATE_WORKING,
    text=json.dumps(
        {"tool": "web_search", "args": {"query": "process-gpt"}},
        ensure_ascii=False,
    ),
)
evt_start.metadata.update({"event_type": "tool_usage_started"})
await event_queue.enqueue_event(evt_start)

# ... ๋„๊ตฌ ์‹ค์ œ ํ˜ธ์ถœ ...

# ๋„๊ตฌ ํ˜ธ์ถœ ์ข…๋ฃŒ
evt_end = new_text_status_update_event(
    task_id=task_id, context_id=context_id,
    state=TaskState.TASK_STATE_WORKING,
    text=json.dumps(
        {"tool": "web_search", "result_summary": "...", "elapsed_ms": 312},
        ensure_ascii=False,
    ),
)
evt_end.metadata.update({"event_type": "tool_usage_finished"})
await event_queue.enqueue_event(evt_end)

TaskState ๋Š” lifecycle, metadata ๋Š” ๋„๋ฉ”์ธ ๋ถ„๋ฅ˜: A2A ํ‘œ์ค€ envelope ์•ˆ์— ๋จธ๋ฌด๋ฅด๋ฉด์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋„ enum ์œผ๋กœ ์ •ํ™•ํžˆ ๊ธฐ๋กํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. metadata ์ž์ฒด๋Š” A2A TaskStatusUpdateEvent ์˜ ํ‘œ์ค€ free-form ํ•„๋“œ๋ผ "A2A ํ‘œ์ค€๋งŒ ์‚ฌ์šฉ" ์›์น™๊ณผ ์ถฉ๋Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋นˆ๋„ ์ฃผ์˜: tool_usage ๋Š” trace ์„ฑ๊ฒฉ์ด๋ผ LLM ํ•œ ๋ฒˆ์— ๋„๊ตฌ 5๋ฒˆ ํ˜ธ์ถœํ•˜๋ฉด events row 10๊ฐœ๊ฐ€ ์Œ“์ž…๋‹ˆ๋‹ค. ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•˜๋ฉด Executor ์ธก์—์„œ sampling/aggregation ์„ ์ ์šฉํ•˜๊ฑฐ๋‚˜, ๊ฒฐ๊ณผ๋งŒ ํ•œ ๋ฒˆ์— ๋ฌถ์–ด emit ํ•˜์„ธ์š”. tool_usage_* ๋Š” ๋„๊ตฌ ํ˜ธ์ถœ ๋‹จ์œ„์—์„œ๋งŒ emitํ•˜๊ณ , ํ† ํฐ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฃจํ”„ ์•ˆ์—๋Š” ์ ˆ๋Œ€ ๋„ฃ์ง€ ๋งˆ์„ธ์š”.


4. ์‚ฌ์šฉ ์˜ˆ์‹œ

์ด SDK๋Š” โ€œํ•˜๋‚˜์˜ ์™„์ œํ’ˆ ์„œ๋น„์Šคโ€๊ฐ€ ์•„๋‹ˆ๋ผ, ๋‚ด ์„œ๋น„์Šค์— ๋ถ™์—ฌ์„œ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ/๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ๋Š” ํ•œ ํ”„๋กœ์„ธ์Šค์—์„œ ๋‹ค์Œ์„ ๋™์‹œ์— ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ํ”„๋กœ์„ธ์Šค(ํด๋ง): await server.run()๋กœ DB์—์„œ todo๋ฅผ ๊ฐ€์ ธ์™€ ์ฒ˜๋ฆฌ
  • ์ฑ„ํŒ…(SSE): /chat/stream ์—”๋“œํฌ์ธํŠธ๋กœ ์š”์ฒญ์„ ๋ฐ›์•„ Message-only๋กœ ์‘๋‹ต + chats์— ์ €์žฅ
  • ์ฑ„ํŒ… ๋ถ€๊ฐ€ ๋ผ์šฐํŠธ: /chat/stream/attach(์žฌ์ ‘์†) ยท /chat/stop(์ค‘์ง€) ยท /chat/steer(์ˆ˜์ • ์ง€์‹œ)

4.1 ์„œ๋ฒ„ ๊ตฌ์„ฑ ์˜ˆ์‹œ (ํด๋ง + SSE ํ•จ๊ป˜)

import asyncio

import uvicorn
from starlette.applications import Starlette

from processgpt_agent_sdk import ProcessGPTAgentServer
from my_service.my_executor import MyExecutor


async def main():
    server = ProcessGPTAgentServer(
        agent_executor=MyExecutor(),
        agent_type="langchain-react",
        # ์š”์ฒญ ๋ณธ๋ฌธ์˜ tenant_id ๋ฅผ ์š”์ฒญ์ž์˜ JWT ๋กœ ๊ฒ€์ฆํ•œ๋‹ค(๊ธฐ๋ณธ๊ฐ’). 4.6 ์ฐธ๊ณ .
        tenant_auth=True,
    )

    app = Starlette()
    # /chat/stream ยท /chat/stream/attach ยท /chat/stop ยท /chat/steer ๋ฅผ ํ•œ ๋ฒˆ์— ๋ถ™์ด๊ณ ,
    # tenant_auth ๊ฐ€ ์ผœ์ ธ ์žˆ์œผ๋ฉด ์ŠคํŠธ๋ฆผ ๊ฒฝ๋กœ์— ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด๋„ ๊ฑด๋‹ค.
    server.mount_chat_routes(app)

    uvicorn_server = uvicorn.Server(
        uvicorn.Config(app, host="127.0.0.1", port=8010, log_level="info")
    )

    await asyncio.gather(
        server.run(),
        uvicorn_server.serve(),
    )


if __name__ == "__main__":
    asyncio.run(main())

์ŠคํŠธ๋ฆผ ํ•˜๋‚˜๋งŒ ํ•„์š”ํ•˜๋ฉด mount_chat_sse(app, path="/chat/stream") ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(ํ•˜ํŠธ๋น„ํŠธ์™€ ๋Ÿฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋Š” ์ด ๊ฒฝ๋กœ์—๋„ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค). ๋‹ค๋งŒ ์žฌ์ ‘์†ยท์ค‘์ง€ ๋ผ์šฐํŠธ์™€ ํ…Œ๋„ŒํŠธ ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด๋Š” mount_chat_routes() ๋งŒ ๋ถ™์—ฌ ์ค๋‹ˆ๋‹ค.

4.2 Executor ๊ตฌํ˜„ ์˜ˆ์‹œ (A2A ํ‘œ์ค€๋งŒ ์‚ฌ์šฉ)

์ œ 1์›์น™: Executor ๋Š” A2A ํ‘œ์ค€ ์ด๋ฒคํŠธ๋งŒ emit. SDK ๋งค์ง ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(metadata.update({"type":"token",...}) ๊ฐ™์€) ์ผ์ ˆ ์‚ฌ์šฉ ๊ธˆ์ง€. A2A ์ด๋ฒคํŠธ ํƒ€์ž… ์ž์ฒด๊ฐ€ ๋ผ์šฐํŒ… ํ‚ค.

์ด๋ฒคํŠธ ํ๋ฆ„:

  1. Task(state=SUBMITTED) โ€” ๋ผ์ดํ”„์‚ฌ์ดํด ์‹œ์ž‘
  2. ํ† ํฐ๋งˆ๋‹ค:
    • Message(text=token) โ€” ์ฑ„ํŒ…(SSE) ํ† ํฐ ์ฒญํฌ. ChatEventQueue ๊ฐ€ ์ฒ˜๋ฆฌ.
    • TaskStatusUpdateEvent(state=WORKING, text=token) โ€” ํ”„๋กœ์„ธ์Šค ์ง„ํ–‰. ProcessEventQueue ๊ฐ€ events ํ…Œ์ด๋ธ”์— ์ €์žฅ.
  3. TaskStatusUpdateEvent(state=COMPLETED, text=full) โ€” ์ข…๋ฃŒ ์•Œ๋ฆผ (์„ ํƒ)
  4. TaskArtifactUpdateEvent(last_chunk=True, text=full) โ€” ์ตœ์ข… ๊ฒฐ๊ณผ. ๋‘˜ ๋‹ค ์ฒ˜๋ฆฌ (chats / todolist).

๋„๊ตฌ ํ˜ธ์ถœ ๊ฐ™์€ ๋„๋ฉ”์ธ sub-event ๋Š” ์œ„ ์ฝ”๋“œ ํ๋ฆ„๊ณผ ๋ณ„๊ฐœ๋กœ, "๋„๋ฉ”์ธ sub-event ๊ธฐ๋ก" ์„น์…˜์˜ ํŒจํ„ด (metadata["event_type"]) ์„ ์ฐธ๊ณ ํ•ด์„œ ๋„๊ตฌ ํ˜ธ์ถœ ๋‹จ์œ„์—์„œ emit ํ•˜์„ธ์š”.

import os

from a2a.helpers import (
    new_task,
    new_text_artifact_update_event,
    new_text_message,
    new_text_status_update_event,
)
from a2a.types import Role, TaskState
import litellm


class MyExecutor(...):
    async def execute(self, context, event_queue):
        model = os.environ.get("LLM_MODEL")
        proxy_url = (os.environ.get("LLM_PROXY_URL") or "").rstrip("/")
        api_key = os.environ.get("LLM_PROXY_API_KEY")
        api_base = proxy_url if proxy_url.endswith("/v1") else f"{proxy_url}/v1"

        task_id = str(context.task_id)
        context_id = str(context.context_id)

        # 1) ๋ผ์ดํ”„์‚ฌ์ดํด ์‹œ์ž‘
        await event_queue.enqueue_event(
            new_task(task_id=task_id, context_id=context_id, state=TaskState.TASK_STATE_SUBMITTED)
        )

        # 2) LLM ์ŠคํŠธ๋ฆฌ๋ฐ
        stream = await litellm.acompletion(
            model=model,
            messages=[
                {"role": "system", "content": "You are a helpful assistant. Reply in Korean."},
                {"role": "user", "content": context.get_user_input()},
            ],
            temperature=0, stream=True,
            api_base=api_base, api_key=api_key,
        )

        full = ""
        async for chunk in stream:
            try:
                token = chunk.choices[0].delta.content
            except Exception:
                token = None
            if not token:
                continue
            full += token

            # ์ฑ„ํŒ…์šฉ โ€” Message 1๊ฐœ = SSE token 1๊ฐœ
            await event_queue.enqueue_event(
                new_text_message(text=token, role=Role.ROLE_AGENT)
            )

            # ํ”„๋กœ์„ธ์Šค์šฉ โ€” events ํ…Œ์ด๋ธ”์— ์ง„ํ–‰ row 1๊ฐœ์”ฉ.
            # ๋ถ€ํ•˜๊ฐ€ ์šฐ๋ ค๋˜๋ฉด ์—ฌ๊ธฐ์„œ ์ง์ ‘ ํ•„ํ„ฐ๋ง/์ง‘๊ณ„ (์˜ˆ: JSON payload ๋‹จ์œ„๋กœ๋งŒ emit).
            await event_queue.enqueue_event(
                new_text_status_update_event(
                    task_id=task_id, context_id=context_id,
                    state=TaskState.TASK_STATE_WORKING,
                    text=token,
                )
            )

        # 3) ์ข…๋ฃŒ ์•Œ๋ฆผ (์„ ํƒ)
        await event_queue.enqueue_event(
            new_text_status_update_event(
                task_id=task_id, context_id=context_id,
                state=TaskState.TASK_STATE_COMPLETED,
                text=full,
            )
        )

        # 4) ์ตœ์ข… ๊ฒฐ๊ณผ โ€” chats / todolist ์–‘์ชฝ์ด ๋™์ผ ์ด๋ฒคํŠธ๋กœ ์ €์žฅ
        await event_queue.enqueue_event(
            new_text_artifact_update_event(
                task_id=task_id, context_id=context_id,
                name="assistant_response",
                text=full,
                last_chunk=True,
            )
        )

์ €์žฅ ํ˜•์‹: chats ํ…Œ์ด๋ธ”์— ์ €์žฅ๋˜๋Š” payload ๊ตฌ์กฐ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ฑ…์ž„์ง‘๋‹ˆ๋‹ค. Executor๋Š” raw A2A ์ด๋ฒคํŠธ๋งŒ emitํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ €์žฅ ์Šคํ‚ค๋งˆ๋ฅผ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด mount_chat_sse(persist=...)๋กœ ์ปค์Šคํ…€ persist ํ•จ์ˆ˜๋ฅผ ์ฃผ์ž…ํ•˜์„ธ์š”.

ํ•˜์œ„ํ˜ธํ™˜: ๊ธฐ์กด์— ์ฑ„ํŒ… ๊ฒฝ๋กœ์—์„œ Message๋กœ ์ตœ์ข… ์‘๋‹ต์„ emitํ•˜๋˜ Executor๋„ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ChatEventQueue๋Š” TaskArtifactUpdateEvent(๊ถŒ์žฅ) ๋˜๋Š” Message ๋‘˜ ๋‹ค ์ตœ์ข… ์‘๋‹ต์œผ๋กœ ๋ฐ›์•„๋“ค์ž…๋‹ˆ๋‹ค.

4.3 ์ฑ„ํŒ…(SSE) ์š”์ฒญ ์˜ˆ์‹œ

์š”์ฒญ ๋ฐ”๋”” ์˜ˆ์‹œ:

curl -N -X POST http://127.0.0.1:8010/chat/stream \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer <supabase-jwt>' \
  -d '{"message":"hello","conversation_id":"conv-1","tenant_id":"t1","user_uid":"u1"}'

tenant_auth=True(๊ธฐ๋ณธ๊ฐ’) ๋ฉด Authorization: Bearer โ€ฆ ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ์ด ์—†์œผ๋ฉด 401, ๋ณธ๋ฌธ์˜ tenant_id ๊ฐ€ ๊ทธ ์‚ฌ์šฉ์ž์˜ ์†Œ์†์ด ์•„๋‹ˆ๋ฉด 403 ์ž…๋‹ˆ๋‹ค. ๋ณธ๋ฌธ์— tenant_id ๋ฅผ ๋„ฃ์ง€ ์•Š์œผ๋ฉด ๊ฒ€์ฆ๋œ ๊ฐ’์ด ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. ํ•˜์œ„ํ˜ธํ™˜์œผ๋กœ ๋ณธ๋ฌธ user_jwt ๋„ ๋ฐ›์Šต๋‹ˆ๋‹ค.

4.4 ์„ค์น˜(์˜ต์…˜: SSE, ์ธ์ฆ)

์ฑ„ํŒ…(SSE)์„ ํฌํ•จํ•ด ์‚ฌ์šฉํ•˜๋ ค๋ฉด extras๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

pip install "process-gpt-agent-sdk[sse]"

ํ…Œ๋„ŒํŠธ ์ธ์ฆ๊นŒ์ง€ ์“ฐ๋ ค๋ฉด JWT ๊ฒ€์ฆ์šฉ extras๋ฅผ ํ•จ๊ป˜ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

pip install "process-gpt-agent-sdk[sse,auth]"
extras ๋“ค์–ด์˜ค๋Š” ๊ฒƒ ์–ธ์ œ ํ•„์š”ํ•œ๊ฐ€
sse starlette, uvicorn ์ฑ„ํŒ… ๋ผ์šฐํŠธ๋ฅผ ๋งˆ์šดํŠธํ•  ๋•Œ
auth pyjwt[crypto] tenant_auth=True ๋กœ ๋‘˜ ๋•Œ

auth ๋Š” ํ•จ์ˆ˜ ์•ˆ์—์„œ import ํ•˜๋ฏ€๋กœ, ์ธ์ฆ์„ ๋ˆ ์„œ๋ฒ„๋Š” ์„ค์น˜ํ•˜์ง€ ์•Š์•„๋„ ๊ธฐ๋™์— ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค(๊ฒ€์ฆ์„ ์‹ค์ œ๋กœ ์‹œ๋„ํ•˜๋Š” ์ˆœ๊ฐ„ ์„ค์น˜ ์•ˆ๋‚ด์™€ ํ•จ๊ป˜ 500 ์ด ๋‚ฉ๋‹ˆ๋‹ค).

์ฐธ๊ณ ๋กœ, ๋ ˆํฌ์—๋Š” ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ƒ˜ํ”Œ(sample_server/minimal_server.py, sample_server/minimal_executor.py)๋„ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

4.5 ์ฑ„ํŒ… ์ „์†ก ๊ณ„์ธต (ํ•˜ํŠธ๋น„ํŠธ ยท ์žฌ์ ‘์† ยท ์ค‘์ง€)

mount_chat_routes() ๋ฅผ ์“ฐ๋ฉด ์•„๋ž˜ ์„ธ ๊ฐ€์ง€๊ฐ€ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ์˜ต๋‹ˆ๋‹ค. Executor ๋Š” ๋ฐ”๋€Œ์ง€ ์•Š์Šต๋‹ˆ๋‹ค โ€” ์ง€๊ธˆ๊นŒ์ง€์ฒ˜๋Ÿผ A2A ์ด๋ฒคํŠธ๋งŒ emit ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Šฅ ๋ฌด์—‡์„ ํ•ด๊ฒฐํ•˜๋‚˜
ํ•˜ํŠธ๋น„ํŠธ ํ•œ ํ„ด์€ LLM ์ด ์˜ค๋ž˜ ์ƒ๊ฐํ•˜๋Š” ๋™์•ˆ ์ˆ˜ ๋ถ„์”ฉ ์•„๋ฌด ์ด๋ฒคํŠธ๋„ ๋‚ด๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค. ์ค‘๊ฐ„ ํ”„๋ก์‹œ(Cloudflare ๋“ฑ)๊ฐ€ ์œ ํœด ์ปค๋„ฅ์…˜์„ 100์ดˆ ์•ˆํŒŽ์—์„œ ๋Š์œผ๋ฉด ๋ฐฑ์—”๋“œ๋Š” ๊ณ„์† ๋„๋Š”๋ฐ ํ™”๋ฉด๋งŒ "์ƒ๊ฐ ์ค‘โ€ฆ" ์—์„œ ๋ฉˆ์ถ˜๋‹ค. 15์ดˆ๋งˆ๋‹ค SSE ์ฃผ์„(: keep-alive)์„ ๋ผ์›Œ ์ปค๋„ฅ์…˜์„ ์‚ด๋ ค ๋‘”๋‹ค. ์ฃผ์„์ด๋ผ ํ”„๋ก ํŠธ ํŒŒ์„œ(data: ๋งŒ ์ฒ˜๋ฆฌ)๋Š” ๋ฌด์‹œํ•œ๋‹ค.
์žฌ์ ‘์† ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ๋ฐฉ์„ ๋‹ค์‹œ ์—ด๋ฉด ์ง„ํ–‰ ์ค‘์ธ ํ„ด์˜ ๊ฒฐ๊ณผ๋ฅผ ์˜์˜ ๋ชป ๋ฐ›์•˜๋‹ค. /chat/stream/attach ๊ฐ€ ์ง€๊ธˆ๊นŒ์ง€ ์Œ“์ธ ๋ณธ๋ฌธ์„ snapshot 1๊ฑด์œผ๋กœ ์ฃผ๊ณ  ์ดํ›„ ํ† ํฐ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ž‡๋Š”๋‹ค.
์ค‘์ง€ ํ”„๋ก ํŠธ์˜ ์ค‘์ง€ ๋ฒ„ํŠผ์€ ์ž๊ธฐ ์ชฝ fetch ๋งŒ ๋Š์„ ๋ฟ์ด๋ผ ์„œ๋ฒ„ ์‹คํ–‰์€ ๊ณ„์† ๋Œ๋ฉฐ ํ† ํฐยท๋„๊ตฌ ํ˜ธ์ถœ์„ ๊ทธ๋Œ€๋กœ ์†Œ๋น„ํ–ˆ๋‹ค. /chat/stop ์ด ์‹คํ–‰ task ๋ฅผ ์‹ค์ œ๋กœ ์ทจ์†Œํ•œ๋‹ค.

์—ฌ๊ธฐ์— ๋”ํ•ด, ๊ฐ™์€ ๋ฐฉ์— ์ƒˆ ํ„ด์ด ๋“ค์–ด์˜ค๋ฉด ์ด์ „ ํ„ด์„ ๋จผ์ € ๋Š์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ๊ฐ€ ๋กœ๋”ฉ ์ค‘ ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์„œ๋ฒ„์—๋Š” ์ทจ์†Œ ์‹ ํ˜ธ๋ฅผ ์ฃผ์ง€ ์•Š์•„, ์ด๊ฒŒ ์—†์œผ๋ฉด ๊ฐ™์€ conversation_id ์— ๋‘ ์‹คํ–‰์ด ๊ฒน์นฉ๋‹ˆ๋‹ค.

์žฌ์ ‘์† ์š”์ฒญ

curl -N -X POST http://127.0.0.1:8010/chat/stream/attach \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer <supabase-jwt>' \
  -d '{"conversation_id":"conv-1","tenant_id":"t1"}'

ํ™œ์„ฑ ํ„ด์ด ์žˆ์œผ๋ฉด text/event-stream ์œผ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.

event: message
data: {"type": "snapshot", "content": "์ง€๊ธˆ๊นŒ์ง€ ์“ด ๋ณธ๋ฌธ"}

event: message
data: {"type": "token", "content": "์ด์–ด์„œ"}

ํ™œ์„ฑ ํ„ด์ด ์—†์œผ๋ฉด SSE ๊ฐ€ ์•„๋‹ˆ๋ผ 200 {"active": false} ์ž…๋‹ˆ๋‹ค. 404 ๊ฐ€ ์•„๋‹Œ ์ด์œ ๋Š”, ์ฒซ attach ์‹œ๋„๋Š” ํ•ญ์ƒ "์•„์ง ์ŠคํŠธ๋ฆผ ์—†์Œ" ์ด๋ผ 404 ๋กœ ๋‘๋ฉด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ๋ธŒ๋ผ์šฐ์ € ๋„คํŠธ์›Œํฌ ํƒญ์— ์‹คํŒจ ์š”์ฒญ์ด ์Œ“์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ๋Š” content-type ์ด text/event-stream ์ด ์•„๋‹ˆ๋ฉด ์กฐ์šฉํžˆ ์ข…๋ฃŒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ค‘์ง€ ์š”์ฒญ

curl -X POST http://127.0.0.1:8010/chat/stop \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer <supabase-jwt>' \
  -d '{"conversation_id":"conv-1","tenant_id":"t1"}'
์‘๋‹ต ์˜๋ฏธ
{"stopped": true} ์ง„ํ–‰ ์ค‘์ด๋˜ ํ„ด์„ ์ทจ์†Œํ–ˆ๋‹ค
{"stopped": false, "reason": "no_active_turn"} ์ทจ์†Œํ•  ์‹คํ–‰์ด ์—†๋‹ค(์ด๋ฏธ ๋๋‚ฌ๊ฑฐ๋‚˜ HITL ๋Œ€๊ธฐ ์ค‘)
403 {"stopped": false, "reason": "forbidden"} ์š”์ฒญ์ž์˜ ํ…Œ๋„ŒํŠธ์™€ ๋ฐฉ ์†Œ์œ  ํ…Œ๋„ŒํŠธ๊ฐ€ ๋‹ค๋ฅด๋‹ค

์žฌ์ ‘์†ยท์ค‘์ง€ ๋ชจ๋‘ ๋ฐฉ ์†Œ์œ  ํ…Œ๋„ŒํŠธ(chat_rooms.tenant_id)์™€ ์š”์ฒญ์ž์˜ ํ…Œ๋„ŒํŠธ๊ฐ€ ๊ฐ™์„ ๋•Œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ์„ ์กฐํšŒํ•˜์ง€ ๋ชปํ•˜๋ฉด ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค(fail-closed).

ํ•˜ํŠธ๋น„ํŠธ ์ฃผ๊ธฐ๋Š” SSE_HEARTBEAT_SECONDS ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค(๊ธฐ๋ณธ 15์ดˆ).

4.6 ์ˆ˜์ • ์ง€์‹œ (์ง„ํ–‰ ์ค‘์ธ ํ„ด์˜ ๋ฐฉํ–ฅ ๋ฐ”๊พธ๊ธฐ)

์ž‘์—…์ด ๋๋‚˜๊ธฐ ์ „์— ๋ฐฉํ–ฅ์ด ์–ด๊ธ‹๋‚œ ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์„ ๋•Œ, ์‹คํ–‰์„ ์ทจ์†Œํ•˜๊ณ  ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹œํ‚ค๋Š” ๋Œ€์‹  ์ง€๊ธˆ๊นŒ์ง€์˜ ๋งฅ๋ฝ์„ ์œ ์ง€ํ•œ ์ฑ„ ์ง€์‹œ๋งŒ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๊ธด ์ž‘์—…์„ ์ž์œจ์ ์œผ๋กœ ๋Œ๋ ค ๋†“๊ณ  ํ•„์š”ํ•  ๋•Œ๋งŒ ๊ฐœ์ž…ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฑด ํŠน์ • ์—์ด์ „ํŠธ์˜ ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋ผ ํ‘œ์ค€ ๋™์ž‘์ž…๋‹ˆ๋‹ค. SDK ๊ฐ€ ์š”์ฒญ ํ˜•ํƒœ์™€ ์ด๋ฒคํŠธ๊นŒ์ง€๋ฅผ ์†Œ์œ ํ•˜๊ณ , ์‹ค์ œ ์‹คํ–‰ ์ „ํ™˜์€ ์—์ด์ „ํŠธ๋ณ„ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋งก์Šต๋‹ˆ๋‹ค โ€” ์ง€์‹œ๋ฅผ ์–ธ์ œ ์ง‘์–ด๋„ฃ์–ด์•ผ ์•ˆ์ „ํ•œ์ง€๋Š” ๋Ÿฐํƒ€์ž„๋งˆ๋‹ค ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

curl -X POST http://127.0.0.1:8010/chat/steer \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer <supabase-jwt>' \
  -d '{"conversation_id":"conv-1","tenant_id":"t1","message":"ํ‘œ ๋Œ€์‹  ๊ธ€๋กœ ์จ ์ค˜"}'

/chat/stream ์œผ๋กœ {"action":"steer", ...} ๋ฅผ ๋ณด๋‚ด๋„ ๊ฐ™์Šต๋‹ˆ๋‹ค(์—”๋“œํฌ์ธํŠธ๋ฅผ ํ•˜๋‚˜๋งŒ ์•„๋Š” ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•ด). action ์ด ์—†๋Š” ๊ธฐ์กด ์š”์ฒญ์€ ์ข…์ „๋Œ€๋กœ ์ƒˆ ํ„ด์„ ๋Œ๋ฆฝ๋‹ˆ๋‹ค. ๋ชจ๋ฅด๋Š” action ์€ 400 ์ž…๋‹ˆ๋‹ค โ€” ์˜คํƒ€(steeer)๋ฅผ ํ‰๋ฒ”ํ•œ ๋ฉ”์‹œ์ง€๋กœ ํ˜๋ฆฌ๋ฉด ๋ฐฉํ–ฅ์„ ๋ฐ”๊พธ๋ ค๋˜ ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ธ ํ„ด์„ ๋Œ€์ฒดํ•ด ์ž‘์—…์„ ๋‚ ๋ฆฝ๋‹ˆ๋‹ค.

์‘๋‹ต ์˜๋ฏธ
{"accepted": true, "directive_id": "โ€ฆ"} ๋ฐ›์•˜๋‹ค. ๋ฐ˜์˜์€ ์•„๋‹ˆ๋‹ค(์•„๋ž˜ ์ฐธ๊ณ )
{"accepted": true, "duplicate": true, "directive_id": "โ€ฆ"} ๊ฐ™์€ ๋ฌธ์žฅ์„ ์—ฐ์†์œผ๋กœ ๋ฐ›์•˜๋‹ค. ์ฒ˜์Œ ์ ‘์ˆ˜์˜ id ๋ฅผ ๊ทธ๋Œ€๋กœ ์ค€๋‹ค
409 {"accepted": false, "reason": "no_active_turn"} ๋Œ๊ณ  ์žˆ๋Š” ํ„ด์ด ์—†๋‹ค(์ด๋ฏธ ๋๋‚ฌ๊ฑฐ๋‚˜ ๋งˆ๋ฌด๋ฆฌ์— ๋“ค์–ด๊ฐ”๋‹ค)
409 {"accepted": false, "reason": "awaiting_human_input"} ์‚ฌ๋žŒ์˜ ๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ๋ฉˆ์ถฐ ์žˆ๋‹ค. ๊ทธ ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ๊ฒƒ์ด ๋ฐฉํ–ฅ ์ „ํ™˜์ด๋‹ค
501 {"accepted": false, "reason": "unsupported"} ์ด Executor ๊ฐ€ steer() ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๋‹ค
400 {"accepted": false, "reason": "empty_message"} ๋ณด๋‚ผ ์ง€์‹œ๊ฐ€ ์—†๋‹ค
403 {"accepted": false, "reason": "forbidden"} ์š”์ฒญ์ž์˜ ํ…Œ๋„ŒํŠธ์™€ ๋ฐฉ ์†Œ์œ  ํ…Œ๋„ŒํŠธ๊ฐ€ ๋‹ค๋ฅด๋‹ค

์ ‘์ˆ˜์™€ ๋ฐ˜์˜์€ ๋‹ค๋ฅธ ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค. ์ ‘์ˆ˜ ์‹œ์ ์˜ ์—์ด์ „ํŠธ๋Š” ์•„์ง ์›๋ž˜ ์ง€์‹œ๋Œ€๋กœ ๋„๊ตฌ๋ฅผ ๋Œ๋ฆฌ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘˜์„ ํ•œ ์ด๋ฒคํŠธ๋กœ ํ•ฉ์น˜๋ฉด ํ™”๋ฉด์€ ์ ‘์ˆ˜๋งŒ์œผ๋กœ "๋ฐ˜์˜ ์™„๋ฃŒ" ๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ์‚ฌ์šฉ์ž๋Š” ๋ฐ˜์˜๋˜์ง€ ์•Š์€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜์˜๋œ ๊ฒƒ์œผ๋กœ ์ฝ์Šต๋‹ˆ๋‹ค.

event: message
data: {"type": "steer_accepted", "directive_id": "โ€ฆ", "content": "ํ‘œ ๋Œ€์‹  ๊ธ€๋กœ ์จ ์ค˜"}

event: message
data: {"type": "steer_applied", "directive_id": "โ€ฆ", "content": "ํ‘œ ๋Œ€์‹  ๊ธ€๋กœ ์จ ์ค˜"}

๋‘ ์ด๋ฒคํŠธ๋Š” ํ„ด์˜ ์ถœ๋ ฅ ํ๋กœ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค โ€” ์›๋ž˜ ํด๋ผ์ด์–ธํŠธ์™€ ์žฌ์ ‘์†ํ•œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฐ™์€ ๊ฒƒ์„ ๋ด…๋‹ˆ๋‹ค. ์ ‘์ˆ˜๋งŒ ๋˜๊ณ  ์•„์ง ๋ฐ˜์˜๋˜์ง€ ์•Š์€ ์ง€์‹œ๋Š” ์žฌ์ ‘์† ์Šค๋ƒ…์ƒท์— pending_steers ๋กœ๋„ ์‹ค๋ฆฝ๋‹ˆ๋‹ค.

์ƒํ™ฉ ์ฒ˜๋ฆฌ
๋„๊ตฌ ์‹คํ–‰ ์ค‘ ์ ‘์ˆ˜๋งŒ ํ•˜๊ณ  ๋Œ€๊ธฐ์—ด์— ๋„ฃ๋Š”๋‹ค. ๋„๊ตฌ๋ฅผ ์ค‘๊ฐ„์— ๋Š์ง€ ์•Š๋Š”๋‹ค โ€” ์“ฐ๋‹ค ๋งŒ ํŒŒ์ผ์ด๋‚˜ ๊ฒฐ๊ณผ ์—†๋Š” ๋„๊ตฌ ํ˜ธ์ถœ์„ ๋‚จ๊ธฐ๋Š” ํŽธ์ด ๋” ๋‚˜์˜๋‹ค. ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋‹ค์Œ ์•ˆ์ „ ์ง€์ ์—์„œ ์ง‘์–ด ๊ฐ„๋‹ค
์™„๋ฃŒ ์ง์ „ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋งˆ๋ฌด๋ฆฌ ์ „์— ๋Œ€๊ธฐ์—ด์„ ๋‹ซ๊ณ  ๋‚จ์€ ์ง€์‹œ๋ฅผ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ง‘์–ด ๊ฐ„๋‹ค. ๋‹ซํžŒ ๋’ค์˜ ์ง€์‹œ๋Š” no_active_turn ์œผ๋กœ ๊ฑฐ์ ˆํ•œ๋‹ค โ€” ๋ฐ›์•„ ๋‘๊ณ  ์•„๋ฌด ๋ฐ๋„ ๋ฐ˜์˜ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ๋ณด๋‹ค ๊ฑฐ์ ˆ์ด ์ •์งํ•˜๋‹ค
์ค‘๋ณต ์—ฐ์† ์ˆ˜์‹  ๋‘ ๋ฒˆ์งธ๋ถ€ํ„ฐ๋Š” ์Œ“์ง€ ์•Š๊ณ  duplicate: true ์™€ ์ฒ˜์Œ ์ ‘์ˆ˜์˜ id ๋ฅผ ์ค€๋‹ค. ์ ‘์ˆ˜ ์ด๋ฒคํŠธ๋„ ๋‹ค์‹œ ๋‚ด๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค
์‚ฌ๋žŒ ํ™•์ธ ๋Œ€๊ธฐ ์ค‘ ๋Œ๊ณ  ์žˆ๋Š” ์‹คํ–‰์ด ์—†์–ด ๋„ฃ์„ ๊ณณ์ด ์—†๋‹ค. awaiting_human_input ์œผ๋กœ ๊ฑฐ์ ˆํ•œ๋‹ค
์žฌ์ ‘์† ์ ‘์ˆ˜ยท๋ฐ˜์˜์ด ๋‹ค๋ฅธ ์ด๋ฒคํŠธ์™€ ๊ฐ™์€ ํ๋กœ ๋‚˜๊ฐ€๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š”๋‹ค + ์Šค๋ƒ…์ƒท์˜ pending_steers

์–ด๋Œ‘ํ„ฐ ์ชฝ(์—์ด์ „ํŠธ ์ €์žฅ์†Œ) ์ด ๊ตฌํ˜„ํ•  ๊ฒƒ์€ ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค โ€” ์ง€๊ธˆ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ์ •ํ•˜๋Š” ๊ฒƒ๊ณผ, ์•ˆ์ „ ์ง€์ ์—์„œ ์‹ค์ œ๋กœ ์–น๋Š” ๊ฒƒ.

from processgpt_agent_sdk import SteerDirective, SteerResult, get_steering_inbox, mark_applied

class MyExecutor(AgentExecutor):
    async def steer(self, directive: SteerDirective) -> SteerResult | None:
        """์ง€๊ธˆ ๋ฐฉํ–ฅ์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋Š”๊ฐ€. None ์ด๋ฉด SDK ์˜ ํ‘œ์ค€ ํŒ์ •์„ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค."""
        if await self._parked_on_question(directive.conversation_id):
            return SteerResult.reject("awaiting_human_input")
        return None

๊ทธ๋ฆฌ๊ณ  ์‹คํ–‰ ์ชฝ์—์„œ๋Š”, ๋‹ค์Œ ํŒ๋‹จ์ด ์‹œ์ž‘๋˜๊ธฐ ์ง์ „(๋„๊ตฌ๊ฐ€ ๋๋‚˜๊ณ  ๋ชจ๋ธ์„ ๋ถ€๋ฅด๊ธฐ ์ „)๋งˆ๋‹ค:

taken = await get_steering_inbox().take(cid)     # ์ง‘์–ด ๊ฐ€๊ธฐ โ‰  ๋ฐ˜์˜
for directive in taken:
    await mark_applied(directive)                # ์‹ค์ œ๋กœ ๋‹ค์Œ ํŒ๋‹จ์— ๋„ฃ๋Š” ์ˆœ๊ฐ„
    ...                                          # directive.message ๋ฅผ ์ž…๋ ฅ์— ์–น๋Š”๋‹ค

# ๋” ์ด์ƒ ์–น์„ ์ง€์ ์ด ์—†๋‹ค๋ฉด(ํ„ด์ด ๋๋‚˜๋ ค ํ•œ๋‹ค๋ฉด) ๋Œ€๊ธฐ์—ด์„ ๋‹ซ๋Š”๋‹ค.
# ๋‹ซ์€ ๋’ค์—๋„ ์‹คํ–‰์ด ์ด์–ด์ง€๊ฒŒ ๋๋‹ค๋ฉด reopen(cid) ์œผ๋กœ ๋‹ค์‹œ ์—ฐ๋‹ค.
await get_steering_inbox().close(cid)

๊ทธ "์•ˆ์ „ ์ง€์ " ์ด ์–ด๋””์ธ์ง€๋Š” ๋Ÿฐํƒ€์ž„์ด ์ •ํ•ฉ๋‹ˆ๋‹ค. deepagents ๋Š” LangChain ๋ฏธ๋“ค์›จ์–ด์˜ ๋ชจ๋ธ ํ˜ธ์ถœ ์ง์ „ ํ›…(abefore_model)์—์„œ ์ง‘์–ด ๊ฐ€๊ณ , ํ„ด์„ ๋๋‚ด๋ ค๋Š” ์‹œ์  (aafter_model)์— ํ•œ ๋ฒˆ ๋” ํ™•์ธํ•ด ๋‚จ์€ ์ง€์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ชจ๋ธ๋กœ ๋˜๋Œ๋ฆฝ๋‹ˆ๋‹ค.

steer() ๊ฐ€ ์—†์œผ๋ฉด ๊ทธ ์—์ด์ „ํŠธ๋Š” ๋ฏธ์ง€์›(501)์ž…๋‹ˆ๋‹ค. ํ‘œ์ค€ ๋™์ž‘์„ ์ •์˜ํ•˜๋Š” ๊ฒƒ๊ณผ ๋ชจ๋“  ์—์ด์ „ํŠธ๊ฐ€ ๊ทธ๊ฒƒ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ฃผ์žฅํ•˜๋Š” ๊ฒƒ์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

4.7 ํ…Œ๋„ŒํŠธ ์ธ์ฆ

์ฑ„ํŒ… ์š”์ฒญ์˜ tenant_id ๋Š” Executor ๋ฅผ ์ง€๋‚˜ ์Šคํ‚ฌ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋กœ๋“œยท์ƒŒ๋“œ๋ฐ•์Šค ๋งˆ์šดํŠธยท์ž‘์—…๊ณต๊ฐ„ ๊ฒฝ๋กœ๊นŒ์ง€ ๊ทธ๋Œ€๋กœ ํ˜๋Ÿฌ๊ฐ‘๋‹ˆ๋‹ค. ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ’๋งŒ ๋ฐ”๊ฟ” ํ˜ธ์ถœํ•ด ๋‚จ์˜ ํ…Œ๋„ŒํŠธ ์ž์›์— ๋‹ฟ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ tenant_auth ์˜ ๊ธฐ๋ณธ๊ฐ’์€ ์ผœ์ง์ž…๋‹ˆ๋‹ค.

server = ProcessGPTAgentServer(
    agent_executor=MyExecutor(),
    agent_type="crewai-action",
    tenant_auth=True,   # ๊ธฐ๋ณธ๊ฐ’
)
server.mount_chat_routes(app)

๊ฒ€์ฆ ๊ฒฝ๋กœ๋Š” ํ† ํฐ ํ—ค๋”์˜ alg ์— ๋”ฐ๋ผ ๊ฐˆ๋ฆฝ๋‹ˆ๋‹ค.

  1. ๋น„๋Œ€์นญ(ES256/RS256 โ€ฆ) โ€” Supabase JWKS(/auth/v1/.well-known/jwks.json) ๋กœ์ปฌ ๊ฒ€์ฆ. ์ตœ์‹  Supabase ํ”„๋กœ์ ํŠธ(JWT signing keys)๊ฐ€ ์—ฌ๊ธฐ ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  2. HS256 + SUPABASE_JWT_SECRET โ€” ๋ ˆ๊ฑฐ์‹œ ๋Œ€์นญํ‚ค ํ”„๋กœ์ ํŠธยท์ปค์Šคํ…€ SSO ํ† ํฐ ๋กœ์ปฌ ๊ฒ€์ฆ.
  3. ๋‘˜ ๋‹ค ๋ถˆ๊ฐ€ํ•˜๋ฉด GoTrue /auth/v1/user ์— ์œ„์ž„.

์†Œ์† ํ…Œ๋„ŒํŠธ๋Š” JWT ํด๋ ˆ์ž„(tenant_id / app_metadata.tenant_id / tenant_ids)์„ ๋จผ์ € ๋ณด๊ณ , ์—†์œผ๋ฉด users ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค(๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์†Œ์† ๋Œ€์‘). ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋Š” ํ† ํฐ ๋‹จ์œ„๋กœ 60์ดˆ ์บ์‹œํ•˜๊ณ , ํ† ํฐ ๋งŒ๋ฃŒ๊ฐ€ ๋” ์ด๋ฅด๋ฉด ๊ทธ ์‹œ์ ๊นŒ์ง€๋งŒ ์บ์‹œํ•ฉ๋‹ˆ๋‹ค.

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์“ฐ์ž„
SUPABASE_URL JWKS ์ฃผ์†Œ๋ฅผ ๋งŒ๋“ ๋‹ค(๋น„๋Œ€์นญ ๊ฒ€์ฆ)
SUPABASE_JWT_SECRET HS256 ๋Œ€์นญํ‚ค ๊ฒ€์ฆ(์žˆ์„ ๋•Œ๋งŒ)

ํŒ์ • ๊ทœ์น™

์ƒํ™ฉ ๊ฒฐ๊ณผ
ํ† ํฐ ์—†์Œ 401
์š”์ฒญํ•œ tenant_id ๊ฐ€ ์†Œ์†์ด ์•„๋‹˜ 403
์š”์ฒญ์— tenant_id ์—†๊ณ  ์†Œ์†์ด ํ•˜๋‚˜ ๊ทธ ๊ฐ’์œผ๋กœ ์ฑ„์›Œ ํ†ต๊ณผ
์š”์ฒญ์— tenant_id ์—†๊ณ  ์†Œ์†์ด ์—ฌ๋Ÿฟ 400 (tenant_id is required)
์†Œ์† ํ…Œ๋„ŒํŠธ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์Œ 403

์ง์ ‘ ๋งŒ๋“  ๋ผ์šฐํŠธ์—๋„ ๊ฐ™์€ ๊ทœ์น™์„ ๊ฑธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from processgpt_agent_sdk import tenant_guard, request_tenant_id

@tenant_guard
async def my_handler(request):
    # ์š”์ฒญ์ด ๋ณด๋‚ธ ๊ฐ’์ด ์•„๋‹ˆ๋ผ ๊ฒ€์ฆ๋œ ๊ฐ’๋งŒ ์“ด๋‹ค
    tenant_id = request_tenant_id(request)
    ...

ํ…Œ๋„ŒํŠธ ์Šค์ฝ”ํ”„๊ฐ€ ์—†๋Š” ์—”๋“œํฌ์ธํŠธ๋Š” auth_guard ๋กœ ์ธ์ฆ๋งŒ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

๋„๋Š” ๊ฒฝ์šฐ: ์•ž๋‹จ์— ๋ณ„๋„ ์ธ์ฆ ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ๋กœ์ปฌ ๊ฐœ๋ฐœ์ผ ๋•Œ๋งŒ tenant_auth=False ๋กœ ๋‘ก๋‹ˆ๋‹ค. ์ด๋•Œ๋Š” ๊ธฐ๋™ ๋กœ๊ทธ์— ๊ฒฝ๊ณ ๊ฐ€ ๋‚จ๊ณ , ์š”์ฒญ ๋ณธ๋ฌธ์˜ tenant_id ๊ฐ€ ๊ทธ๋Œ€๋กœ Executor ์— ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.


5. โš ๏ธ JSON ์ง๋ ฌํ™” ์ฃผ์˜ (str() ์ ˆ๋Œ€ ๊ธˆ์ง€)

๋ฐ˜๋“œ์‹œ json.dumps()๋กœ ์ง๋ ฌํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • โŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ˆ๋จ:

    text = str({"key": "value"})  # Python dict string โ†’ JSON ์•„๋‹˜

    DB์— "'{key: value}'" ๊ผด๋กœ ๋ฌธ์ž์—ด ์ €์žฅ๋จ โ†’ ํŒŒ์‹ฑ ์‹คํŒจ

  • โœ… ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ•จ:

    text = json.dumps({"key": "value"}, ensure_ascii=False)

    DB์— {"key": "value"} JSON ์ €์žฅ๋จ โ†’ ํŒŒ์‹ฑ ์„ฑ๊ณต

๐Ÿ‘‰ SDK๋Š” ๋‚ด๋ถ€์—์„œ json.loads๋กœ ์žฌํŒŒ์‹ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํ‘œ์ค€ JSON ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ฉด ๋ฌด์กฐ๊ฑด ๋ฌธ์ž์—ด๋กœ๋งŒ ๋‚จ์Šต๋‹ˆ๋‹ค.


6. ์‚ฌ์šฉ๋ฒ• (๋‚ด ์ฝ”๋“œ์— ๋ถ™์ด๊ธฐ)

ํ•ต์‹ฌ์€ Executor ์•ˆ์—์„œ ๋ชจ๋“œ๋ฅผ ๋ถ„๊ธฐํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋™์ผํ•œ A2A ์ด๋ฒคํŠธ ์‹œํ€€์Šค๋ฅผ emitํ•˜๋ฉด, ํ”„๋ ˆ์ž„์›Œํฌ์˜ EventQueue ๊ตฌํ˜„์ฒด๊ฐ€ ํ”„๋กœ์„ธ์Šค/์ฑ„ํŒ…์— ๋งž๊ฒŒ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.

  • ๊ณตํ†ต ์ด๋ฒคํŠธ ํ๋ฆ„: Task โ†’ TaskStatusUpdateEvent[..] โ†’ TaskArtifactUpdateEvent(last_chunk=True)
  • ํ”„๋กœ์„ธ์Šค(ํด๋ง) ๊ฒฝ๋กœ: ProcessEventQueue๊ฐ€ status๋Š” events ํ…Œ์ด๋ธ”, artifact๋Š” todolist ํ…Œ์ด๋ธ”์— ์ €์žฅ
  • ์ฑ„ํŒ…(SSE) ๊ฒฝ๋กœ: ChatEventQueue๊ฐ€ status๋Š” SSE message ์ฒญํฌ๋กœ, ์ตœ์ข… artifact๋Š” SSE done + chats ํ…Œ์ด๋ธ” ์ €์žฅ์œผ๋กœ ๋ณ€ํ™˜. ์ตœ์ข… ์ด๋ฒคํŠธ์— pdfFiles ๋ฅผ ์‹ค์œผ๋ฉด ์‚ฐ์ถœ๋ฌผ๋„ ๊ฐ™์ด ๋”ฐ๋ผ๊ฐ‘๋‹ˆ๋‹ค(7.5)
  • ์ „์†ก ๊ณ„์ธต: ํ•˜ํŠธ๋น„ํŠธยท์žฌ์ ‘์†ยท์ค‘์ง€ยทํ…Œ๋„ŒํŠธ ๊ฒ€์ฆ์€ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Executor ๋Š” ์ด๋“ค์„ ์•Œ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค (4.5 ยท 4.6)

6.1 ํ”„๋กœ์„ธ์Šค(ํด๋ง)๋งŒ ์‹คํ–‰

from processgpt_agent_sdk import ProcessGPTAgentServer

server = ProcessGPTAgentServer(agent_executor=MyExecutor(), agent_type="crewai-action")
await server.run()

6.2 ์ฑ„ํŒ…(SSE) ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€

SSE๋ฅผ ์“ฐ๋ ค๋ฉด extras ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

pip install "process-gpt-agent-sdk[sse,auth]"
from starlette.applications import Starlette

from processgpt_agent_sdk import ProcessGPTAgentServer

server = ProcessGPTAgentServer(agent_executor=MyExecutor(), agent_type="crewai-action")
app = Starlette()
server.mount_chat_routes(app)
# POST /chat/stream ยท /chat/stream/attach ยท /chat/stop ยท /chat/steer

๊ฒฝ๋กœ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ์ผ๋ถ€๋งŒ ๋ถ™์ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์ „ ํ”„๋ก ํŠธ๊ฐ€ ์“ฐ๋˜ ๋ณ„์นญ ๊ฒฝ๋กœ๊ฐ€ ์žˆ์œผ๋ฉด stream_paths ์— ํ•จ๊ป˜ ๋„˜๊น๋‹ˆ๋‹ค.

server.mount_chat_routes(
    app,
    stream_paths=("/chat/stream", "/{agent_id}/chat/stream"),
    attach_path="/chat/stream/attach",   # None ์ด๋ฉด ๋ถ™์ด์ง€ ์•Š๋Š”๋‹ค
    stop_path="/chat/stop",              # None ์ด๋ฉด ๋ถ™์ด์ง€ ์•Š๋Š”๋‹ค
    steer_path="/chat/steer",            # None ์ด๋ฉด ๋ถ™์ด์ง€ ์•Š๋Š”๋‹ค
)

FastAPI ์•ฑ์—๋„ ๊ทธ๋Œ€๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ผ์šฐํŠธ๋Š” ํ•ญ์ƒ Starlette ๋ผ์šฐํŠธ๋กœ ๋“ฑ๋ก๋˜๋Š”๋ฐ, FastAPI ์˜ add_api_route() ์— ๋„˜๊ธฐ๋ฉด ํ•ธ๋“ค๋Ÿฌ ์‹œ๊ทธ๋‹ˆ์ฒ˜(ํƒ€์ž… ์ฃผ์„ ์—†๋Š” request)๋ฅผ ํ•„์ˆ˜ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•ด์„ํ•ด ๋ชจ๋“  ์š”์ฒญ์ด 422 ๋กœ ๋–จ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์š”์ฒญ ๋ฐ”๋”” ์˜ˆ์‹œ:

{
  "message": "์•ˆ๋…•",
  "tenant_id": "t1",
  "user_uid": "u1",
  "user_email": "user@example.com",
  "user_name": "ํ™๊ธธ๋™",
  "user_jwt": "",
  "conversation_id": "conv-1",
  "file": null,
  "files": [],
  "file_count": 0,
  "stream": true,
  "metadata": {}
}

6.3 ํด๋ง + SSE๋ฅผ ํ•œ ํ”„๋กœ์„ธ์Šค์—์„œ ํ•จ๊ป˜ ์‹คํ–‰ (๊ถŒ์žฅ ์˜ˆ์‹œ)

import asyncio

import uvicorn
from starlette.applications import Starlette

from processgpt_agent_sdk import ProcessGPTAgentServer


async def main():
    server = ProcessGPTAgentServer(agent_executor=MyExecutor(), agent_type="crewai-action")

    app = Starlette()
    server.mount_chat_routes(app)

    uvicorn_server = uvicorn.Server(
        uvicorn.Config(app, host="127.0.0.1", port=8010, log_level="info")
    )

    await asyncio.gather(
        server.run(),
        uvicorn_server.serve(),
    )


if __name__ == "__main__":
    asyncio.run(main())

์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํด๋ง ํ”„๋กœ์„ธ์Šค์™€ HTTP API ํ”„๋กœ์„ธ์Šค๋ฅผ ๋ถ„๋ฆฌ ์šด์˜ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ์Šต๋‹ˆ๋‹ค.

6.4 ๋‹ค์ค‘ ํŒŒ๋“œ ๋ฐฐํฌ (๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ต์ฒด)

์žฌ์ ‘์†ยท์ค‘์ง€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์˜ ๊ธฐ๋ณธ ๊ตฌํ˜„์€ ํ”„๋กœ์„ธ์Šค ๋กœ์ปฌ dict ์ž…๋‹ˆ๋‹ค(๋‹จ์ผ uvicorn ์›Œ์ปค ์ „์ œ). ํŒŒ๋“œ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋„์šฐ๋ฉด "A ํŒŒ๋“œ๊ฐ€ ๋Œ๋ฆฌ๋Š” ํ„ด์— B ํŒŒ๋“œ๊ฐ€ ๋ฐ›์€ attach ์š”์ฒญ" ์ด ๋งž์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ณต์œ  ๋ฐฑ์—”๋“œ ๊ตฌํ˜„์œผ๋กœ ๊ฐˆ์•„๋ผ์›๋‹ˆ๋‹ค. SDK ๋Š” ๊ณ„์•ฝ๋งŒ ์†Œ์œ ํ•˜๊ณ  ๊ณต์œ  ์ƒํƒœ ๋ฐฑ์—”๋“œ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๊ณ ๋ฆ…๋‹ˆ๋‹ค.

from processgpt_agent_sdk import (
    ChatRunRegistry,
    set_run_registry,
    set_inflight_registry,
)

class RedisRunRegistry(ChatRunRegistry):
    async def start_run(self, conversation_id): ...
    async def record(self, conversation_id, payload): ...
    async def mark_done(self, conversation_id): ...
    async def subscribe(self, conversation_id): ...
    async def unsubscribe(self, conversation_id, q): ...

set_run_registry(RedisRunRegistry())

asyncio.Task.cancel() ์€ ๊ทธ task ๋ฅผ ๋งŒ๋“  ํ”„๋กœ์„ธ์Šค ์•ˆ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, ์ค‘์ง€์˜ ๊ฒฝ์šฐ ๊ณต์œ  ๋ฐฑ์—”๋“œ๋Š” "์ด ๋ฐฉ์„ ์–ด๋А ํŒŒ๋“œ๊ฐ€ ๋“ค๊ณ  ์žˆ๋Š”์ง€" ์†Œ์œ ๊ถŒ ๊ธฐ๋ก๊ณผ ์ทจ์†Œ ์‹ ํ˜ธ ์ „๋‹ฌ์—๋งŒ ์“ฐ๊ณ  ์ทจ์†Œ ์ž์ฒด๋Š” ํ•ญ์ƒ ์†Œ์œ  ํŒŒ๋“œ์—์„œ ์ผ์–ด๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

supersede ์™€ cancel ์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. InflightRegistry ๋Š” ๋‘ ๋ฉ”์„œ๋“œ๋ฅผ ๋”ฐ๋กœ ๋‘ก๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ ์–ธ์ œ ๋ถˆ๋ฆฌ๋‚˜ ๊ธฐ๋ณธ ๋™์ž‘
supersede(cid) ๊ฐ™์€ ๋ฐฉ์— ์ƒˆ ํ„ด์ด ์‹œ์ž‘๋  ๋•Œ cancel() ์„ ๋ถ€๋ฅธ๋‹ค
cancel(cid) /chat/stop ์œผ๋กœ ๋ช…์‹œ์  ์ค‘์ง€ํ•  ๋•Œ ์‹คํ–‰ task ๋ฅผ ์ทจ์†Œํ•œ๋‹ค

์ƒˆ ์š”์ฒญ์ด ์ด์ „ ์‘๋‹ต์„ ๋Œ€์ฒดํ•˜๋Š”์ง€ ์ด์–ด๊ฐ€๋Š”์ง€๋Š” ์„œ๋ฒ„๋งˆ๋‹ค ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋Œ€ํ‘œ์ ์œผ๋กœ HITL(์‚ฌ๋žŒ ํ™•์ธ) ์‘๋‹ต์€ ์ด์ „ ํ„ด์ด ๋‚จ๊ธด interrupt ๋ฅผ ์žฌ๊ฐœํ•˜๋Š” ๊ฒƒ์ด๋ผ, ๊ทธ ํ„ด์„ ์ทจ์†Œํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ๊ฐ€ ์‚ฌ๋ผ์ ธ ์žฌ๊ฐœ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค. ์–ด๋А ์ชฝ์ธ์ง€๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์„ ํ•ด์„ํ•ด์•ผ ์•Œ ์ˆ˜ ์žˆ๊ณ  ๊ทธ๊ฑด Executor ์˜ ๋ชซ์ด๋ฏ€๋กœ, ๊ทธ๋Ÿฐ ์„œ๋ฒ„๋Š” supersede() ๋ฅผ no-op ์œผ๋กœ ์žฌ์ •์˜ํ•˜๊ณ  Executor ์•ˆ์—์„œ ์ง์ ‘ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

from processgpt_agent_sdk import InflightRegistry, set_inflight_registry

class ExecutorDecides(InflightRegistry):
    async def supersede(self, conversation_id):
        return False   # ๋Œ€์ฒด/์ด์–ด๊ฐ€๊ธฐ ํŒ๋‹จ์€ Executor ๊ฐ€ ํ•œ๋‹ค

set_inflight_registry(ExecutorDecides())

7. ์—์ด์ „ํŠธ ์‚ฐ์ถœ๋ฌผ (artifacts) โ€” 0.8.0 ์ถ”๊ฐ€

์—์ด์ „ํŠธ๊ฐ€ ๋งŒ๋“  ํŒŒ์ผ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋‚ด์ฃผ๋Š” ์ผ์„ SDK ๊ฐ€ ๊ณต์šฉ์œผ๋กœ ๊ฐ–์Šต๋‹ˆ๋‹ค. ์—์ด์ „ํŠธ๋งˆ๋‹ค ๋‹ค์‹œ ๋งŒ๋“ค ๊ฒƒ์ด ์—†์Šต๋‹ˆ๋‹ค.

7.1 ๋ฌด์—‡์ด ๋ฌธ์ œ์˜€๋‚˜

์ด ๋ชจ๋“ˆ์ด ์ƒ๊ธฐ๊ธฐ ์ „์—๋Š” ์—์ด์ „ํŠธ๋งˆ๋‹ค ์ž๊ธฐ ๋ฐฉ์‹์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•œ์ชฝ์€ ์ž์ฒด store ์™€ collector ๋ฅผ, ๋‹ค๋ฅธ ์ชฝ์€ ํ•œ ์‹œ๊ฐ„์งœ๋ฆฌ ํ† ํฐ ์ฃผ์†Œ๋ฅผ ๋“ค๊ณ  ์žˆ์—ˆ๊ณ , ๋‘˜ ๋‹ค ์ฃผ์†Œ๊ฐ€ ์ฃฝ์œผ๋ฉด ํŒŒ์ผ์„ ์˜์˜ ๋ฐ›์„ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๊ณต๊ฐœ ๋ฒ„ํ‚ท์˜ ์˜๊ตฌ ์ฃผ์†Œ๋ฅผ ์“ฐ๋˜ ์ชฝ์€ ๋ฐ˜๋Œ€ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค โ€” ๊ณ„์•ฝ์„œ ๊ฒ€ํ†  ๊ฒฐ๊ณผ๋‚˜ ์‚ฌ๋‚ด ๋ณด๊ณ ์„œ๊ฐ€ ์ฃผ์†Œ๋งŒ ์•Œ๋ฉด ๋ˆ„๊ตฌ๋‚˜, ์–ธ์ œ๊นŒ์ง€๋‚˜ ์—ด๋ฆฌ๋Š” ์ž๋ฆฌ์— ๋†“์˜€์Šต๋‹ˆ๋‹ค.

7.2 ๊ทœ์•ฝ์€ ๋””๋ ‰ํ„ฐ๋ฆฌ๋‹ค

์ด๋ฒˆ ํ„ด์˜ outputs/ ์— ๋†“์ธ ๊ฒƒ๋งŒ ์‚ฐ์ถœ๋ฌผ์ž…๋‹ˆ๋‹ค.

๋‹ต๋ณ€ ๋ณธ๋ฌธ์„ ํ›‘์–ด ๊ฒฝ๋กœ๋ฅผ ์ฐพ์•„๋‚ด๋Š” ๋ฐฉ์‹์€ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ฒฝ๋กœ๋ฅผ ์ ๋Š” ์ฃผ์ฒด๊ฐ€ ์—์ด์ „ํŠธ๋ผ์„œ, ๋ฌธ์žฅ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๊ทœ์น™์„ ๊ณ ์ณ์•ผ ํ•˜๊ณ  ๋ชป ์žก์œผ๋ฉด ์‚ฌ์šฉ์ž๋Š” ํŒŒ์ผ์„ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” ์—์ด์ „ํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋งํ•˜๋“  ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ฑฐ๋‘˜ ๋•Œ๋Š” ์ด๋ฒˆ ํ„ด์— ์ƒˆ๋กœ ์ƒ๊ธฐ๊ฑฐ๋‚˜ ๋ฐ”๋€ ๊ฒƒ๋งŒ ๋ด…๋‹ˆ๋‹ค(snapshot). ๊ทธ๋Ÿฌ์ง€ ์•Š์œผ๋ฉด ์ด์–ด์ง€๋Š” ํ„ด๋งˆ๋‹ค ๊ฐ™์€ ํŒŒ์ผ์ด ๋‹ค์‹œ ์˜ฌ๋ผ๊ฐ€ ํ™”๋ฉด์— ๊ฐ™์€ ๊ฒƒ์ด ์—ฌ๋Ÿฌ ๋ฒˆ ๋œน๋‹ˆ๋‹ค.

7.3 ๊ตฌ์„ฑ

๋ชจ๋“ˆ ํ•˜๋Š” ์ผ
ArtifactCollector outputs/ ๋ฅผ ๊ฑฐ๋‘ฌ ์‚ฐ์ถœ๋ฌผ ๋ ˆ์ฝ”๋“œ๋กœ ๋งŒ๋“ ๋‹ค
ArtifactStore (Protocol) ๋ณด๊ด€ํ•˜๊ณ  ์ฃผ์†Œ๋ฅผ ๋ฐœ๊ธ‰ํ•˜๋Š” ๊ณ„์•ฝ
MementoArtifactStore ๊ธฐ๋ณธ ๊ตฌํ˜„ โ€” Memento ๋ฅผ ๊ฑฐ์ณ ๋น„๊ณต๊ฐœ ๋ฒ„ํ‚ท์— ๋ณด๊ด€
build_artifact ยท download_file ์„œ๋ฒ„ ์•ˆ์˜ ์ •๋ณธ โ†” ํ™”๋ฉด์ด ์ฝ๋Š” ๋ชจ์–‘
strip_local_paths ๋ณธ๋ฌธ์— ๋‚จ์€ ๋‚ด๋ถ€ ๊ฒฝ๋กœ๋ฅผ ์‚ฐ์ถœ๋ฌผ ์ฃผ์†Œ๋กœ ์น˜ํ™˜

๋ชจ๋‘ from processgpt_agent_sdk.artifacts import ... ๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

7.4 ์ฃผ์†Œ๋Š” ๋งŒ๋ฃŒ๋œ๋‹ค

์‚ฐ์ถœ๋ฌผ์€ ๋น„๊ณต๊ฐœ ๋ฒ„ํ‚ท์— ๋“ค์–ด๊ฐ€๊ณ , ์ฃผ์†Œ๋Š” ํ•œ ์‹œ๊ฐ„์งœ๋ฆฌ ์„œ๋ช… ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค. ์ฃผ์†Œ๊ฐ€ ์ฃฝ์–ด๋„ ํŒŒ์ผ์€ ๋‚จ์•„ ์žˆ์œผ๋ฏ€๋กœ ๋ ˆ์ฝ”๋“œ์— ์‹ค๋ฆฐ file_id ๋กœ ๋‹ค์‹œ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค.

stored = await store.url_for(tenant_id="acme", file_id="artifacts/<uuid>.docx")
stored.url         # ์ƒˆ ์„œ๋ช… ์ฃผ์†Œ
stored.expires_at  # ์ƒˆ ๋งŒ๋ฃŒ ์‹œ๊ฐ

๋ ˆ์ฝ”๋“œ์—๋Š” file_id ์™€ url_expires_at ์ด ํ•จ๊ป˜ ์‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด ๋‘˜์ด ์—†์œผ๋ฉด ํ™”๋ฉด์€ ์ฃผ์†Œ๊ฐ€ ์ฃฝ์€ ๋’ค ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค โ€” ๋งŒ๋ฃŒ ์‹œ๊ฐ์ด ๋น„์–ด ์žˆ์œผ๋ฉด "๋งŒ๋ฃŒ๊ฐ€ ์—†๋‹ค"๋Š” ๋œป์ž…๋‹ˆ๋‹ค (์˜› ๊ณต๊ฐœ ๋ฒ„ํ‚ท์˜ ์˜๊ตฌ ์ฃผ์†Œ).

7.5 ๋ถ™์ด๋Š” ๋ฒ•

from processgpt_agent_sdk.artifacts import (
    ArtifactCollector, MementoArtifactStore, download_file, strip_local_paths,
)

collector = ArtifactCollector(MementoArtifactStore(MEMENTO_BASE_URL))

# ํ„ด์„ ์‹œ์ž‘ํ•  ๋•Œ ์ฐ๋Š”๋‹ค โ€” ์ด๋ฒˆ ํ„ด์ด ๋งŒ๋“  ๊ฒƒ๋งŒ ๊ฐ€๋ ค๋‚ด๊ธฐ ์œ„ํ•ด.
before = collector.snapshot(outputs_dir)

# ... ์—์ด์ „ํŠธ๊ฐ€ outputs/ ์— ์ตœ์ข…๋ณธ์„ ๋†“๋Š”๋‹ค ...

files = await collector.collect(
    outputs_dir,
    tenant_id=tenant_id,
    conversation_id=conversation_id,
    before=before,
    turn_id=turn_id,
)

# ๋ณธ๋ฌธ์— ๋‚จ์€ ๋‚ด๋ถ€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ๋กœ ๋ฐ”๊พผ๋‹ค.
answer = strip_local_paths(answer, files)

๊ฑฐ๋‘” ๋ ˆ์ฝ”๋“œ๋Š” ์ตœ์ข… ์ด๋ฒคํŠธ์˜ metadata ์— pdfFiles ๋กœ ์‹ฃ์Šต๋‹ˆ๋‹ค.

ParseDict({"role": "assistant", "pdfFiles": [download_file(f) for f in files]},
          artifact_evt.metadata)

๊ทธ๋Ÿฌ๋ฉด SDK ๊ฐ€ ๋‘ ๊ฐ€์ง€๋ฅผ ํ•จ๊ป˜ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • chats.messages.pdfFiles ๋กœ ์ €์žฅ โ€” ๋ฐฉ์„ ๋‹ค์‹œ ์—ด์–ด๋„ ํŒŒ์ผ์ด ๋‚จ์Šต๋‹ˆ๋‹ค.
  • SSE done ์— files ๋กœ ์‹ค์–ด ๋ณด๋ƒ„ โ€” ํ™”๋ฉด์ด ์ŠคํŠธ๋ฆฌ๋ฐ ๋์— ์“ฐ๋Š” ํ–‰์—๋„ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

โš ๏ธ done ์— ์‹ฃ์ง€ ์•Š์œผ๋ฉด ํ™”๋ฉด์ด ์ž๊ธฐ๊ฐ€ ๋ณธ ๊ฒƒ์œผ๋กœ ์“ด ํ–‰์—๋Š” ์‚ฐ์ถœ๋ฌผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋ฐฉ์„ ๋‹ค์‹œ ์—ด์—ˆ์„ ๋•Œ ๊ทธ ํ–‰์ด ์ด๊ธฐ๋ฉด ๋งŒ๋“  ํŒŒ์ผ์ด ์‚ฌ๋ผ์ง„ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค. ์šด์˜์—์„œ ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ ๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค.

7.6 ๊ฑฐ๋‘์ง€ ์•Š๋Š” ๊ฒƒ

๊ธฐ์ค€ ๊ฐ’ ์ด์œ 
ํ˜•์‹ DEFAULT_EXTENSIONS ์‹คํ–‰ ํŒŒ์ผยท์†Œ์Šคยท๋กœ๊ทธ๋Š” ๊ฒฐ๊ณผ๋ฌผ์ด ์•„๋‹ˆ๋‹ค
ํฌ๊ธฐ 50MB (DEFAULT_MAX_BYTES) ์ด๋ณด๋‹ค ํฌ๋ฉด ์—…๋กœ๋“œ๊ฐ€ ํ„ด์„ ๋ถ™์žก๊ณ  ํ™”๋ฉด์—์„œ๋„ ๋ฐ›๋‹ค๊ฐ€ ๋Š๊ธด๋‹ค
๋นˆ ํŒŒ์ผ ์ œ์™ธ

collect(..., index=False) ๋Š” ์ƒ‰์ธ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ฒ˜๋Ÿผ ์‚ฌ๋žŒ์ด ํ•œ ๋ฒˆ ๋ณด๊ณ  ๋งˆ๋Š” ๋ถ€์‚ฐ๋ฌผ์— ์”๋‹ˆ๋‹ค โ€” ์šด์˜์—์„œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ PDF ๋ฅผ ์ƒ‰์ธํ•˜๋‹ค๊ฐ€ ํ„ด์ด 5๋ถ„์”ฉ ๋ฉˆ์ถ˜ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

decorate ๋กœ ๋ ˆ์ฝ”๋“œ๋งˆ๋‹ค ์ €์žฅ์†Œ๋ณ„ ํ‘œ์‹œ(๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ Œ๋”ยท๊ฒ€์ˆ˜ ํŒ์ •)๋ฅผ ๋ง๋ถ™์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

7.7 ์šด์˜ ์ค€๋น„

Memento ์ชฝ์— ๊ณต๊ฐœ ์ •์ฑ…์ด ์—†๋Š” ๋ฒ„ํ‚ท์ด ํ•˜๋‚˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—†์œผ๋ฉด ์‚ฐ์ถœ๋ฌผ ์—…๋กœ๋“œ๊ฐ€ ์ „๋ถ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ณธ๊ฐ’ ๋œป
ARTIFACT_BUCKET artifacts ์‚ฐ์ถœ๋ฌผ ์ „์šฉ ๋น„๊ณต๊ฐœ ๋ฒ„ํ‚ท
ARTIFACT_URL_TTL_SECONDS 3600 ์„œ๋ช… ์ฃผ์†Œ ์ˆ˜๋ช…(์ดˆ)

์ž์„ธํ•œ ๋‚ด์šฉ์€ Memento ์ €์žฅ์†Œ์˜ docs/artifact-bucket.md ์— ์žˆ์Šต๋‹ˆ๋‹ค.


8. ์ž‘์—… ์ ์œ ์˜ ๋งŒ๋ฃŒ ์‹œํ•œ (lease)

8.1 ๋ฌด์—‡์ด ๋ฌธ์ œ์˜€๋‚˜

์›Œ์ปค๊ฐ€ todolist ํ•œ ๊ฑด์„ ์ง‘์œผ๋ฉด ๊ทธ ํ–‰์€ draft_status='STARTED' ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ์ „์—๋Š” ๊ฑฐ๊ธฐ์„œ ๋์ด์—ˆ์Šต๋‹ˆ๋‹ค โ€” ๋งŒ๋ฃŒ๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์›Œ์ปค๊ฐ€ kill -9 ๋กœ ์ฃฝ์œผ๋ฉด(OOM, ๋…ธ๋“œ ์ถ•์ถœ, KEDA ์ถ•์†Œ) ๊ทธ ํ–‰์€ STARTED ๋กœ ์˜๊ตฌํžˆ ๋‚จ๊ณ , ์ง‘๋Š” ์กฐ๊ฑด(draft_status IS NULL ๋˜๋Š” FB_REQUESTED)์— ๋‹ค์‹œ ๊ฑธ๋ฆฌ์ง€ ์•Š์•„ ์•„๋ฌด๋„ ์ง‘์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์˜์›ํžˆ ๋๋‚˜์ง€ ์•Š๋Š” ์ž‘์—…์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

8.2 SDK ๊ฐ€ ํ•˜๋Š” ์ผ

์—์ด์ „ํŠธ ์ฝ”๋“œ๊ฐ€ ๋”ฐ๋กœ ํ•  ์ผ์€ ์—†์Šต๋‹ˆ๋‹ค. ProcessGPTAgentServer ๊ฐ€ ์•Œ์•„์„œ ํ•ฉ๋‹ˆ๋‹ค.

  1. ์ง‘์„ ๋•Œ lease ๋ฅผ ๊ฑด๋‹ค โ€” fetch_pending_task ์— p_lease_seconds ๋ฅผ ๋„˜๊น๋‹ˆ๋‹ค.
  2. ์ˆ˜ํ–‰ ์ค‘ ์—ฐ์žฅํ•œ๋‹ค โ€” LeaseKeeper ๊ฐ€ ๋ณ„๋„ OS ์Šค๋ ˆ๋“œ์—์„œ renew_task_lease ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ถ€๋ฆ…๋‹ˆ๋‹ค. ์ต์Šคํํ„ฐ๊ฐ€ ๋™๊ธฐ ํ˜ธ์ถœ๋กœ ์ด๋ฒคํŠธ ๋ฃจํ”„๋ฅผ ๋ถ™์žก๊ณ  ์žˆ์–ด๋„ ์—ฐ์žฅ์ด ๋ฉˆ์ถ”์ง€ ์•Š์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค(๋ฉˆ์ถ”๋ฉด ์‚ด์•„์„œ ์ผํ•˜๋Š” ์ค‘์ธ ์ž‘์—…์ด ํšŒ์ˆ˜๋˜์–ด ๋‘ ๋ฒˆ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค).
  3. ํšŒ์ˆ˜๋‹นํ•˜๋ฉด ๋ฒ„๋ฆฐ๋‹ค โ€” ์—ฐ์žฅ์ด not_owner ๋กœ ๊ฑฐ์ ˆ๋˜๋ฉด(๋‹ค๋ฅธ ์›Œ์ปค๊ฐ€ ์ด๋ฏธ ๊ฐ€์ ธ๊ฐ”๋‹ค) ์ง„ํ–‰ ์ค‘์ธ execute() ๋ฅผ ์ทจ์†Œํ•˜๊ณ , FAILED ๋กœ ๋งˆํ‚นํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ ์ž‘์—…์€ ์ด์ œ ๋‚จ์˜ ๊ฒƒ์ด๊ณ , FAILED ๋กœ ๋ฎ์œผ๋ฉด ๊ทธ์ชฝ์ด ๋๋‚ธ ๊ฒฐ๊ณผ๋ฅผ ์‹คํŒจ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.
  4. ๋– ๋‚  ๋•Œ ํ•ด์ œํ•œ๋‹ค โ€” ์ •์ƒ ์ข…๋ฃŒ ์‹œ release_task_lease ๋กœ ์ ์œ ๋ฅผ ์ฆ‰์‹œ ๋น„์›๋‹ˆ๋‹ค.

COMPLETEDยทHUMAN_ASKEDยทCANCELLED ๋กœ ๋„˜์–ด๊ฐ„ ์ž‘์—…์˜ ์—ฐ์žฅ ์‹คํŒจ๋Š” "๋ฒ„๋ ค๋ผ" ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค(not_started). heartbeat ๋งŒ ๋ฉˆ์ถ”๊ณ  ์ž‘์—…์€ ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค โ€” ์‚ฌ๋žŒ ๋‹ต๋ณ€์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ž‘์—…์ด lease ๋งŒ๋ฃŒ๋กœ ํšŒ์ˆ˜๋˜์ง€ ์•Š๋Š” ๊ฒƒ๋„ ๊ฐ™์€ ์ด์œ ์ž…๋‹ˆ๋‹ค(RPC ๊ฐ€ STARTED ๋งŒ ํšŒ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค).

8.3 ์„ค์ •

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ณธ๊ฐ’ ๋œป
TASK_LEASE_SECONDS 120 ์ ์œ ๊ฐ€ ์œ ์ง€๋˜๋Š” ์‹œ๊ฐ„
TASK_LEASE_HEARTBEAT_SECONDS lease/4 (=30) ์—ฐ์žฅ ์ฃผ๊ธฐ
TASK_MAX_CLAIMS 3 ํ•œ ์ž‘์—…์ด ์ ์œ ๋  ์ˆ˜ ์žˆ๋Š” ํšŸ์ˆ˜(์ตœ์ดˆ + ํšŒ์ˆ˜ 2ํšŒ). ๋„˜์œผ๋ฉด RPC ๊ฐ€ FAILED ๋กœ ์ข…๊ฒฐ

์ฃผ๊ธฐ๋ฅผ lease ์˜ 1/4 ๋กœ ๋‘” ๊ฒƒ์€, ์ผ์‹œ์  DB ์˜ค๋ฅ˜๋กœ ์—ฐ์† ์„ธ ๋ฒˆ ๋†“์ณ๋„ lease ๊ฐ€ ๋‚จ์•„ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค. 1/2 ๋กœ ๋‘๋ฉด ํ•œ ๋ฒˆ ๋†“์น˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋งŒ๋ฃŒ์— ๋‹ฟ์•„ ๋ฉ€์ฉกํ•œ ์ž‘์—…์ด ํšŒ์ˆ˜๋ฉ๋‹ˆ๋‹ค.

8.4 DB ์ชฝ ์š”๊ตฌ์‚ฌํ•ญ

todolist ์— lease_until timestamptz ์™€ claim_count integer ๊ฐ€ ์žˆ์–ด์•ผ ํ•˜๊ณ , fetch_pending_task ๊ฐ€ p_lease_seconds/p_max_claims ๋ฅผ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์Šคํ‚ค๋งˆ์™€ RPC, ๊ทธ๋ฆฌ๊ณ  kind ํด๋Ÿฌ์Šคํ„ฐ ์‹ค์ธก ๊ฒฐ๊ณผ๋Š” process-gpt-infra-docker ์ €์žฅ์†Œ์— ์žˆ์Šต๋‹ˆ๋‹ค (volumes/db/{init.sql,migration.sql}, tests/lease/README.md).

๊ตฌ๋ฒ„์ „ SDK ์™€ ์„ž์—ฌ ๋Œ์•„๋„ ๋ฉ๋‹ˆ๋‹ค. p_lease_seconds ๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š๋Š” ํ˜ธ์ถœ์€ lease ์—†์ด ์ง‘๊ณ (= ์˜ˆ์ „ ๋™์ž‘), lease ๊ฐ€ ๋น„์–ด ์žˆ๋Š” ์ ์œ ๋Š” ํšŒ์ˆ˜ ๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค.


9. ๋ฒ„์ „์—…

  • ./release.sh ๋ฒ„์ „
  • ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ : python -m ensurepip --upgrade

10. integrations ๋ชจ๋“ˆ ์•ˆ๋‚ด

  • ์Šคํ† ๋ฆฌ์ง€ ์—…๋กœ๋“œ ์œ ํ‹ธ์€ processgpt_agent_sdk.integrations.storage ๋กœ ๋ถ„๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด processgpt_agent_sdk.utils.upload_file_to_bucket, upload_files_to_bucket ๋Š” ํ•˜์œ„ํ˜ธํ™˜์šฉ์œผ๋กœ ์œ ์ง€๋˜์ง€๋งŒ deprecated ์ž…๋‹ˆ๋‹ค.
  • ์‹ ๊ทœ ์ฝ”๋“œ๋Š” ์•„๋ž˜ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:
    • from processgpt_agent_sdk.integrations.storage import upload_file_to_bucket, upload_files_to_bucket

9.1 ์ฑ„ํŒ… ์ „์†ก ๊ณ„์ธต ๋ชจ๋“ˆ (0.5.0 ์ถ”๊ฐ€)

๋ชจ๋“ˆ ๋“ค์–ด ์žˆ๋Š” ๊ฒƒ
processgpt_agent_sdk.tenant_auth authorize_tenant, tenant_guard, auth_guard, request_tenant_id, ChatTenantGuardMiddleware, TenantAuthError
processgpt_agent_sdk.chat_registry ChatRunRegistry, InflightRegistry, set_run_registry, set_inflight_registry
processgpt_agent_sdk.chat_sse with_heartbeat, apply_heartbeat, format_sse_message, make_attach_handler, make_stop_handler

์…‹ ๋‹ค ์ตœ์ƒ์œ„(from processgpt_agent_sdk import ...)๋กœ๋„ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ mount_chat_routes() ํ•˜๋‚˜๋ฉด ์ถฉ๋ถ„ํ•˜๊ณ , ์ด ๋ชจ๋“ˆ๋“ค์€ ๋ผ์šฐํŠธ๋ฅผ ์ง์ ‘ ์กฐ๋ฆฝํ•˜๊ฑฐ๋‚˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ๊ต์ฒดํ•  ๋•Œ๋งŒ ์”๋‹ˆ๋‹ค.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages