콘텐츠로 이동

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 --> W["Worker Thread<br/>asyncio.to_thread"]
    W --> DM["DialogManager"]
    DM --> DST["DSTManager<br/>NLU + Policy + Transition"]
    DST --> ER{"Executor Resolver"}
    ER -->|"params.skill"| SE["SkillExecutor"]
    ER -->|"collect_slots"| TE["TaskOrientedExecutor"]
    ER -->|"default"| GE["GeneralResponseExecutor"]
    SE --> SC["SkillRuntimeClient"]
    SC -->|"POST /v1/skills/execute"| SR["sohri-skill-runtime"]
    SR -->|"skill result"| SC
    SC --> SE
    SE --> IR["Internal Result"]
    TE --> IR
    GE --> IR
    IR --> RB["API Response Builder"]
    RB --> A
    A --> C

    A -. "400 / 422" .-> CE["Client Error"]
    DM -. "Unhandled exception" .-> E500["HTTP 500"]

FastAPI route 함수는 async이지만 scenario load, validation과 dialogue turn 같은 blocking 작업은 asyncio.to_thread()로 worker thread에 넘깁니다. 이를 통해 해당 작업이 실행되는 동안 FastAPI event loop가 직접 차단되는 것을 줄입니다. 같은 process 안에서는 session별 RLock이 동일 session의 turn을 직렬화합니다. 다른 session은 병렬 처리될 수 있지만 이 lock은 worker process나 pod 사이에 공유되지 않습니다.

2. Request parsing과 validation

처리는 다음 순서로 진행됩니다.

  1. FastAPI가 method와 path에 맞는 route를 선택합니다.
  2. JSON body를 route의 Pydantic request model로 검증합니다.
  3. 생략된 field에는 model 기본값이 적용됩니다.
  4. 정의되지 않은 추가 field는 현재 기본 설정에 따라 무시됩니다.
  5. Route가 endpoint별 operational requirement를 추가 검사합니다.

Pydantic type 또는 shape가 맞지 않으면 runtime을 호출하기 전에 HTTP 422가 발생합니다. 반면 scenario_name=""처럼 schema상 허용되지만 실제 처리에 필요한 값이 비어 있으면 route가 HTTP 400을 반환합니다.

ChatbotSessionLocation object는 예외적으로 latitudelongitude가 모두 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 + params.skill
        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_firstfalse 순서로 결정됩니다. 활성화되면 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
    DST->>DST: NLU + policy
    DST->>EX: node에 맞는 executor 실행
    opt params.skill이 있는 node
        EX->>SR: skill_id + arguments + context
        SR-->>EX: success/result 또는 error
    end
    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.duration
  • message_input.emotion
  • message_input.requery_required
  • message_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의 params.skill이 비어 있지 않으면 executor resolver가 SkillExecutor를 선택합니다. 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: weather<br/>input_map: latitude, longitude"]
    MAP["SkillExecutor 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의 호환 응답:

{
  "success": true,
  "result": {
    "summary": "서울은 현재 맑고 기온은 24도입니다."
  },
  "metadata": {}
}

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_updatesallowed_updates에 key가 명시된 경우만 수용합니다. Reserved context key와 _ prefix key는 허용 목록에 있어도 버립니다.

response_mode="direct"(기본값)는 response_pathoutput.responseoutput.answeroutput.summaryoutput.text 순으로 비어 있지 않은 문자열을 찾고, 없으면 success/default template, 마지막으로 non-empty output object의 JSON 문자열을 사용합니다. response_mode="nlg"일 때만 output과 citations를 NLG context에 넣습니다. 현재 구현은 정확히 "nlg"인 값만 NLG mode로 취급하고 그 외 값은 direct처럼 처리하므로 scenario validation에서 허용값을 관리해야 합니다.

Timeout, 연결 실패, HTTP 오류, 너무 큰 응답 또는 schema 오류가 발생하면 params.fallbackresponses.skill_errorresponses.errorresponses.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"]
    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이면서 rolemessage가 모두 문자열인 항목만 보존합니다.

이 변환은 완료 이벤트를 한 번만 소비하는 저장 장치를 두지 않습니다. 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나 DSTManager가 없거나 dialogue state가 만료되었으면 service가 None을 반환하고 route가 HTTP 404로 변환합니다. 조회는 asyncio.to_thread()에서 실행되므로 Redis 같은 blocking store를 사용하더라도 FastAPI event loop를 직접 차단하지 않습니다.

현재 API singleton은 use_redis=false이므로 실제 저장은 process-local memory입니다. Session lock도 process-local이며 worker/Pod 사이의 동시성을 제어하지 않습니다. ContextStore TTL 만료가 SessionManager 설정, DSTManager runtime과 lock을 원자적으로 정리하지도 않습니다.

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과 개인정보 정책을 적용해야 합니다.

관련 문서