Skip to content

feat: 멀티턴 챗봇 API 구현 - #27

Merged
pearseona merged 1 commit into
developfrom
feat/23-multiturn-chat-api
Aug 2, 2026
Merged

feat: 멀티턴 챗봇 API 구현#27
pearseona merged 1 commit into
developfrom
feat/23-multiturn-chat-api

Conversation

@kite-pp

@kite-pp kite-pp commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

📝 개요

분석 컨텍스트와 대화 히스토리를 받아 Gemini 기반 금융사기 대응 상담을
제공하는 POST /chat 엔드포인트를 추가. Stateless로 동작하며, role
검증/빈 메시지 차단, Gemini 장애/timeout 처리, 민감정보 요구 금지
가이드라인을 시스템 프롬프트에 반영.

🔗 관련 이슈

🎯 주요 변경 사항

  • app/chat/schemas.py: ChatRequest(analysisContext, messages) / ChatResponse DTO 정의. role은 user/assistant만 허용, content/explanation 공백 차단, messages 빈 리스트 차단, extra="forbid"로 미정의 필드 차단
  • app/chat/prompts.py: 위험 점수·등급·카테고리·분석 설명·탐지 근거를 주입하는 시스템 프롬프트 템플릿. 지급정지·신고(112/118/1332)·금융기관 재확인 중심 대응 가이드 + 민감정보(비밀번호/OTP/전체 계좌번호) 요구 금지 원칙 + 컨텍스트를 지시가 아닌 참고 데이터로 격리하는 최소 프롬프트 인젝션 방어 원칙 포함
  • app/chat/service.py: ChatService — 대화 히스토리를 Gemini contents 포맷으로 변환(role: assistant→model 매핑), GeminiClient 재사용, Rate Limit/HTTP Error/Timeout/Parse Error/Missing Key를 ChatServiceError로 통일 처리, MOCK_SECURITY_API 환경변수 기반 mock 모드 지원
  • app/chat/router.py: POST /chat 엔드포인트, 서비스 실패 시 502 + 안내 메시지로 변환 (대화 내용/에러 상세는 로그·응답에 노출하지 않음)
  • app/main.py: chat.router/api prefix로 등록
  • tests/chat/: 스키마 검증, 프롬프트 생성, 서비스(mock 모드·API 키 누락·role 매핑·rate limit·timeout·빈 응답), 라우터 E2E(422/502/200, OpenAPI 노출) 테스트 23건 추가
  • [후속 수정] Spring 팀의 실제 계약에 맞춰 AnalysisContext 필드명/구조 정렬: riskGrade→riskLevel, phishingType→category, summary→explanation, indicatorsChatRequest 최상위에서 AnalysisContext 내부로 이동하고 list[str]list[Indicator({type, description})]로 구조 변경 (category/Indicator.type은 아직 확정된 enum이 없어 자유 문자열로 수용)

📸 사진

실제 Gemini API 라이브 호출 + Swagger UI(/docs)에서 직접 실행하여 동작 확인 (멀티턴 문맥 유지, 지급정지·신고 채널 안내, 민감정보 요구 거부 응답 확인). 별도 스크린샷 없음.

✅ PR 체크리스트

  • 관련 이슈를 연결했습니다.
  • 구현 범위와 변경 이유를 설명했습니다.
  • 로컬 테스트(uvicorn 구동 또는 테스트 코드)를 통과했습니다.
  • API 변경 사항이 있다면 Swagger / API 명세에 반영했습니다.
  • 민감 정보(API Key, 시크릿 키 등)가 코드·로그·테스트 데이터에 포함되지 않았습니다.
  • 프론트엔드 또는 메인 백엔드(Spring)에 영향을 주는 응답 스키마 또는 Enum 변경이 있다면 팀에 공유했습니다.
  • 병합(Merge) 전 작업 브랜치를 삭제하지 않았습니다.

