[Feat] /stream/v2 백엔드 SSE 어댑터#117
Conversation
백엔드(Proovy-server)가 호출하는 /stream/v2 + /health 계약에 맞춰 SSE 어댑터를 추가한다. 내부 그래프·emitter·envelope은 그대로 두고 경계에서만 변환한다. - events.py: to_stream_v2 — token→llm.token.delta, done→run.completed, error→run.failed, 그 외 drop. 모든 data에 thread_id 포함 - emitter.stream(serializer=) 파라미터화 — None 직렬화 결과는 skip - _runner.start_graph_task — /solve·/stream/v2 공유 실행 헬퍼 (dead code 방지) - schemas/stream.py StreamInput — camelCase(message/threadId/userId) 수용 - api/stream.py POST /stream/v2 + GET /health, main에 prefix 없이 등록 - solve.py는 공유 runner로 리팩터 (동작·기존 테스트 동일) 멀티턴은 threadId 왕복 + Postgres 체크포인트로 기존 동작 그대로. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 12 minutes and 46 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughPR은 백엔드 ChangesStream v2 어댑터 및 SSE 통합
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
요약
백엔드 /stream/v2 vocab 어댑터와 _runner 공유 리팩터 방향은 적절하고, 계약 매핑·drop 동작 테스트가 잘 갖춰져 있습니다. 다만 StreamInput.userId가 빈 문자열을 허용해 /solve와 달리 체크포인트 키 :{thread_id}로 수렴할 수 있어 멀티턴 격리가 약해집니다. 루트 /health는 프로세스 생존만 확인하며, to_stream_v2의 SSE id는 내부 seq를 그대로 써 drop 시 gap이 남을 수 있습니다(백엔드가 id seq를 쓰는지는 추가 확인 필요).
[문제 1]
- 심각도: High
- 범주: 보안 / 정확성
- 근거:
StreamInput.user_id기본값이""이고 필수 검증이 없어,userId생략·빈 값 요청이 200으로 통과합니다._runner의 체크포인트 키는f"{user_id}:{thread_id}"이므로user_id==""이면 키가:{threadId}형태가 되어, 서로 다른 호출자가 동일threadId만 맞추면 같은 Postgres 체크포인트 스레드를 공유할 수 있습니다./api/v1/solve의SolveRequest는user_id를 필수(Field(...))로 두어 이 경로와 불일치합니다. - 수정안:
SolveRequest와 동일하게user_id: str = Field(..., alias="userId", min_length=1)로 필수화하거나, validator에서 빈 문자열을 422로 거부합니다.test_stream_v2_missing_user_id_returns_422같은 회귀 테스트를 추가하는 것이 좋습니다.
[문제 2]
- 심각도: Medium
- 범주: 정확성 (추가 확인 필요)
- 근거:
to_stream_v2는 내부 이벤트를None으로 drop하지만 SSEid는 여전히f"{thread_id}:{event.seq}"(내부 단조 seq)를 사용합니다. 예:page_start(seq=0) drop 후 첫 토큰이 seq=3이면 wireid는th-1:3이 되어 seq 1·2가 비어 보입니다. 백엔드/BFF가Last-Event-ID의 seq로 gap·재전송을 판단하면 오탐 가능성이 있습니다. PR 설명상 재연결은 BFF 관할이라 영향 범위는 추가 확인 필요입니다. - 수정안: (1) 백엔드가
data.thread_id만 쓰고idseq는 무시한다면 docstring/계약에 명시. (2) seq 기반 판단이 필요하면 v2 전용 wire seq를 별도 카운터로 부여하거나, drop된 이벤트는id에 반영하지 않도록 매핑합니다.
[문제 3]
- 심각도: Medium
- 범주: 유지보수성 / 운영
- 근거: 루트
GET /health는 항상{"status":"ok"}만 반환합니다. lifespan에서 초기화한 checkpointer·Daytona·build_graph실패 여부, DB 연결 불가 등은 반영되지 않아, 백엔드 사전 체크가 “트래픽 투입 가능”과 동치가 아닐 수 있습니다. - 수정안: 의도가 liveness만이면
summary/docstring에 “ready 아님”을 명시하거나,/api/v1/health처럼 최소 의존성(예: checkpointer ping)을 검사하는 readiness를 분리합니다.
[문제 4]
- 심각도: Low
- 범주: 정확성 (추가 확인 필요)
- 근거:
run.failedpayload에message와thread_id만 포함하고, 내부ErrorPayload.code(credit_exhausted,timeout등)는 전달하지 않습니다. 백엔드가 실패 유형별 분기를code로 한다면 정보 손실입니다. - 수정안: 백엔드 계약에
code필드가 있으면data에code(및 필요 시recoverable)를 추가하고 단위 테스트를 보강합니다. 계약에 없으면 현 상태 유지 가능합니다.
Sent by Cursor Automation: Chowon Reviewer
|
|
||
| message: str = Field(..., description="사용자 입력(문제)") | ||
| thread_id: str | None = Field(None, alias="threadId", description="대화 맥락 ID (없으면 생성)") | ||
| user_id: str = Field("", alias="userId", description="사용자 ID") |
There was a problem hiding this comment.
[High · 보안/정확성] user_id 기본값 "" + 비필수라 userId 생략 시 200이 됩니다. 체크포인트 키가 :{threadId}로 수렴되어 동일 threadId를 아는 호출자 간 멀티턴 상태가 공유될 수 있습니다 (/solve는 user_id 필수).
수정안: Field(..., alias="userId", min_length=1) 또는 validator로 빈 문자열 422. 회귀 테스트 추가 권장.
| return None | ||
| return { | ||
| "event": name, | ||
| "id": f"{event.thread_id}:{event.seq}", |
There was a problem hiding this comment.
[Medium · 정확성 · 추가 확인 필요] 내부 이벤트 drop 후에도 id가 내부 event.seq를 그대로 사용해 wire상 seq gap이 생깁니다. BFF가 Last-Event-ID의 seq로 gap/재전송을 보면 오탐 가능성이 있습니다.
수정안: 백엔드가 id를 무시한다면 계약에 명시. seq 기반이면 v2 전용 wire seq 카운터를 두세요.
| @router.get("/health", summary="헬스체크 (백엔드 사전 체크)") | ||
| async def health() -> dict[str, str]: | ||
| """백엔드가 SSE 시작 전 호출하는 헬스 엔드포인트.""" | ||
| return {"status": "ok"} |
There was a problem hiding this comment.
[Medium · 운영] 항상 ok만 반환해 checkpointer/DB/Daytona/그래프 준비 상태와 무관합니다. 백엔드 사전 체크가 readiness로 쓰이면 장애 파드로 트래픽이 갈 수 있습니다.
수정안: liveness 전용이면 docstring에 명시하거나, 최소 의존성 검사(readiness)를 추가/분리하세요.
) cursor 리뷰 3건 반영: - StreamInput.user_id 필수+min_length=1 — 빈 userId 시 체크포인트 키가 ":{threadId}"로 수렴해 threadId를 아는 호출자끼리 멀티턴 상태가 공유되는 문제 차단 (userId 누락·빈문자열 422 회귀 테스트 추가) - to_stream_v2: SSE id 제거 — 내부 이벤트 drop으로 wire seq가 불연속이라 소비자가 gap 오탐할 수 있음. 백엔드는 서버-투-서버 Flux로 Last-Event-ID 미사용이라 id 불필요 (재연결/replay는 백엔드 durable store 관할) - /health: liveness 전용임을 docstring 명시 — DB/Daytona/그래프 readiness는 검사하지 않음(백엔드 checkProovyAiHealth는 연결 가능 여부만 봄) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chowon442
left a comment
There was a problem hiding this comment.
/stream/v2 백엔드 계약 호환성 이슈 1건만 남깁니다.
|
|
||
| # v1 미사용 — 수용만 (백엔드가 크레딧·인증·파일을 책임) | ||
| files_url: list[str] = Field(default_factory=list, alias="filesUrl") | ||
| chosen_features: list[str] = Field(default_factory=list, alias="chosenFeatures") |
There was a problem hiding this comment.
[High · 백엔드 계약 호환성] chosenFeatures: null 요청이 422로 거부됩니다. Proovy-server dev 기준 ConversationRequest.chosenFeatures는 optional이고, ChatServiceImpl에서 그대로 ProovyAiRequest.chosenFeatures에 넣습니다. Jackson 기본 null 직렬화라면 사용자가 기능을 선택하지 않은 일반 요청이 AI /stream/v2 경계에서 실패할 수 있습니다.
수정안: chosen_features를 list[str] | None으로 받거나 validator로 None -> [] 처리하고, 서버 payload 형태(chosenFeatures: null)의 회귀 테스트를 추가해 주세요.
There was a problem hiding this comment.
반영했습니다 (359cc87) 🙏 — chosenFeatures뿐 아니라 filesUrl/agentConfig/streamTokens까지 mode="before" validator로 null → 기본값([], {}, True) 흡수하도록 했고, 백엔드 null 직렬화 페이로드(chosenFeatures: null 등) 회귀 테스트도 추가했습니다.
chowon442 리뷰 반영: 백엔드(Proovy-server)는 기능 미선택 시
chosenFeatures/filesUrl/agentConfig/streamTokens를 null로 직렬화하는데,
기존 타입(list/dict/bool)이 null을 거부해 일반 요청이 422로 깨질 수 있었다.
mode="before" validator로 None→기본값([], {}, True) 흡수. null 페이로드
회귀 테스트 추가.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>


