refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env) 统一错误契约 Auth 与事务边界 Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client 可观测性(OpenTelemetry + LGTM)
This commit is contained in:
@@ -5,11 +5,11 @@ from __future__ import annotations
|
||||
import json
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_async_db
|
||||
from app.core.deps_types import DbDep
|
||||
from app.core.errors import BadRequestError, NotFoundError
|
||||
from app.core.memoir_pipeline_progress import get_pipeline_run_for_eval
|
||||
from app.features.evaluation.admin_service import EvaluationAdminService
|
||||
from app.features.evaluation.deps import (
|
||||
@@ -17,11 +17,9 @@ from app.features.evaluation.deps import (
|
||||
get_evaluation_admin_service,
|
||||
get_memoir_readiness_service,
|
||||
get_replay_conversation_service,
|
||||
get_session_catalog_service,
|
||||
)
|
||||
from app.features.evaluation.errors import (
|
||||
EvaluationBadRequestError,
|
||||
EvaluationNotFoundError,
|
||||
)
|
||||
from app.features.evaluation.errors import EvaluationBadRequestError
|
||||
from app.features.evaluation.importers.user_export_markdown import (
|
||||
extract_memoir_chapter_sections_from_export_md,
|
||||
extract_source_user_id_from_export_md,
|
||||
@@ -60,6 +58,8 @@ from app.features.evaluation.schemas import (
|
||||
from app.features.evaluation.session_catalog_service import SessionCatalogService
|
||||
from app.features.evaluation.user_export_fixtures import read_user_export_fixture
|
||||
|
||||
SessionCatalogDep = Annotated[SessionCatalogService, Depends(get_session_catalog_service)]
|
||||
|
||||
router = APIRouter(tags=["internal-evaluation"])
|
||||
|
||||
|
||||
@@ -69,18 +69,12 @@ async def eval_api_ping() -> dict[str, str | bool]:
|
||||
return {"ok": True, "service": "life-echo-internal-eval"}
|
||||
|
||||
|
||||
def _eval_http_exc(
|
||||
e: EvaluationNotFoundError | EvaluationBadRequestError,
|
||||
) -> HTTPException:
|
||||
if isinstance(e, EvaluationNotFoundError):
|
||||
return HTTPException(status_code=404, detail=e.detail)
|
||||
return HTTPException(status_code=400, detail=e.detail)
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=SessionListResponse)
|
||||
async def list_sessions(
|
||||
_auth: InternalEvalAuth,
|
||||
db: Annotated[AsyncSession, Depends(get_async_db)],
|
||||
catalog: SessionCatalogDep,
|
||||
offset: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
user_id: str | None = Query(None),
|
||||
@@ -90,7 +84,6 @@ async def list_sessions(
|
||||
description="按会话 status 过滤,如 active",
|
||||
),
|
||||
):
|
||||
catalog = SessionCatalogService(db)
|
||||
rows, total = await catalog.list_sessions(
|
||||
offset=offset, limit=limit, user_id=user_id, q=q, status=status
|
||||
)
|
||||
@@ -119,12 +112,11 @@ async def list_sessions(
|
||||
async def get_session_dialogue(
|
||||
conversation_id: str,
|
||||
_auth: InternalEvalAuth,
|
||||
db: Annotated[AsyncSession, Depends(get_async_db)],
|
||||
catalog: SessionCatalogDep,
|
||||
):
|
||||
catalog = SessionCatalogService(db)
|
||||
out = await catalog.get_session_dialogue(conversation_id)
|
||||
if not out:
|
||||
raise HTTPException(status_code=404, detail="conversation not found")
|
||||
raise NotFoundError("conversation not found")
|
||||
return out
|
||||
|
||||
|
||||
@@ -134,12 +126,11 @@ async def get_session_dialogue(
|
||||
async def get_session_transcript(
|
||||
conversation_id: str,
|
||||
_auth: InternalEvalAuth,
|
||||
db: Annotated[AsyncSession, Depends(get_async_db)],
|
||||
catalog: SessionCatalogDep,
|
||||
):
|
||||
catalog = SessionCatalogService(db)
|
||||
tr = await catalog.get_transcript(conversation_id)
|
||||
if not tr:
|
||||
raise HTTPException(status_code=404, detail="conversation not found")
|
||||
raise NotFoundError("conversation not found")
|
||||
return SessionTranscriptOut(
|
||||
conversation_id=tr.conversation_id,
|
||||
user_id=tr.user_id,
|
||||
@@ -155,12 +146,11 @@ async def get_session_transcript(
|
||||
async def get_playground_conversation_judge(
|
||||
conversation_id: str,
|
||||
_auth: InternalEvalAuth,
|
||||
db: Annotated[AsyncSession, Depends(get_async_db)],
|
||||
catalog: SessionCatalogDep,
|
||||
):
|
||||
catalog = SessionCatalogService(db)
|
||||
tr = await catalog.get_transcript(conversation_id)
|
||||
if not tr:
|
||||
raise HTTPException(status_code=404, detail="conversation not found")
|
||||
raise NotFoundError("conversation not found")
|
||||
judge = await catalog.get_playground_conversation_judge_json(conversation_id)
|
||||
return PlaygroundConversationJudgeOut(
|
||||
conversation_id=conversation_id,
|
||||
@@ -185,22 +175,16 @@ async def get_memoir_pipeline_run(
|
||||
] = None,
|
||||
):
|
||||
if not phase1_task_id and not memoir_correlation_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="provide phase1_task_id or memoir_correlation_id",
|
||||
)
|
||||
raise BadRequestError("provide phase1_task_id or memoir_correlation_id")
|
||||
if phase1_task_id and memoir_correlation_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="provide only one of phase1_task_id or memoir_correlation_id",
|
||||
)
|
||||
raise BadRequestError("provide only one of phase1_task_id or memoir_correlation_id")
|
||||
snap = get_pipeline_run_for_eval(
|
||||
user_id.strip(),
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
phase1_task_id=phase1_task_id,
|
||||
)
|
||||
if not snap:
|
||||
raise HTTPException(status_code=404, detail="pipeline snapshot not found")
|
||||
raise NotFoundError("pipeline snapshot not found")
|
||||
return MemoirPipelineRunOut.model_validate(snap)
|
||||
|
||||
|
||||
@@ -220,15 +204,10 @@ async def memoir_phase1_ready(
|
||||
),
|
||||
],
|
||||
):
|
||||
try:
|
||||
return await svc.memoir_phase1_ready_for_segments(
|
||||
conversation_id=conversation_id,
|
||||
segment_ids=segment_ids,
|
||||
)
|
||||
except EvaluationNotFoundError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
return await svc.memoir_phase1_ready_for_segments(
|
||||
conversation_id=conversation_id,
|
||||
segment_ids=segment_ids,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -240,14 +219,9 @@ async def memoir_submit_phase1(
|
||||
_auth: InternalEvalAuth,
|
||||
svc: Annotated[MemoirReadinessService, Depends(get_memoir_readiness_service)],
|
||||
):
|
||||
try:
|
||||
return await svc.submit_memoir_phase1_for_conversation(
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
except EvaluationNotFoundError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
return await svc.submit_memoir_phase1_for_conversation(
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/sessions/replay-bootstrap", response_model=ReplayBootstrapOut)
|
||||
@@ -258,10 +232,7 @@ async def replay_bootstrap(
|
||||
ReplayConversationService, Depends(get_replay_conversation_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
cid = await replay.bootstrap_conversation(body.user_id)
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
cid = await replay.bootstrap_conversation(body.user_id)
|
||||
return ReplayBootstrapOut(conversation_id=cid)
|
||||
|
||||
|
||||
@@ -272,10 +243,7 @@ async def create_eval_sandbox(
|
||||
ReplayConversationService, Depends(get_replay_conversation_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
uid, cid, phone, nick = await replay.create_eval_sandbox()
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
uid, cid, phone, nick = await replay.create_eval_sandbox()
|
||||
return EvalSandboxOut(
|
||||
user_id=uid,
|
||||
conversation_id=cid,
|
||||
@@ -293,42 +261,34 @@ async def replay_conversation(
|
||||
],
|
||||
):
|
||||
if body.fixture_filename and body.user_utterances:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="provide only one of fixture_filename or user_utterances",
|
||||
raise BadRequestError("provide only one of fixture_filename or user_utterances")
|
||||
segment_ids: list[str] = []
|
||||
timing = None
|
||||
if body.fixture_filename:
|
||||
fn = body.fixture_filename.strip()
|
||||
n, echo, segment_ids, timing = await replay.replay_fixture(
|
||||
conversation_id=body.conversation_id,
|
||||
fixture_filename=fn,
|
||||
flush_memoir_after=body.flush_memoir_after,
|
||||
skip_memoir=body.skip_memoir,
|
||||
skip_tts=body.skip_tts,
|
||||
)
|
||||
elif body.user_utterances is not None:
|
||||
utt = [str(u) for u in body.user_utterances if str(u).strip()]
|
||||
if not utt:
|
||||
raise EvaluationBadRequestError("user_utterances is empty")
|
||||
n, segment_ids, timing = await replay.replay_utterances(
|
||||
conversation_id=body.conversation_id,
|
||||
utterances=utt,
|
||||
flush_memoir_after=body.flush_memoir_after,
|
||||
skip_memoir=body.skip_memoir,
|
||||
skip_tts=body.skip_tts,
|
||||
)
|
||||
echo = utt
|
||||
else:
|
||||
raise EvaluationBadRequestError(
|
||||
"fixture_filename or user_utterances required"
|
||||
)
|
||||
try:
|
||||
segment_ids: list[str] = []
|
||||
timing = None
|
||||
if body.fixture_filename:
|
||||
fn = body.fixture_filename.strip()
|
||||
n, echo, segment_ids, timing = await replay.replay_fixture(
|
||||
conversation_id=body.conversation_id,
|
||||
fixture_filename=fn,
|
||||
flush_memoir_after=body.flush_memoir_after,
|
||||
skip_memoir=body.skip_memoir,
|
||||
skip_tts=body.skip_tts,
|
||||
)
|
||||
elif body.user_utterances is not None:
|
||||
utt = [str(u) for u in body.user_utterances if str(u).strip()]
|
||||
if not utt:
|
||||
raise EvaluationBadRequestError("user_utterances is empty")
|
||||
n, segment_ids, timing = await replay.replay_utterances(
|
||||
conversation_id=body.conversation_id,
|
||||
utterances=utt,
|
||||
flush_memoir_after=body.flush_memoir_after,
|
||||
skip_memoir=body.skip_memoir,
|
||||
skip_tts=body.skip_tts,
|
||||
)
|
||||
echo = utt
|
||||
else:
|
||||
raise EvaluationBadRequestError(
|
||||
"fixture_filename or user_utterances required"
|
||||
)
|
||||
except EvaluationNotFoundError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
return ReplayConversationOut(
|
||||
conversation_id=body.conversation_id,
|
||||
turns_replayed=n,
|
||||
@@ -348,17 +308,12 @@ async def judge_conversation_manual(
|
||||
EvalJudgeManualService, Depends(get_eval_judge_manual_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
payload = await judge_svc.judge_conversation(
|
||||
body.conversation_id,
|
||||
body.fixture_filename,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
except EvaluationNotFoundError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
payload = await judge_svc.judge_conversation(
|
||||
body.conversation_id,
|
||||
body.fixture_filename,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
return ManualJudgeConversationOut.model_validate(payload)
|
||||
|
||||
|
||||
@@ -411,18 +366,13 @@ async def retry_baseline_conversation_judge(
|
||||
EvalJudgeManualService, Depends(get_eval_judge_manual_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
payload = await judge_svc.retry_baseline_conversation_judge(
|
||||
body.conversation_id,
|
||||
body.fixture_filename,
|
||||
include_baseline_turn_judges=body.include_baseline_turn_judges,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
except EvaluationNotFoundError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
payload = await judge_svc.retry_baseline_conversation_judge(
|
||||
body.conversation_id,
|
||||
body.fixture_filename,
|
||||
include_baseline_turn_judges=body.include_baseline_turn_judges,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
return RetryBaselineJudgeOut.model_validate(payload)
|
||||
|
||||
|
||||
@@ -434,15 +384,12 @@ async def judge_memoir_chapters_manual(
|
||||
EvalJudgeManualService, Depends(get_eval_judge_manual_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
payload = await judge_svc.judge_memoir_for_user(
|
||||
body.user_id,
|
||||
body.baseline_sections,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
payload = await judge_svc.judge_memoir_for_user(
|
||||
body.user_id,
|
||||
body.baseline_sections,
|
||||
judge_provider=body.judge_provider,
|
||||
judge_model=body.judge_model,
|
||||
)
|
||||
return ManualJudgeMemoirOut.model_validate(payload)
|
||||
|
||||
|
||||
@@ -490,10 +437,7 @@ async def get_user_memoir_snapshot(
|
||||
EvalJudgeManualService, Depends(get_eval_judge_manual_service)
|
||||
],
|
||||
):
|
||||
try:
|
||||
payload = await judge_svc.memoir_snapshot(user_id)
|
||||
except EvaluationBadRequestError as e:
|
||||
raise _eval_http_exc(e) from e
|
||||
payload = await judge_svc.memoir_snapshot(user_id)
|
||||
return UserMemoirSnapshotOut.model_validate(payload)
|
||||
|
||||
|
||||
@@ -519,11 +463,9 @@ async def get_user_export_fixture(
|
||||
try:
|
||||
turns, raw_md = read_user_export_fixture(filename)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="invalid fixture filename"
|
||||
) from None
|
||||
raise BadRequestError("invalid fixture filename") from None
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="fixture not found") from None
|
||||
raise NotFoundError("fixture not found")
|
||||
memoir_tuples = extract_memoir_chapter_sections_from_export_md(raw_md)
|
||||
return UserExportFixtureDetailOut(
|
||||
filename=filename,
|
||||
|
||||
Reference in New Issue
Block a user