@@ -15,10 +15,11 @@ messaging │
1515 └──→ voice ──→ │
1616 │ │
1717 └──→ rag ────┴──→ storage (S3)
18- └──→ httpx ( Core API)
18+ └──→ core (httpx, Core internal API)
1919
2020config: 모두가 의존
2121model: 모두가 의존 (Pydantic 스키마)
22+ observability: chain 에 붙는 LangChain 콜백 — core 경유로 호출 로그 POST
2223```
2324
2425원칙:
@@ -45,22 +46,21 @@ settings = Settings() # singleton
4546```
4647
4748### ` model/ `
48- - RabbitMQ envelope 모델
49- - 도메인 객체 (` AnalyzedResume ` , ` QuestionPool ` , ` FollowUpResult ` )
50- - LLM 응답 schema (Pydantic) — ` OutputParser ` 에 사용
49+ - RabbitMQ envelope 모델 (` envelope.py ` )
50+ - 메시지 페이로드 — ` model/messages/ ` 하위에 도메인별 파일
51+ (` analyze.py ` , ` questions.py ` , ` followup.py ` , ` feedback.py ` , ` voice.py ` , ` tts.py ` , ` realtime.py ` )
52+ - LLM 응답 schema (Pydantic) — 체인의 구조화 출력 검증에 사용
53+ - 모든 페이로드 모델은 ` _config.camel_config() ` 로 wire 필드명을 camelCase 로 직렬화
5154
5255``` python
53- # model/messages.py
54- class ResumeAnalyzeRequest (BaseModel ):
55- resume_id: int
56- s3_key: str
57-
58- class ResumeAnalyzed (BaseModel ):
59- resume_id: int
60- summary: str
61- tech_stack: list[str ]
62- document_s3_key: str
63- embedding_chunk_count: int
56+ # model/messages/questions.py (발췌)
57+ class QuestionPoolCallbackPayload (BaseModel ):
58+ model_config = camel_config()
59+
60+ session_id: int
61+ kind: CallbackKind = " POOL"
62+ questions: list[GeneratedQuestion] = []
63+ status: GenerationStatus = " OK"
6464```
6565
6666### ` api/ `
@@ -70,11 +70,15 @@ class ResumeAnalyzed(BaseModel):
7070
7171### ` messaging/ `
7272- aio-pika consumer / publisher
73- - 큐별 consumer 함수 분리
73+ - 큐별 consumer 는 ` messaging/consumers/{name}_consumer.py ` 로 분리
74+ (resume/repository/web/cover_letter/questions/followup/feedback/voice/tts)
75+ - 조립·기동은 ` runner.py ` 의 ` MessagingRuntime ` (§3), 연결은 ` connection.py ` ,
76+ 콜백 발행은 ` publisher.py ` , 멱등은 ` idempotency.py ` (` LruIdempotencyStore ` ),
77+ RealTime 직접 발행은 ` progress.py ` (분석 진행)·` session_notify.py ` (델타/오디오)
7478- 모든 consumer는 envelope parsing → trace_context → 비즈니스 핸들러 호출 패턴
7579
7680``` python
77- # messaging/resume_consumer.py
81+ # messaging/consumers/ resume_consumer.py (패턴)
7882async def consume (message : AbstractIncomingMessage) -> None :
7983 async with message.process(requeue = False ):
8084 envelope = parse_envelope(message)
@@ -86,52 +90,65 @@ async def consume(message: AbstractIncomingMessage) -> None:
8690```
8791
8892### ` analyzer/ `
89- - use case 단위 (` resume_analyzer.py ` , ` repo_analyzer.py ` , ` feedback_generator.py ` )
90- - 외부 입력 → 내부 모듈 조합 → 결과 publish
93+ - 분석 use case 단위 (` resume_analyzer.py ` , ` repository_analyzer.py ` , ` web_resume_analyzer.py ` )
94+ - 소스 추출 추상화는 ` analyzer/sources/ ` (PDF/GitHub/웹/텍스트), 임베딩 인제스트는 ` _embedding_step.py `
95+ - 외부 입력 → 내부 모듈 조합 → 결과 publish. 피드백 생성은 analyzer 가 아니라
96+ ` messaging/consumers/feedback_consumer.py ` + ` chain/feedback_generation_chain.py ` 에 있다
9197- LLM 호출 자체는 ` chain/ ` 으로 위임
9298
9399### ` chain/ `
94- - LangChain 체인 정의
100+ - LangChain 체인 정의 (` document_analysis_chain.py ` , ` question_generation_chain.py ` ,
101+ ` followup_generation_chain.py ` , ` feedback_generation_chain.py ` , ` pdf_vision.py ` , ` sentence_split.py ` )
95102- ` chain/prompts/ ` 하위에 prompt 템플릿 (모든 프롬프트가 한 곳에)
96- - ` chain/parsers/ ` 출력 파서
103+ - 출력 파싱은 별도 모듈 없이 각 체인 안에서 Pydantic 구조화 출력으로 검증
97104
98105### ` rag/ `
99- - 청킹 (` splitter .py` )
100- - 임베딩 생성 (` embedder.py ` )
101- - 검색 어댑터 (Core API client ` pgvector_client .py` )
106+ - 청킹 (` chunker .py` — ` MarkdownChunker ` )
107+ - 임베딩 생성 (` embedder.py ` — provider 추상화 + Gemini/Mock 구현 )
108+ - 검색은 rag 모듈이 아니라 ` core/client .py: search_embeddings ` (Core ` POST /api/internal/embeddings/search ` )
102109
103- ### ` voice/ ` (Phase 2)
104- - ` voice/stt/ ` — interface + provider impls
105- - ` voice/tts/ `
106- - ` voice/analysis/ ` — WPM, filler, silence
110+ ### ` core/ `
111+ - Core 내부 API httpx 클라이언트 (` client.py ` ) — ` X-Internal-API-Key ` 인증
112+ - GitHub token 위임 · 임베딩 upsert/검색 · AI 호출 로그 기록 (엔드포인트 목록:
113+ [ ` /docs/messaging.md §10 ` ] ( ../../../docs/messaging.md ) )
114+
115+ ### ` voice/ `
116+ - ` voice/stt/ ` — interface + provider impls (배치 Whisper/Deepgram + 라이브 Deepgram Live)
117+ - ` voice/tts/ ` — provider 추상화 (Gateway/Gemini/OpenAI/Mock)
118+ - ` voice/analysis/ ` — WPM, filler, silence (` metrics.py ` )
107119
108120### ` storage/ `
109- - S3 client wrapper (` s3.py ` )
110- - key 생성 헬퍼 (` keys.py ` ) — [ ` /docs/storage.md §2 ` ] ( ../../../docs/storage.md ) 컨벤션 준수
121+ - ` ObjectStorage ` 추상화 (` base.py ` ) + ` s3.py ` / ` local_fs.py ` 구현, ` factory.py ` 로 토글
122+ - 객체 key 는 각 사용처에서 [ ` /docs/storage.md §2 ` ] ( ../../../docs/storage.md ) 컨벤션대로 조립 (전용 헬퍼 모듈 없음)
123+
124+ ### ` observability/ `
125+ - ` llm_logging_callback.py ` — LangChain ` AsyncCallbackHandler ` . 토큰/latency 측정 후
126+ ` core/client.py: record_ai_log ` 로 Core ` POST /api/internal/ai-logs ` (fire-and-forget)
111127
112128---
113129
114130## 3. 진입점
115131
116132### REST (FastAPI)
117133- ` api/health.py ` — 헬스체크
118- - ` api/internal/* ` — Core가 호출할 수 있는 동기 endpoint (필요 시 )
134+ - ` api/voice_stream.py ` — ` /internal/voice/stream ` WS (RealTime 이 프록시한 실시간 음성 답변, RT3 )
119135
120136### MQ Consumer
121- - ` messaging/runner.py ` (도입 예정) — 모든 consumer를 시작하는 entry
122- - ` main.py ` lifespan에서 자동 시작 (또는 별도 프로세스로 분리 검토)
137+ - ` messaging/runner.py ` — ` MessagingRuntime ` 이 의존성(체인·스토리지·Core 클라이언트·notifier)을
138+ 조립하고 모든 consumer 를 시작/종료하는 단일 entry
139+ - ` main.py ` lifespan 에서 ` runtime.start() ` / ` runtime.stop() ` 호출
123140
124141``` python
125- # main.py 의 lifespan
142+ # main.py 의 lifespan (실제 패턴)
126143@asynccontextmanager
127144async def lifespan (app : FastAPI):
128- connection = await connect_robust (settings.rabbitmq_url )
129- channel = await connection.channel()
130- await start_resume_consumer(channel)
131- await start_repo_consumer(channel )
132- await start_session_consumer(channel)
133- yield
134- await connection.close ()
145+ runtime = MessagingRuntime (settings)
146+ app.state.messaging = runtime
147+ try :
148+ await runtime.start( )
149+ yield
150+ finally :
151+ await runtime.stop ()
135152```
136153
137154---
@@ -165,25 +182,25 @@ class AnalysisError(Exception):
165182
166183이력서 분석 (US-09)을 예로 들면:
167184
168- 1 . ` model/messages.py ` 에 ` ResumeAnalyzeRequest ` , ` ResumeAnalyzed ` , ` ResumeFailed ` 정의
169- 2 . ` messaging/resume_consumer.py ` 구현 (envelope parse → handler 호출)
185+ 1 . ` model/messages/{name} .py ` 에 ` ResumeAnalyzeRequest ` , ` ResumeAnalyzed ` , ` ResumeFailed ` 정의
186+ 2 . ` messaging/consumers/ resume_consumer.py ` 구현 (envelope parse → handler 호출)
1701873 . ` analyzer/resume_analyzer.py ` 구현
171188 ``` python
172189 async def handle (req : ResumeAnalyzeRequest) -> None :
173- pdf_bytes = await s3 .get(req.s3_key)
190+ pdf_bytes = await storage .get(req.s3_key)
174191 text = extract_text(pdf_bytes)
175192 result = await resume_chain.ainvoke({" text" : text})
176193 md_key = f " analyzed/resume/ { req.resume_id} /summary.md "
177- await s3 .put(md_key, result.markdown)
178- chunks = split(result.markdown)
194+ await storage .put(md_key, result.markdown)
195+ chunks = chunker. split(result.markdown)
179196 embeddings = await embedder.embed(chunks)
180- await pgvector_client.upsert( req.resume_id, chunks, embeddings )
197+ await core_client.upsert_embeddings( document_id = req.analyzed_document_id, ... )
181198 await publisher.publish_callback(ResumeAnalyzed(... ))
182199 ```
183- 4 . ` chain/resume_analyzer_chain .py ` (prompt + LLM + parser )
200+ 4 . ` chain/{name}_chain .py ` (prompt + LLM + Pydantic 구조화 출력 )
1842015 . 단위 테스트 (mock LLM)
1852026 . 통합 테스트 (Testcontainer RabbitMQ + MinIO)
186- 7 . main .py lifespan에 consumer 등록
203+ 7 . ` messaging/runner .py` 의 ` MessagingRuntime ` 에 consumer 등록
187204
188205---
189206
0 commit comments