24장. 서비스 통합과 실행
FastAPI — 파이썬 함수를 주소로 만든다
한 줄 요약
FastAPI는 파이썬 함수를 웹 주소에 연결해 브라우저에서 호출할 수 있게 해 주는 웹 프레임워크입니다. 함수 위에 한 줄을 붙이면 그 함수가 해당 주소의 요청을 처리하고, 들어오는 입력은 4장에서 쓴 Pydantic이 처리 전에 검사합니다.
1. 함수 위에 한 줄
from fastapi import FastAPI
app = FastAPI(title="하루마켓 고객지원 멀티에이전트")
@app.get("/api/health")
def health():
return {"ok": True, "customer": session.customer_id}
| 줄 | 뜻 |
|---|---|
app = FastAPI(...) |
서버 프로그램 하나를 만든다 |
@app.get("/api/health") |
"/api/health 주소로 조회 요청(GET) 이 오면 아래 함수를 실행한다" |
return {...} |
돌려준 딕셔너리가 JSON으로 바뀌어 응답이 된다 |
이렇게 주소 하나와 함수 하나가 짝지어진 것을 엔드포인트(endpoint) 라고 부릅니다.
요청에는 종류가 있습니다. 이번 장에서는 두 가지만 씁니다.
| 종류 | 쓰임 | 데코레이터 |
|---|---|---|
| GET | 무언가를 가져온다. 브라우저 주소창에 주소를 치면 GET이다 | @app.get(...) |
| POST | 내용을 보내서 처리를 시킨다 | @app.post(...) |
2. 우리 서버의 엔드포인트 여덟 개
고객 쪽 셋과 상담원 쪽 다섯입니다.
| 종류와 주소 | 하는 일 | 누가 부르나 |
|---|---|---|
GET / |
채팅 화면(index.html)을 내려 준다 |
고객의 브라우저 주소창 |
GET /api/health |
서버가 살아 있는지, 누구로 로그인되어 있는지 알려 준다 | 채팅 화면(머리글 아래 표시), 점검할 때 |
POST /api/chat |
문의를 받아 Supervisor를 실행하고 답을 흘려보낸다 | 채팅 화면 |
GET /admin |
상담원 화면(admin.html)을 내려 준다 |
상담원의 브라우저 주소창 |
GET /api/actions |
처리 내역 전체(배송지 변경, 교환 접수, 환불·취소 요청)를 돌려준다 | 상담원 화면 |
GET /api/approvals |
그 가운데 승인을 기다리는 요청만 돌려준다 | 점검할 때, 다른 프로그램 |
POST /api/approvals/{action_id} |
상담원의 승인 또는 반려를 받아 기록한다 | 상담원 화면의 승인·반려 버튼 |
GET /api/tickets |
에이전트가 처리하지 못해 넘겨받은 문의(티켓) 목록을 돌려준다 | 상담원 화면 |
GET /과 GET /admin이 화면을 주고, 그 화면들이 나머지 주소를 부릅니다. 화면과 API를 한 서버가 함께 내어 주는 구조입니다.
상담원 쪽 주소의 함수는 모두 몇 줄입니다. 일은 21장의 haru_actions.py가 하고, 서버는 주소만 붙입니다.
@app.get("/api/approvals")
def approvals():
"""승인을 기다리는 환불·주문 취소 요청."""
return pending_approvals()
@app.post("/api/approvals/{action_id}")
def approve(action_id: str, body: Decision):
"""상담원이 승인(approve=true) 또는 반려(approve=false)한다."""
result = decide(action_id, approve=body.approve, by="상담원")
log.info("[승인] %s → %s", action_id, result.get("status", result))
return result
| 줄 | 뜻 |
|---|---|
"/api/approvals/{action_id}" |
주소의 중괄호 자리에 온 값이 함수의 action_id 인자로 들어온다. /api/approvals/A00001이면 action_id는 "A00001" |
body: Decision |
함께 보낸 내용. 모양은 아래 3절의 Pydantic 모델이 정한다 |
decide(action_id, approve=..., by="상담원") |
23장의 결과 기록 노드가 부르던 그 함수. 승인대기를 승인완료나 반려로 바꾼다 |
23장에서 Command(resume="approve")가 하던 일을, 여기서는 상담원 화면의 버튼이 보낸 요청이 합니다. 끝에서 불리는 함수는 같습니다.
3. 들어오는 입력을 문 앞에서 검사한다
/api/chat과 /api/approvals/{action_id}는 밖에서 오는 내용을 받습니다. 무엇이 올지 모르므로 모양부터 정해 둡니다.
from pydantic import BaseModel
class ChatRequest(BaseModel):
session_id: str
message: str
class Decision(BaseModel):
approve: bool
@app.post("/api/chat")
def chat(req: ChatRequest):
... # req.session_id, req.message 로 꺼내 쓴다
| 모델 | 어느 주소의 입력인가 | 칸 |
|---|---|---|
ChatRequest |
POST /api/chat |
어느 대화인지(session_id), 문의 내용(message) |
Decision |
POST /api/approvals/{action_id} |
승인이면 true, 반려면 false (approve) |
4장에서 Pydantic으로 LLM의 출력을 검사했습니다. 여기서는 같은 도구로 바깥에서 들어오는 입력을 검사합니다. 방향만 다르고 하는 일은 같습니다.
모양이 맞지 않는 요청은 우리 함수가 실행되기도 전에 FastAPI가 돌려보냅니다. session_id를 빼고 보내면 이런 응답이 옵니다(이 교재를 준비하며 실제로 받은 응답입니다).
{"detail":[{"type":"missing","loc":["body","session_id"],"msg":"Field required","input":{"message":"안녕"}}]}
응답 코드는 422입니다. "요청의 형식은 알아봤지만 내용이 약속과 다르다"는 뜻입니다. Supervisor는 불리지 않았고 LLM 비용도 나가지 않았습니다.
믿을 수 없는 것이 들어오는 자리마다 검사를 둡니다. LLM의 출력이 들어오는 자리(4장), 도구의 인자가 들어오는 자리(8장), 그리고 바깥의 요청이 들어오는 자리(24장).
4. 서버를 실행하는 것은 Uvicorn이다
FastAPI로 쓴 것은 "어떤 주소에 어떤 함수"라는 목록입니다. 실제로 포트를 열고 요청을 기다리는 일은 Uvicorn이라는 프로그램이 합니다.
$ python -m uvicorn app.main:app --reload --port 8000
| 조각 | 뜻 |
|---|---|
python -m uvicorn |
지금 켜져 있는 환경(myenv)의 Uvicorn을 실행한다 |
app.main:app |
app 폴더의 main.py 안에 있는 app이라는 변수 |
--reload |
코드를 고쳐 저장하면 서버가 자동으로 다시 시작된다. 개발할 때만 쓴다 |
--port 8000 |
8000번 포트에서 기다린다 |
포트(port) 는 한 컴퓨터 안에서 프로그램을 구분하는 번호입니다. 브라우저에서 http://localhost:8000을 열면 "내 컴퓨터(localhost)의 8000번에서 기다리는 프로그램"에게 요청이 갑니다.
app.main:app에서 앞의 app은 폴더 이름이고 뒤의 app은 변수 이름입니다. 이름이 같아서 헷갈리기 쉽습니다. 그리고 이 명령은 app 폴더가 보이는 자리, 곧 haru-market 폴더 맨 위에서 실행해야 합니다.
5. 서버가 시작될 때 한 번 일어나는 일
main.py의 맨 바깥에 이런 줄이 있습니다.
DEMO_CUSTOMER = "C003"
session = CustomerSession(DEMO_CUSTOMER)
supervisor = build_supervisor(session, verbose=False)
histories: dict[str, list] = {} # session_id → messages
함수 밖에 있으므로 서버가 시작될 때 한 번만 실행됩니다. Supervisor와 전문 에이전트 셋을 미리 만들어 두고, 요청이 올 때마다 그것을 다시 씁니다.
반대로 처리 기록은 미리 읽어 두지 않습니다. load_actions()와 pending_approvals()는 불릴 때마다 memory_store/actions.json을 다시 읽습니다. 그래서 채팅 쪽에서 방금 등록한 승인 요청이 상담원 쪽 주소에 곧바로 보입니다.
두 가지를 짚어 둡니다.
- 로그인 고객이 C003 한 명으로 고정되어 있습니다. 실제 로그인은 이 과정의 범위 밖입니다. 8장에서 만든
CustomerSession에 시연용 고객을 넣어 쓰는 것이고, 실제 서비스라면 로그인 정보에서 고객을 알아내 요청마다 세션을 만듭니다. verbose=False로 만들었으므로 22장의[위임 →]줄은 터미널에 찍히지 않습니다. 그 정보는 이제 화면으로 갑니다.
핵심 정리
- FastAPI는 함수 위에
@app.get/@app.post한 줄을 붙여 주소로 만듭니다. - 우리 서버의 엔드포인트는 여덟 개입니다. 고객 쪽 화면·점검·채팅 셋, 상담원 쪽 화면·처리 내역·승인 대기·승인/반려·티켓 목록 다섯.
POST /api/approvals/{action_id}는 23장과 같은decide()를 부릅니다. 승인 게이트가 웹에서는 버튼이 됩니다.- 들어오는 입력은 Pydantic 모델로 모양을 정해 문 앞에서 검사합니다. 틀리면 422로 돌려보내고 함수는 실행되지 않습니다.
- 서버를 실제로 띄우는 것은 Uvicorn입니다.
app.main:app은 "app/main.py의app변수"입니다. - Supervisor는 서버가 시작될 때 한 번 만들어 두고 계속 씁니다.