Skip to content

Commit 7142a3e

Browse files
authored
Merge pull request #34 from Team-StackUp/feature/sprint1-leftovers-us02-03-04-08
Feature/sprint1 leftovers us02 03 04 08
2 parents 6411e5e + a437eef commit 7142a3e

87 files changed

Lines changed: 3813 additions & 446 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ S3_REGION=us-east-1
2727
LLM_API_KEY=
2828
LLM_BASE_URL=https://factchat-cloud.mindlogic.ai/v1/gateway
2929
LLM_PRO_MODEL=gemini-3.1-pro-preview
30+
LLM_FLASH_MODEL=gemini-3.1-flash-lite-preview
31+
LLM_FLASH_TEMPERATURE=0.4
32+
LLM_FLASH_MAX_TOKENS=512
3033

3134
# RealTime server
3235
REALTIME_PORT=38020

SPRINT2_PLAN.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Sprint 2 — 텍스트 면접 E2E 작업 계획
2+
3+
> 범위: **US-13 ~ US-20**. 세션 생성 → 첫 질문 → 텍스트 답변 → 꼬리질문 → 세션 종료까지의 골든 패스 완성. 피드백 리포트(US-24)는 Sprint 3로 분리.
4+
5+
작성일: 2026-05-29 / 기준 브랜치: `main` (Sprint 1 #30까지 머지 완료)
6+
7+
---
8+
9+
## 1. Sprint 1 정리
10+
11+
- 열린 PR 없음. Sprint 1 마지막 PR(#30, Sprint1 백엔드 및 AI서버 API 연동)이 5/28에 머지됨.
12+
- 머지 완료: auth(GitHub OAuth + JWT), resume 업로드/분석, GitHub repo 등록/분석, document 메타·요약·임베딩 upsert, SSE stream-token + 분석 상태 푸시, RabbitMQ DLX·DLQ + 재시도 정책(US-28), `.env` 자동 sync 워크플로.
13+
- **Sprint 2 진입 준비 완료 상태.**
14+
15+
---
16+
17+
## 2. 현재 면접 도메인 진척도
18+
19+
| 레이어 | 상태 |
20+
|---|---|
21+
| **DB** | 7개 테이블(`interview_sessions`, `interview_messages`, `session_contexts`, `message_voice_analyses`, `session_feedbacks` 등) + 모든 ENUM 이미 마이그레이션됨 |
22+
| **Backend `session/`** | [`InterviewSession.java`](backend/src/main/java/com/stackup/stackup/session/domain/InterviewSession.java), [`InterviewMessage.java`](backend/src/main/java/com/stackup/stackup/session/domain/InterviewMessage.java), Repository, Enum 까지만. `application/` · `presentation/` · `infrastructure/` 패키지 **부재** |
23+
| **AI 서버** | `analyze.resume / .repository / .web` 컨슈머만 본 구현. `generate.questions` · `generate.followup` 큐는 정의만, **코드 없음** ([`ai/CLAUDE.md §16`](ai/CLAUDE.md)) |
24+
| **Frontend** | [`features/interview/index.ts`](frontend/src/features/interview/index.ts), [`pages/Interview/index.ts`](frontend/src/pages/Interview/index.ts), [`features/feedback/index.ts`](frontend/src/features/feedback/index.ts), [`pages/History/index.ts`](frontend/src/pages/History/index.ts) 모두 1줄 빈 export 스캐폴딩 |
25+
| **RealTime** | SSE + RabbitMQ `realtime.session.*` bridge 동작 중. Core 가 publish 만 하면 그대로 fan-out |
26+
| **메시징 스펙** | [`docs/messaging.md §5.6~5.9`](docs/messaging.md), envelope · 큐 · DLQ 모두 확정 |
27+
28+
데이터·흐름·SSE 이벤트 카탈로그 모두 사양화 완료 ([`docs/data-flow.md §3`](docs/data-flow.md), [`docs/event-stream.md §3.3~3.4`](docs/event-stream.md)) — **사양은 다 있고 코드만 채우면 됨.**
29+
30+
---
31+
32+
## 3. 작업 분해 — 트랙별
33+
34+
### 트랙 A — Backend session 도메인
35+
36+
세션 생성부터 종료까지의 트랜잭션 · 메시지 발행 · SSE push 전부.
37+
38+
#### A-1. Application / Presentation 계층 신설
39+
- `session/application/SessionService`
40+
- `create(cmd)``interview_sessions` INSERT(`status=READY`) + `session_contexts` INSERT
41+
- `start(id)``READY → IN_PROGRESS`, `started_at` 세팅
42+
- `submitAnswer(id, content)` — INTERVIEWEE 메시지 INSERT + `generate.followup` 발행
43+
- `end(id, reason)``IN_PROGRESS → COMPLETED`, `ended_at`
44+
- `get(id)`, `list(userId, pageable)`, `getMessages(id)`
45+
- `session/application/dto/``SessionCreateCommand`, `SessionResult`, `MessageSubmitCommand`, `MessageResult`
46+
- `session/presentation/SessionController`
47+
- `POST /api/sessions`
48+
- `POST /api/sessions/{id}/start`
49+
- `POST /api/sessions/{id}/messages`
50+
- `POST /api/sessions/{id}/end`
51+
- `GET /api/sessions/{id}`
52+
- `GET /api/sessions`
53+
- `GET /api/sessions/{id}/messages`
54+
55+
#### A-2. RabbitMQ 발행 (`session/infrastructure/SessionEventPublisher`)
56+
- 세션 생성 commit 후 `generate.questions` 발행 (`@TransactionalEventListener(AFTER_COMMIT)`, [`docs/messaging.md §3`](docs/messaging.md))
57+
- 답변 INSERT commit 후 `generate.followup` 발행
58+
59+
#### A-3. RabbitMQ 컨슈머 (`session/infrastructure/SessionCallbackHandler`)
60+
`core.callback.questions` 구독, `payload.kind`로 분기.
61+
- `POOL` — 질문 풀 저장 + 첫 질문 `interview_messages` INSERT + SSE `session.message` push + (옵션) `realtime.session.notify` 발행
62+
- `FOLLOWUP` — INTERVIEWER 메시지 INSERT(`parent_message_id` 매핑) + SSE push + `total_question_count >= max_questions` 시 자동 종료 트리거
63+
64+
#### A-4. 세션 종료 정책
65+
- 백엔드 컷: `total_question_count >= max_questions` 또는 timeout
66+
- `interview_sessions.status=COMPLETED`, `ended_at`, SSE `session.state` push
67+
68+
#### A-5. ArchUnit 룰 위반 점검 ([`backend/CLAUDE.md §16`](backend/CLAUDE.md))
69+
70+
#### A-6. 테스트
71+
- `SessionServiceTest` (Mockito)
72+
- `SessionControllerTest` (`@WebMvcTest`)
73+
- 컨슈머 통합 테스트 (Testcontainer RabbitMQ + PG)
74+
75+
---
76+
77+
### 트랙 B — AI 서버 질문 / 꼬리질문
78+
79+
[`ai/CLAUDE.md §14`](ai/CLAUDE.md) 절차 따라.
80+
81+
#### B-1. 메시지 모델
82+
- `model/messages/generate.py``GenerateQuestionsPayload`, `GenerateFollowupPayload`, `CallbackQuestionsPayload`
83+
84+
#### B-2. 컨슈머 2종
85+
- `messaging/consumers/questions_consumer.py`
86+
- `messaging/consumers/followup_consumer.py`
87+
- 멱등(messageId) 패턴은 기존 컨슈머 동일
88+
89+
#### B-3. 질문 풀 생성 체인 (`chain/question_pool_chain.py`)
90+
- 입력: `interviewType`, `jobCategory`, `documentIds`, `maxQuestions`
91+
- RAG: Core 내부 API `POST /api/internal/embeddings/search` 로 컨텍스트 chunk fetch → 프롬프트 주입
92+
- LLM: **Pro 모델** (Gemini 3.1 Pro), Pydantic 출력 파서로 `[{ category, question }]` 강제
93+
- 프롬프트: `chain/prompts/question_pool.py` 신규
94+
95+
#### B-4. 꼬리질문 체인 (`chain/followup_chain.py`)
96+
- 입력: 직전 질문 / 답변, sessionId
97+
- **Flash 모델** + 답변 평가 스키마(`specificity`, `logic`, `structure`)
98+
- SLA < 3 초 — 응답 토큰 cap, streaming 미사용
99+
100+
#### B-5. 임베딩 provider
101+
- **운영/개발: [`GeminiEmbeddingProvider`](ai/src/ai_server/rag/embedder.py) 사용** (이미 구현되어 있음). 설정에서 default 로 스위치
102+
- **테스트: `MockEmbeddingProvider`** 유지 (단위/통합 테스트 결정론 확보)
103+
- [`ai/CLAUDE.md §16`](ai/CLAUDE.md) 의 "MockEmbeddingProvider default" 기술은 본 스프린트에서 갱신
104+
105+
#### B-6. 콜백 발행
106+
- `callback.questions` (`kind=POOL` / `FOLLOWUP`)
107+
108+
#### B-7. `ai_request_logs`
109+
- Core API 또는 자체 publish 로 input/output 토큰 · latency 기록
110+
111+
#### B-8. 테스트
112+
- `FakeListLLM` mock 으로 chain 단위
113+
- Testcontainer 로 컨슈머 통합
114+
115+
---
116+
117+
### 트랙 C — Frontend Interview UX
118+
119+
[`frontend/CLAUDE.md §5`](frontend/CLAUDE.md) 라우트 가이드 기준.
120+
121+
#### C-1. 라우터 등록
122+
- `/sessions/new`, `/sessions/:id`, `/history`, `/history/:id`
123+
- `RequireAuth` wrap
124+
125+
#### C-2. `features/interview/`
126+
- `api/interview.ts` — create / start / submitAnswer / end / get / list / getMessages
127+
- `model/types.ts``Session`, `InterviewMessage`, ENUM 미러 ([`docs/glossary.md`](docs/glossary.md))
128+
- `model/useSession.ts`, `useSessionMessages.ts`, `useSubmitAnswer.ts`
129+
- `ui/SessionConfigForm.tsx` — mode / type / jobCategory / maxQuestions / maxDuration + `analyzed_documents` 멀티셀렉트(컨텍스트 문서)
130+
- `ui/MessageList.tsx`, `ui/AnswerInput.tsx`, `ui/SessionControls.tsx` (start, end, 진행률)
131+
- `ui/QuestionPendingState.tsx` — AI 풀 생성 대기용 4-state pending
132+
133+
#### C-3. SSE 연동
134+
- 기존 [`useEventStream.ts`](frontend/src/shared/hooks/useEventStream.ts)`session.message` · `session.state` 핸들러 추가
135+
- `GET /api/stream/sessions/{sessionId}` — Sprint 1 의 stream-token API 재사용
136+
137+
#### C-4. `pages/Interview/`
138+
- `NewSessionPage` (config form 만)
139+
- `InterviewPage` (`:id`, AsyncBoundary 로 감싸고 진행 중 / 종료 분기)
140+
141+
#### C-5. `pages/History/`
142+
- 세션 리스트(상태 뱃지) · 세션 상세(메시지 시퀀스)
143+
144+
#### C-6. 테스트
145+
- Vitest 로 hook
146+
- Playwright 로 "세션 생성 → 답변 3회 → 종료" 골든 패스
147+
148+
---
149+
150+
### 트랙 D — RealTime 서버
151+
152+
현재 `q.realtime.session.notify` 컨슈머가 SSE 로 fan-out 됨. **Core 가 publish 만 시작하면 별도 작업 없음.** 다만 확인할 것:
153+
154+
- Core 가 `q.realtime.session.notify` 로 publish vs 자기 인메모리 SSE 둘 중 어디로 보낼지 정책 통일 ([`docs/event-stream.md §6`](docs/event-stream.md)) — Phase 1 은 Core 인메모리로도 충분. RealTime 경유로 가면 멀티 인스턴스 대비 가능. **Sprint 2 에선 인메모리 + 별도 publish 둘 다 가능** — 결정 필요
155+
- session 권한 검증(`/api/stream/sessions/:id` 본인 세션 검사) 이 이미 들어가 있는지 확인
156+
157+
---
158+
159+
## 4. 결정 사항 / 미결정 사항
160+
161+
### 4.1 확정된 결정 (2026-05-29)
162+
163+
| # | 항목 | 결정 | 근거 |
164+
|---|---|---|---|
165+
| 1 | **임베딩 provider** | 운영 / 개발은 [`GeminiEmbeddingProvider`](ai/src/ai_server/rag/embedder.py) 사용 (이미 구현됨), 테스트는 `MockEmbeddingProvider` 유지 | Mock 만 쓰면 RAG 검색이 noise, 신규 구현체 도입 불필요 |
166+
| 2 | **첫 질문 push 방식** | Core 가 `callback.questions (POOL)` 수신 즉시 첫 질문을 `interview_messages` INSERT + SSE `session.message` push | round-trip · race 회피, SSE 인프라 재사용 |
167+
| 3 | **SSE 전송 경로** | Phase 1 은 Core 인메모리 단일화 | RT2 (분석 상태) 와 경로 통일, 디버깅 비용 ↓. RealTime 서버는 멀티 인스턴스 / 음성 단계에서 본격 활용 |
168+
| 4 | **답변 멱등키** | `POST /api/sessions/{id}/messages` 한정으로 `Idempotency-Key` 헤더 도입 (Frontend UUID 발급 → Core 가 `processed_messages` 테이블에 24h 캐시) | `(session_id, sequence_number) UNIQUE` 깨짐 방지, SSE 재연결 + 자동 재시도 시 중복 INSERT 차단 |
169+
170+
### 4.2 미결정 사항 (Sprint 2 진입 전 픽스 필요)
171+
172+
| # | 항목 | 책임 | 메모 |
173+
|---|---|---|---|
174+
| 5 | 세션 종료 후 피드백 (US-24) Sprint 3 로 미루는지 확정 | PO | 본 계획은 분리를 가정. Sprint 2 종료 시점에 `/sessions/:id/feedback` placeholder 만 두는 안 검토 |
175+
| 6 | Flyway 추가 마이그레이션 필요 여부 | Core | 핵심 결정: **질문 풀 저장 위치**`interview_sessions.question_pool JSONB` 컬럼 추가 vs 별도 테이블 vs Core 인메모리. 컬럼 추가 시 마이그레이션 1 개 필요 |
176+
177+
---
178+
179+
## 5. 권장 진행 순서
180+
181+
1. **미결정 사항 5·6 회의로 픽스** (10 분)
182+
2. **메시지 스키마 정합성 PR** — envelope 모델만 Core / AI 양쪽에 추가 → 트랙 A · B 의 unblock
183+
3. **트랙 A · B · C 병렬 진행** (각각 PR 1~2 개)
184+
4. **통합 시나리오 테스트** — 세션 생성 → 첫 질문 → 답변 → 꼬리질문 → 종료 골든 패스 E2E
185+
5. **트랙 D 정책 통일** + 멀티 인스턴스 대비 (선택)
186+
187+
Sprint 1 PR 들이 평균 1~2 개 도메인씩 끊어 머지된 패턴을 보면, Sprint 2 도 트랙별 PR 2~3 개씩(총 8~10 PR)으로 끊는 게 자연스러움.
188+
189+
---
190+
191+
## 6. Definition of Done
192+
193+
- [ ] `/sessions/new` 에서 컨텍스트 문서를 골라 세션 생성 가능
194+
- [ ] 세션 생성 후 첫 질문이 SSE 로 도착
195+
- [ ] 답변 제출 → < 3 초 내 꼬리질문 push
196+
- [ ] `maxQuestions` 도달 시 자동 종료, `/history` 에 COMPLETED 표시
197+
- [ ] `/history/:id` 에서 전체 메시지 트리 조회 가능
198+
- [ ] DLQ 격리 정책 ([`docs/messaging.md §6`](docs/messaging.md)) 가 generate.* 큐에도 적용 확인
199+
- [ ] `ai_request_logs` 에 질문 풀 / 꼬리질문 호출 기록
200+
- [ ] 골든 패스 E2E 테스트 통과

ai/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ LLM_API_KEY=
1818
LLM_BASE_URL=https://factchat-cloud.mindlogic.ai/v1/gateway
1919
LLM_PRO_MODEL=gemini-3.1-pro-preview
2020
LLM_PRO_TEMPERATURE=0.2
21+
LLM_FLASH_MODEL=gemini-3.1-flash-lite-preview
22+
LLM_FLASH_TEMPERATURE=0.4
23+
LLM_FLASH_MAX_TOKENS=512
2124

2225
# Github 관련
2326
CORE_INTERNAL_BASE_URL=http://localhost:38010

ai/CLAUDE.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ ai/
9393
| `ai.analyze.resume` | `analyze.resume` | 본 구현 (PDF → MD) |
9494
| `ai.analyze.repository` | `analyze.repository` | 본 구현 (GitHub README + tree + 소스 sampling) |
9595
| `ai.analyze.web` | `analyze.web` | 본 구현 (URL → trafilatura) |
96-
| `ai.generate.questions` | `generate.questions` | 큐만, 코드 미구현 |
97-
| `ai.generate.followup` | `generate.followup` | 큐만, 코드 미구현 |
96+
| `ai.generate.questions` | `generate.questions` | 본 구현 (Pro 모델, 질문 풀 생성, US-18) |
97+
| `ai.generate.followup` | `generate.followup` | 본 구현 (Flash 모델, 답변 평가+꼬리질문, US-19) |
9898

9999
콜백 발행: `ai.callback.{type}` 익스체인지.
100100
상세 envelope/스키마/재시도: [`/docs/messaging.md`](../docs/messaging.md).
@@ -310,19 +310,20 @@ docker run --env-file .env -p 8000:8000 stackup-ai
310310
## 16. 현재 상태 (2026-05 기준)
311311

312312
- FastAPI 부트스트랩 + 헬스체크
313-
- RabbitMQ consumer `ai.analyze.resume` 본 구현:
314-
- PDF 텍스트 추출 (`analyzer/sources/pdf.py`, pypdf)
313+
- 분석 consumer 본 구현 — `analyze.resume` / `analyze.repository` / `analyze.web`:
314+
- PDF·GitHub Repo·웹 URL 소스 추출 추상화 (`analyzer/sources/`)
315315
- LLM 분석 (`chain/document_analysis_chain.py`, Gemini Pro + Pydantic 출력 파서)
316316
- 분석 MD를 스토리지에 저장
317317
- `callback.analysis` 발행 (status `ANALYZED` / `FAILED`, retriable 플래그 포함)
318-
- **스토리지 추상화 도입** (`storage/`): `ObjectStorage` 인터페이스 + 구현체 두 개.
319-
- `S3Storage` (기본) — boto3 + `asyncio.to_thread`. MinIO·AWS S3 모두 호환. `S3_ENDPOINT_URL`만 바꿔 swap.
320-
- `LocalFilesystemStorage` (dev/test 전용) — aiofiles + traversal 방어.
321-
- `STORAGE_BACKEND=s3|local` 환경변수 한 줄로 전환.
322-
- **소스 추출 추상화 도입** (`analyzer/sources/`): `SourceExtractor` 인터페이스 + `PdfSourceExtractor`.
323-
GitHub repo / 웹 이력서 추출기는 동일 인터페이스로 후속 PR에서 추가 예정.
324-
- 임베딩·청킹·pgvector upsert는 미구현 → 후속 PR. `embedding_chunk_count`는 0으로 발행.
325-
- `analyze.repository` / `generate.questions` / `generate.followup` consumer는 큐 정의만, 코드 없음
318+
- 면접 consumer 본 구현 — `generate.questions` (US-18) / `generate.followup` (US-19):
319+
- 질문 풀 생성 (Pro 모델, `chain/question_generation_chain.py`)
320+
- 꼬리질문 + 답변 평가 (Flash 모델, `chain/followup_generation_chain.py`)
321+
- 콜백: `callback.questions` (`kind=POOL|FOLLOWUP`)
322+
- **임베딩 본 구현** (`rag/`): `MarkdownChunker` + `GeminiEmbeddingProvider` (1536d, `gemini-embedding-001`).
323+
운영/개발 default 는 gemini, 테스트는 `MockEmbeddingProvider`.
324+
- **스토리지 추상화** (`storage/`): `S3Storage`(기본) / `LocalFilesystemStorage`. `STORAGE_BACKEND` 토글.
325+
- **LLM 호출 로깅 본 구현** (`observability/llm_logging_callback.py`, US-30):
326+
LangChain `AsyncCallbackHandler` 가 토큰/latency 측정 → Core `/api/internal/ai-logs` POST.
326327
- 음성 모듈은 Phase 2
327328

328329
각 도입 시 본 문서 갱신.

ai/src/ai_server/chain/document_analysis_chain.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from ai_server.analyzer.sources.base import SourceType
1111
from ai_server.chain.prompts.document_analysis import HUMAN_PROMPT, SYSTEM_PROMPT
1212
from ai_server.config.settings import Settings
13+
from ai_server.core.client import CoreClient
14+
from ai_server.observability.llm_logging_callback import CoreAiLogCallback
1315

1416

1517
class DocumentAnalysisResult(BaseModel):
@@ -42,7 +44,7 @@ async def analyze(
4244

4345

4446
# 프롬프트 -> LLM -> 파서 하나로 묶어서 처리함
45-
def build_document_analysis_chain(settings: Settings) -> Runnable:
47+
def build_document_analysis_chain(settings: Settings, core_client: CoreClient | None = None) -> Runnable:
4648
from langchain_openai import ChatOpenAI
4749

4850
parser = PydanticOutputParser(pydantic_object=DocumentAnalysisResult)
@@ -53,10 +55,19 @@ def build_document_analysis_chain(settings: Settings) -> Runnable:
5355
]
5456
).partial(format_instructions=parser.get_format_instructions())
5557

58+
callbacks = []
59+
if core_client is not None:
60+
callbacks.append(CoreAiLogCallback(
61+
core_client=core_client,
62+
request_type="analyze.document",
63+
default_model=settings.llm_pro_model,
64+
))
65+
5666
llm = ChatOpenAI(
5767
model=settings.llm_pro_model,
5868
temperature=settings.llm_pro_temperature,
5969
api_key=settings.llm_api_key or None,
6070
base_url=settings.llm_base_url,
71+
callbacks=callbacks,
6172
)
6273
return prompt | llm | parser
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
from typing import Protocol
4+
5+
from langchain_core.output_parsers import PydanticOutputParser
6+
from langchain_core.prompts import ChatPromptTemplate
7+
from langchain_core.runnables import Runnable
8+
from pydantic import BaseModel, Field
9+
10+
from ai_server.chain.prompts.followup_generation import HUMAN_PROMPT, SYSTEM_PROMPT
11+
from ai_server.config.settings import Settings
12+
from ai_server.core.client import CoreClient
13+
from ai_server.model.messages.followup import AnswerEvaluation
14+
from ai_server.observability.llm_logging_callback import CoreAiLogCallback
15+
16+
17+
class FollowupResult(BaseModel):
18+
followup_question: str = Field(..., description="한국어 꼬리질문 1개")
19+
answer_evaluation: AnswerEvaluation
20+
21+
22+
class FollowupGenerator(Protocol):
23+
async def generate(
24+
self,
25+
*,
26+
job_category: str,
27+
interview_type: str,
28+
previous_question: str,
29+
answer_text: str,
30+
) -> FollowupResult: ...
31+
32+
33+
class LlmFollowupGenerator:
34+
def __init__(self, chain: Runnable) -> None:
35+
self._chain = chain
36+
37+
async def generate(
38+
self,
39+
*,
40+
job_category: str,
41+
interview_type: str,
42+
previous_question: str,
43+
answer_text: str,
44+
) -> FollowupResult:
45+
result = await self._chain.ainvoke(
46+
{
47+
"job_category": job_category,
48+
"interview_type": interview_type,
49+
"previous_question": previous_question,
50+
"answer_text": answer_text,
51+
}
52+
)
53+
if not isinstance(result, FollowupResult):
54+
raise TypeError(
55+
f"chain returned {type(result).__name__}, expected FollowupResult"
56+
)
57+
return result
58+
59+
60+
def build_followup_generation_chain(settings: Settings, core_client: CoreClient | None = None) -> Runnable:
61+
from langchain_openai import ChatOpenAI
62+
63+
parser = PydanticOutputParser(pydantic_object=FollowupResult)
64+
prompt = ChatPromptTemplate.from_messages(
65+
[
66+
("system", SYSTEM_PROMPT),
67+
("human", HUMAN_PROMPT),
68+
]
69+
).partial(format_instructions=parser.get_format_instructions())
70+
71+
callbacks = []
72+
if core_client is not None:
73+
callbacks.append(CoreAiLogCallback(
74+
core_client=core_client,
75+
request_type="generate.followup",
76+
default_model=settings.llm_flash_model,
77+
))
78+
79+
llm = ChatOpenAI(
80+
model=settings.llm_flash_model,
81+
temperature=settings.llm_flash_temperature,
82+
api_key=settings.llm_api_key or None,
83+
base_url=settings.llm_base_url,
84+
max_tokens=settings.llm_flash_max_tokens,
85+
callbacks=callbacks,
86+
)
87+
return prompt | llm | parser

0 commit comments

Comments
 (0)