분석 컨텍스트와 대화 히스토리를 받아 Gemini 기반 금융사기 대응 상담을
제공하는 POST /chat 엔드포인트를 추가. Stateless로 동작하며, role
검증/빈 메시지 차단, Gemini 장애/timeout 처리, 민감정보 요구 금지
가이드라인을 시스템 프롬프트에 반영.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds validated chat schemas, a Korean safety prompt, Gemini-backed response handling, and a FastAPI /api/chat endpoint. The change includes mock support, service error mapping, application wiring, and tests for schemas, prompts, service behavior, and API responses.

Changes

Chat feature

Layer / File(s) Summary
Chat contracts and safety prompt
app/chat/schemas.py, app/chat/prompts.py, tests/chat/test_schemas.py, tests/chat/test_prompts.py
Defines validated chat request and response models. Builds a Korean system prompt from analysis context and indicators.
Gemini response service
app/chat/service.py, tests/chat/test_service.py
Adds mock and Gemini execution paths, converts chat roles, extracts generated text, and translates Gemini failures into ChatServiceError.
Chat endpoint wiring and validation
app/chat/router.py, app/main.py, tests/chat/test_router.py
Registers /api/chat, delegates requests to ChatService, maps service errors to HTTP 502, and tests validation and OpenAPI exposure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ChatRouter
  participant ChatService
  participant Gemini
  Client->>ChatRouter: POST /api/chat with ChatRequest
  ChatRouter->>ChatService: get_response(request)
  ChatService->>Gemini: Send system prompt and chat history
  Gemini-->>ChatService: Return generated response
  ChatService-->>ChatRouter: Return ChatResponse
  ChatRouter-->>Client: Return message
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: implementing a multi-turn chatbot API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/23-multiturn-chat-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
app/chat/schemas.py (1)

30-49: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add size bounds before payloads reach the paid Gemini API.

ChatRequest.messages (Line 49) has no max_length. ChatMessage.content (Line 34) and AnalysisContext.summary (Line 20) have no max_length either. A caller can send an arbitrarily long conversation history or arbitrarily long text fields. This payload flows unmodified into _build_contents and build_system_prompt in app/chat/service.py, so an unbounded request inflates the Gemini call size and cost.

Add max_length to messages, indicators, content, and summary.

♻️ Example bounds
     role: ChatRole
-    content: str
+    content: str = Field(..., max_length=4000)
-    indicators: list[str] = Field(default_factory=list, description="탐지 근거 목록")
-    messages: list[ChatMessage] = Field(..., min_length=1)
+    indicators: list[str] = Field(default_factory=list, max_length=50, description="탐지 근거 목록")
+    messages: list[ChatMessage] = Field(..., min_length=1, max_length=50)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/chat/schemas.py` around lines 30 - 49, Bound request payload sizes in the
Pydantic schemas: add appropriate max_length constraints to ChatRequest.messages
and indicators, ChatMessage.content, and AnalysisContext.summary. Preserve the
existing defaults, requiredness, and blank-content validation while ensuring
oversized conversation and text fields are rejected before _build_contents or
build_system_prompt.
tests/chat/test_service.py (1)

74-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the generic HTTP-error and parse-error branches.

Tests cover the 429 rate-limit branch (Lines 91-105) and the timeout branch (Lines 108-119) in app/chat/service.py. The generic httpx.HTTPStatusError branch (non-429 status, mapped to "HTTP Error") and the KeyError/IndexError branch (mapped to "Parse Error" when a candidate is missing content/parts/text) have no test. Add a test for each to lock in the mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/chat/test_service.py` around lines 74 - 119, Add two async tests
alongside the existing ChatService error tests: one patching
GeminiClient.generate to raise a non-429 httpx.HTTPStatusError and assert
ChatService.get_response raises ChatServiceError matching “HTTP Error”, and
another returning a malformed candidate that triggers the KeyError/IndexError
parsing path, asserting the error matches “Parse Error”.
app/chat/service.py (1)

15-19: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Align Gemini configuration with the existing settings pattern; verify the -latest alias fits production use.

