API Schema 처리 흐름¶
이 문서는 HTTP request가 Pydantic model로 변환되고 Dialogue Manager를 거쳐 public response가 되기까지의 흐름을 설명합니다. Field 정의는 Schema, 실제 호출 예는 Endpoint를 참고합니다.
1. 전체 흐름¶
flowchart LR
C["HTTP Client"] --> A["FastAPI<br/>Route + Pydantic"]
A --> DM["DialogManager<br/>await (async)"]
DM --> DST["DSTManager<br/>NLU + Policy + Transition"]
DST -->|"skill object, once per node"| NS["Node Skill stage"]
NS --> SC["SkillRuntimeClient"]
DST --> NLU["NLU"]
NS --> JOIN["Join before NLG"]
NLU --> JOIN
DST --> ER{"Executor Resolver"}
ER -->|"collect_slots"| TE["TaskOrientedExecutor"]
ER -->|"default"| GE["GeneralResponseExecutor"]
SC -->|"POST /v1/skills/execute"| SR["sohri-skill-runtime"]
SR -->|"skill result"| SC
SC --> NS
JOIN --> ER
TE --> IR["Internal Result"]
GE --> IR
IR --> RB["API Response Builder"]
RB --> A
A --> C
A -. "400 / 422" .-> CE["Client Error"]
DM -. "Unhandled exception" .-> E500["HTTP 500"]
POST /dm/v1/message 와 session 생성 route 는 turn 처리를 worker thread 로 넘기지
않고 직접 await 합니다. LLM 호출이 async(AsyncOpenAI)이므로 여러 turn 의 LLM
대기가 한 thread 위에서 겹칠 수 있고, 동시 turn 수가 기본 executor 의 worker 수
(min(32, os.cpu_count() + 4))에 묶이지 않습니다. Scenario validation 과 session
삭제처럼 아직 동기인 작업만 asyncio.to_thread() 로 넘깁니다.
Session lock 은 두 겹입니다.
asyncio.Lock(process 내부) — 같은 event loop 에서 같은 session 의 turn 을 직렬화합니다. Event loop 를 막지 않으므로 다른 session 은 계속 진행합니다.- 공유 lock(
SessionLockFactory) — pod 사이 상호배제입니다.memory모드에서는 process-localRLock,redis모드에서는SET NX PX분산 lock 입니다. 후자만이 pod 를 여러 개 띄웠을 때 같은 session 의 동시 turn 을 막습니다. 대기 상한 (SESSION_LOCK_WAIT_MS)을 넘기면SessionLockTimeout이고 route 는 이를 HTTP503+Retry-After로 바꿉니다.
2. Request parsing과 validation¶
처리는 다음 순서로 진행됩니다.
- FastAPI가 method와 path에 맞는 route를 선택합니다.
- JSON body를 route의 Pydantic request model로 검증합니다.
- 생략된 field에는 model 기본값이 적용됩니다.
- 정의되지 않은 추가 field는 현재 기본 설정에 따라 무시됩니다.
- Route가 endpoint별 operational requirement를 추가 검사합니다.
Pydantic type 또는 shape가 맞지 않으면 runtime을 호출하기 전에 HTTP 422가
발생합니다. 반면 scenario_name=""처럼 schema상 허용되지만 실제 처리에 필요한
값이 비어 있으면 route가 HTTP 400을 반환합니다.
ChatbotSessionLocation object는 예외적으로 latitude와 longitude가 모두
schema-required이며 각각 [-90, 90], [-180, 180] 범위를 검증합니다.
API 입출력 model 관계¶
flowchart LR
subgraph SessionCreate["Session 생성"]
SCR["ChatbotSessionCreateRequest"]
SU["ChatbotSessionUser"]
SL["ChatbotSessionLocation"]
CO["CompletionOptions"]
SCR --> SU
SU --> SL
SCR --> CO
SCR --> SRES["ChatbotSessionCreateResponse<br/>또는 ChatbotMessageResponse"]
end
subgraph MessageTurn["Message 처리"]
CR["ChatbotRequest"]
UI["UserInput"]
MI["MessageInput"]
RC["RequeryContext"]
CR --> UI
CR --> MI
MI --> RC
CR --> MRES["ChatbotMessageResponse"]
MRES --> FD["ChatbotFinalData<br/>session 완료 시"]
end
subgraph SessionRead["Session 조회"]
SID["session_id path parameter"]
SID --> SI["ChatbotSessionInfoResponse"]
end
3. Session 생성¶
POST /v1/session¶
sequenceDiagram
autonumber
participant C as Client
participant API as FastAPI
participant DM as DialogManager
participant DST as DSTManager
participant SR as sohri-skill-runtime
C->>API: ChatbotSessionCreateRequest
API->>API: scenario_name 검사<br/>user 기본값 제거
API->>DM: start_session_with_greeting
DM->>DST: scenario load + DialogueState 생성
alt bot_first + skill object
DST->>SR: POST /v1/skills/execute
SR-->>DST: weather skill result
DST->>DST: NLG greeting + state update
else bot_first without skill
DST->>DST: template 또는 일반 NLG greeting
else bot_first false
DST->>DST: initial snapshot
end
DST-->>DM: initial internal result
DM-->>API: session_id + initial result
API-->>C: ChatbotSessionCreateResponse
user에서는 기본값과 None을 제거한 뒤 session context에 저장합니다. 따라서
요청하지 않은 빈 profile이나 metadata는 저장하지 않습니다. scenario_graph는 같은
request model에 존재하지만 이 endpoint에서는 사용하지 않습니다.
bot_first는 request → graph.bot_first → legacy metadata.bot_first → false
순서로 결정됩니다. 활성화되면 initial turn 결과를 initial_* field로 옮깁니다.
POST /v1/session/json¶
flowchart LR
R["ChatbotSessionCreateRequest"] --> G["scenario_graph 검사"]
G --> U["user 기본값 제거"]
U --> D["start_session_with_greeting<br/>scenario_graph 전달"]
D --> I["Internal initial result"]
I --> B["build_message_response"]
B --> O["ChatbotMessageResponse"]
이 endpoint에서는 scenario_name을 무시합니다. /session과 달리 session-create
전용 response가 아니라 message response 형태로 초기 snapshot 또는 greeting 결과를
반환합니다.
4. Scenario validation¶
flowchart LR
R["ChatbotValidateRequest<br/>scenario_graph"] --> V["validate_scenario_from_dict"]
V --> Q{"valid?"}
Q -->|"yes"| S["HTTP 200<br/>status: success"]
Q -->|"no"| E["HTTP 200<br/>status: error"]
Graph validation은 실제 session을 생성하지 않습니다. Validator가 false를 반환해도
transport status는 HTTP 200이며 body가
{"status":"error","message":"Scenario validation failed"}가 됩니다. Request object
자체가 잘못된 경우에는 그 전에 HTTP 422가 발생합니다.
5. Message turn¶
sequenceDiagram
autonumber
participant C as Client
participant API as Message Route
participant DM as DialogManager
participant DST as DSTManager
participant EX as Selected Executor
participant SR as sohri-skill-runtime
C->>API: ChatbotRequest
API->>DM: session_id + message_input.text
DM->>DM: session lock
DM->>DST: process_turn
par skill object가 있는 node
DST->>SR: skill_id + arguments + context
SR-->>DST: success/result 또는 error
and NLU
DST->>DST: intent + dialogue act + entities
end
DST->>DST: policy
DST->>EX: NLU + Skill 결과로 executor/NLG 실행
EX-->>DST: response + slot/context updates
DST->>DST: state update + transition + save
DST-->>DM: internal result
DM-->>API: result + completion options
API->>API: build_message_response
API-->>C: ChatbotMessageResponse
현재 route가 runtime에 전달하지 않는 field:
user_input전체message_input.durationmessage_input.emotionmessage_input.requery_requiredmessage_input.requery_context
message_input.confidence는 예외로 DialogManager.process_turn(input_confidence=...)에
전달합니다. 세션의 stt_confidence_threshold 이하이면 일반 NLU를 생략하고
FallbackPolicy의 이해 불가 경로를 사용합니다. 위 목록의 값들은 request
contract에는 포함되지만 현재 NLU, policy 또는 executor 동작을 변경하지 않습니다.
message_input.confidence는 STT가 만든 입력 신뢰도이고, 이후 규칙/LLM NLU가 만드는
NLUResult.confidence는 별도 내부 값입니다. API client가 NLU confidence를 제공하는
field는 없습니다. 빈 text는 route validation 오류가 아니라 no_input으로 분류되며,
현재 fallback policy는 node 고정 문구보다 LLM 문맥 생성을 우선합니다.
6. sohri-skill-runtime 입출력¶
Node의 skill.name이 비어 있지 않으면 NLU와 병렬인 Node Skill stage가
session의 node ID별로 한 번 호출합니다. Executor resolver는 collect_slots만 보고
Task/General executor를 선택합니다. Graph Builder container에서
SKILL_RUNTIME_BASE_URL로 설정한 service
주소의 /v1/skills/execute를 호출합니다. 같은 Kubernetes namespace 또는 container
network에서는 service DNS 이름을 base URL로 사용할 수 있습니다.
위치 기반 날씨 greeting의 데이터 흐름¶
flowchart TB
REQ["Session request<br/>user.location.latitude<br/>user.location.longitude"]
CTX["DialogueState.context<br/>session_user.location"]
NODE["Scenario node<br/>skill.name: weather<br/>skill.input_map: latitude, longitude"]
MAP["Node Skill stage map_inputs"]
HTTP["Skill Runtime HTTP request<br/>skill_id + arguments + context"]
WEATHER["sohri-skill-runtime<br/>weather"]
RESULT["Skill result<br/>result.summary 등"]
UPDATE["result_map<br/>greeting_weather_summary"]
NLG["NLG<br/>날씨 안내 + SeniMate 인사"]
RESPONSE["initial_response"]
REQ --> CTX
CTX --> MAP
NODE --> MAP
MAP --> HTTP
HTTP --> WEATHER
WEATHER --> RESULT
RESULT --> UPDATE
RESULT --> NLG
UPDATE --> NLG
NLG --> RESPONSE
senimate_weather.json의 greeting node는 다음 mapping을 사용합니다.
session request user.location
→ DialogueState.context.session_user.location
→ input_map.latitude / input_map.longitude
→ Skill Runtime arguments.latitude / arguments.longitude
→ weather
→ result.summary
→ context.greeting_weather_summary + NLG response
Graph Builder 내부 request model은 transport 직전에 Skill Runtime HTTP contract로
변환됩니다. SkillInvocationRequest.input에는 scenario의 input_map이 선택한
allowlist 값만 들어가며, HTTP 전송 시 고정 arguments 위에 같은 이름의 input 값을
덮어써 하나의 arguments object로 합칩니다.
flowchart LR
S["Session user<br/>locale, timezone, location"] --> I["SkillInvocationRequest"]
N["Node params<br/>skill, arguments, input_map"] --> I
D["Dialogue state<br/>session_id, slots, context, NLU"] --> I
I --> H["HTTP JSON<br/>skill_id<br/>arguments<br/>context"]
H --> SR["sohri-skill-runtime"]
SR --> RR["success + result"]
RR --> VR["SkillInvocationResponse<br/>status + output"]
VR --> RM["result_map + allowed_updates"]
RM --> DR["Dialogue response<br/>slot/context updates + text"]
날씨 scenario에서 전송되는 HTTP payload의 형태:
{
"skill_id": "weather",
"arguments": {
"latitude": 37.5172,
"longitude": 127.0473,
"days": 1
},
"context": {
"request_id": "generated-request-id",
"session_id": "session-id",
"skill_version": "v1",
"locale": "ko-KR",
"timezone": "Asia/Seoul"
}
}
Skill Runtime의 호환 응답:
Client는 이를 내부 status="success", output=result 구조로 정규화합니다.
SkillInvocationResponse.output의 model type은 Dict[str, Any]이므로 성공 result는
JSON object여야 합니다. 문자열·배열·null result는 response validation 실패로
SKILL_RUNTIME_INVALID_RESPONSE가 됩니다. Citation과 update field도 각각 typed
list/object로 검증합니다.
result_map은 scenario가 지정한 output/update path만 slot/context로 복사하고,
Runtime의 slot_updates/context_updates는 allowed_updates에 key가 명시된 경우만
수용합니다. Reserved context key와 _ prefix key는 허용 목록에 있어도 버립니다.
Node Skill은 direct mode를 지원하지 않습니다. output, citations, metadata,
status를 NLU 결과와 함께 NLG context에 넣고, 두 비동기 branch가 끝난 뒤 한 번 NLG를
실행합니다. response_mode와 response_path는 validation에서 거부합니다.
Timeout, 연결 실패, HTTP 오류, 너무 큰 응답 또는 schema 오류가 발생하면
오류 상태를 NLG에 전달합니다. NLG까지 실패하면 skill.fallback →
responses.skill_error → responses.error →
responses.default → DM 기본 오류 문장 순으로 반환하고
last_skill_status="error"를 dialogue context에 기록합니다.
7. Internal result에서 public response로 변환¶
dm/api/response_builder.py는 내부 result를 그대로 반환하지 않고 public model을
새로 조립합니다.
flowchart LR
IR["Internal result<br/>response, slots, context, details, error"] --> RB["build_message_response"]
RB --> KEEP["Public fields<br/>session_id, response, current_node<br/>turn_count, time, complete, error<br/>dialog_act, intent, slots<br/>named_entities, executor"]
RB --> DROP["Removed fields<br/>details, context<br/>error_code, error_detail"]
RB --> Q{"session_complete?"}
Q -->|"no"| NORMAL["ChatbotMessageResponse"]
Q -->|"yes"| FINAL["ChatbotFinalData<br/>end_reason + optional data"]
KEEP --> NORMAL
KEEP --> FINAL
| Internal value | Public 처리 |
|---|---|
null session_id, response, current_node |
빈 문자열로 정규화 |
| null count/time | 0, 0.0으로 정규화 |
details, context |
일반 message response에서 제거 |
error_code, error_detail |
제거 |
error |
boolean으로 보존 |
session_complete=false |
final_data 생성 안 함 |
session_complete=true |
completion option에 따라 final_data 생성 |
include_slots=false 또는 include_history=false인 field는 None이 되고,
response_model_exclude_none=true에 의해 JSON에서 생략됩니다. Conversation history는
object이면서 role과 message가 모두 문자열인 항목만 보존합니다.
이 변환은 완료 이벤트를 한 번만 소비하는 저장 장치를 두지 않습니다.
session_complete=true인 결과마다 final_data를 다시 만들 수 있으며, 완료된 state도
후속 /message를 거부하지 않습니다. Terminal node의 재개 transition이 맞으면
current_node는 이동할 수 있지만 일반 runtime은 is_complete를 자동으로 해제하지
않습니다.
8. Session 조회¶
flowchart LR
ID["session_id"] --> E{"Session config와<br/>DSTManager 존재?"}
E -->|"no"| N["HTTP 404"]
E -->|"yes"| L["session lock"]
L --> CS["ContextStore<br/>DialogueState + TTL"]
CS --> Q{"state 존재?"}
Q -->|"no 또는 만료"| N
Q -->|"yes"| F["flat session info"]
F --> R["ChatbotSessionInfoResponse<br/>HTTP 200"]
상태가 존재하면 current node, turn count, 완료 여부, 채워진 slots, context와 TTL을
반환합니다. Session config가 없거나 scenario runtime을 복원하지 못했거나 dialogue
state가 만료되었으면 service가 None을 반환하고 route가 HTTP 404로 변환합니다.
저장소는 SESSION_STORAGE_MODE 가 정합니다. redis 면 session 설정·대화 상태·lock
이 전부 공유 Redis 에 있어 pod 를 늘릴 수 있고, memory 면 process-local 이라
pod 1개 전용입니다(chart 가 replicaCount: 1 을 강제합니다). redis 모드에서는
연결 실패를 memory 로 폴백하지 않고 기동에 실패합니다. Redis 명령이 실패하면
SessionStoreUnavailable 이며 route 는 이를 404 가 아니라 HTTP 503 으로
드러냅니다 - "지우지 못했다"와 "지울 것이 없었다"는 다른 사건이기 때문입니다.
9. 오류를 해석하는 방법¶
| 구분 | 예 | Client 처리 |
|---|---|---|
| HTTP validation | 422 |
detail[]의 field path 확인 |
| Route validation | 400 |
detail 확인 후 request 수정 |
| Body-level validation result | HTTP 200, status="error" |
Scenario 수정 |
| Body-level dialogue error | HTTP 200, error=true |
실패 turn으로 처리 |
| Unhandled server error | 500 |
재시도 정책 적용 전 server log 확인 |
현재 통일된 error response model, authentication middleware와 CORS middleware는 없습니다. Rate limiting, request size와 scenario graph depth 제한도 API layer에서 설정하지 않습니다.
10. 구현 파일 대응¶
| 역할 | 파일 |
|---|---|
| FastAPI application과 root endpoint | dm/api/app.py |
| Router prefix와 metadata | dm/api/routes/common.py, dm/api/routes/overview.py |
| Session·validation routes | dm/api/routes/sessions.py |
| Message route | dm/api/routes/messages.py |
| Request/response models | dm/api/schemas/ |
| Public message response 변환 | dm/api/response_builder.py |
| Executor 선택과 Skill 실행 | dm/core/executors/resolver.py, dm/core/executors/skill_executor.py |
| Skill Runtime HTTP client와 contract | dm/skills/client.py, dm/skills/models.py, dm/skills/mapping.py |
| Process-local DialogManager 생성 | dm/api/dependencies.py |
| Environment와 CLI 설정 | dm/api/config.py, dm/api/parser.py |
| Logging 초기화 | dm/api/bootstrap.py |
Application import 시 .env를 읽고 exp/<API_APP_SYMBOL> directory를 생성합니다.
/session/json은 debug log에 전체 graph를 남길 수 있고 runtime log에는 사용자
발화가 포함될 수 있으므로 운영 log level과 개인정보 정책을 적용해야 합니다.