📌 관련 이슈
🏷️ PR 타입
📝 작업 내용
백엔드(
Proovy-server)가 호출하는POST /stream/v2+GET /health계약에 맞춰 SSE 어댑터를 추가했습니다. 내부 그래프·emitter·envelope 설계는 그대로 두고 경계에서만 변환합니다 —/solve(내부 envelope)는 dev/e2e용으로 유지.events.py:to_stream_v2추가 —token→llm.token.delta,done→run.completed,error→run.failed, 그 외 내부 이벤트는None으로 drop. 모든data에thread_id포함(백엔드가 첫 턴 threadId 영속화에 사용)emitter.stream(serializer=...)파라미터화 — 직렬화 결과가None이면 skip./solve는to_sse,/stream/v2는to_stream_v2app/api/_runner.py:start_graph_task공유 실행 헬퍼로solve.py의_run추출 (/solve·/stream/v2단일 소스, dead code 방지)app/schemas/stream.py:StreamInput— 백엔드 camelCase(message/threadId/userId/...) 수용, 내부 매핑(message→problem등). credit/auth/files 필드는 v1 수용만(백엔드 책임)app/api/stream.py:POST /stream/v2+GET /health,main.py에 prefix 없이 등록(ingress가/aistrip 가정)solve.py: 공유 runner로 리팩터 — 동작·기존 테스트 동일백엔드 계약 매핑:
event:datallm.token.delta{"delta", "thread_id"}run.completed{"thread_id"}run.failed{"message", "thread_id"}멀티턴(threadId 왕복 + Postgres 체크포인트)은 양쪽 설계가 호환 → 계약을 맞추면 기존 동작 그대로 됩니다.
📸 스크린샷
실서버
/stream/v2호출(백엔드와 동일 camelCase 요청) — 실제 LLM 스트림:내부 이벤트(page_start/tool_*/node_result/credit_settled)는 seq 건너뜀으로 drop 확인됨.
✅ 체크리스트
📎 기타 참고사항
https://.../ai/*→ 앱/*매핑(/aistrip)하는지 확인. 그래야 백엔드proovy.ai.host(.../ai)의/stream/v2·/health가 본 라우터에 닿습니다.chosenFeatures/authToken)은 백엔드 책임 → AI는 v1에서 수용 후 무시./stream/v2기대 → 본 어댑터 배포 시점에PROOVY_AI_HOST가 이 서비스를 가리키게.Summary by CodeRabbit
새로운 기능
개선 사항
:포함 불가)테스트