GEMINI_API_KEY and GEMINI_MODEL are read via os.getenv here, with a separate load_dotenv() call. app/infrastructure/gemini/client.py already reads Gemini-related configuration (settings.GEMINI_TIMEOUT_SECONDS, settings.EXTERNAL_API_MAX_RETRIES) from a central settings object. Move these two values into settings to avoid a second .env load path and keep Gemini configuration in one place.

Separately, the default model gemini-flash-latest is a Google-maintained alias. Per Google's model documentation, this alias "Points to an experimental model which will typically be not be suitable for production use and come with more restrictive rate limits." Confirm this default is intentional for this financial-fraud chatbot, since the alias can be hot-swapped by Google and is not guaranteed production-stable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/chat/service.py` around lines 15 - 19, Move Gemini API key and model
configuration from the module-level os.getenv calls into the centralized
settings object, and update the API_URL construction to use those settings
values while removing the redundant dotenv loading path. Review the default
GEMINI_MODEL value and replace the -latest alias with an explicitly supported
production-stable model unless the project intentionally documents and accepts
the alias’s experimental behavior.
app/chat/router.py (1)

10-26: 🔒 Security & Privacy | 🔵 Trivial

Verify network exposure and rate limiting for /api/chat.

This endpoint calls a billed external API (Gemini) on every request and has no authentication or rate-limiting dependency in this router. Confirm whether this FastAPI service is reachable only from the trusted Spring Boot backend, as implied by the AnalysisContext docstring in app/chat/schemas.py, or whether it is directly reachable by end users. If it is directly reachable, add authentication and per-client rate limiting to bound Gemini cost exposure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/chat/router.py` around lines 10 - 26, Verify whether the `chat` endpoint
is restricted to the trusted Spring Boot backend as documented by
`AnalysisContext`; if it is directly reachable by end users, add the project’s
authentication and per-client rate-limiting dependencies to the `chat` route,
preserving its existing request and response behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/chat/router.py`:
- Around line 10-26: Verify whether the `chat` endpoint is restricted to the
trusted Spring Boot backend as documented by `AnalysisContext`; if it is
directly reachable by end users, add the project’s authentication and per-client
rate-limiting dependencies to the `chat` route, preserving its existing request
and response behavior.

In `@app/chat/schemas.py`:
- Around line 30-49: Bound request payload sizes in the Pydantic schemas: add
appropriate max_length constraints to ChatRequest.messages and indicators,
ChatMessage.content, and AnalysisContext.summary. Preserve the existing
defaults, requiredness, and blank-content validation while ensuring oversized
conversation and text fields are rejected before _build_contents or
build_system_prompt.

In `@app/chat/service.py`:
- Around line 15-19: Move Gemini API key and model configuration from the
module-level os.getenv calls into the centralized settings object, and update
the API_URL construction to use those settings values while removing the
redundant dotenv loading path. Review the default GEMINI_MODEL value and replace
the -latest alias with an explicitly supported production-stable model unless
the project intentionally documents and accepts the alias’s experimental
behavior.

In `@tests/chat/test_service.py`:
- Around line 74-119: Add two async tests alongside the existing ChatService
error tests: one patching GeminiClient.generate to raise a non-429
httpx.HTTPStatusError and assert ChatService.get_response raises
ChatServiceError matching “HTTP Error”, and another returning a malformed
candidate that triggers the KeyError/IndexError parsing path, asserting the
error matches “Parse Error”.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0aded7b-9b34-4e8b-ac71-e936c01ad779

📥 Commits

Reviewing files that changed from the base of the PR and between ea97e9b and 178176d.

📒 Files selected for processing (11)
  • app/chat/__init__.py
  • app/chat/prompts.py
  • app/chat/router.py
  • app/chat/schemas.py
  • app/chat/service.py
  • app/main.py
  • tests/chat/__init__.py
  • tests/chat/test_prompts.py
  • tests/chat/test_router.py
  • tests/chat/test_schemas.py
  • tests/chat/test_service.py

@pearseona
pearseona merged commit 54e8e6b into develop Aug 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants