rebuild(stage-11): deliver deterministic heaven workflows

This commit is contained in:
leefer
2026-07-30 07:08:13 +08:00
parent aa3f02bd59
commit 35ae079de7
49 changed files with 7208 additions and 39 deletions
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import json
from collections.abc import Iterator
from typing import Annotated, Literal
from fastapi import APIRouter, Query, Request
from fastapi.responses import StreamingResponse
from backend.data.gateway import MarketDataUnavailable
from backend.features.accounts.auth import SmartAccessPrincipal, SmartWritePrincipal
from backend.features.heaven.schemas import (
DeleteResponse,
FortuneInput,
HeartCompleteInput,
HeartLineInput,
InterpretInput,
TrendInput,
)
from backend.features.heaven.service import HeavenError
from backend.http.errors import AppError
from backend.llm.gateway import LLMGatewayError
router = APIRouter(prefix="/heaven", tags=["heaven"])
@router.get("/setup", response_model=dict)
def setup(
request: Request,
principal: SmartAccessPrincipal,
reading_date: Annotated[str, Query(alias="date", min_length=10, max_length=10)],
) -> dict:
return _call(request, "setup", principal, reading_date)
@router.post("/trend/load", response_model=dict)
def load_trend(payload: TrendInput, request: Request, principal: SmartWritePrincipal) -> dict:
return _call(request, "trend", principal, payload.query, payload.trade_date, payload.manual)
@router.post("/fortune", response_model=dict)
def create_fortune(payload: FortuneInput, request: Request, principal: SmartWritePrincipal) -> dict:
return _call(request, "fortune", principal, payload.trade_date)
@router.post("/heart/line", response_model=dict)
def heart_line(payload: HeartLineInput, request: Request, principal: SmartWritePrincipal) -> dict:
return _call(request, "heart_line", principal, payload.trade_date, payload.values)
@router.post("/heart/complete", response_model=dict)
def complete_heart(
payload: HeartCompleteInput, request: Request, principal: SmartWritePrincipal
) -> dict:
return _call(
request,
"complete_heart",
principal,
payload.trade_date,
payload.values,
payload.first_thought_confirmed,
)
@router.get("/readings", response_model=list[dict])
def readings(
request: Request,
principal: SmartAccessPrincipal,
mode: Annotated[Literal["trend", "fortune", "heart"] | None, Query()] = None,
reading_date: Annotated[str | None, Query(alias="date")] = None,
) -> list[dict]:
return _call(request, "readings", principal, mode, reading_date)
@router.delete("/readings/{reading_id}", response_model=DeleteResponse)
def delete_reading(
reading_id: int, request: Request, principal: SmartWritePrincipal
) -> DeleteResponse:
return DeleteResponse(deleted=_call(request, "delete", principal, reading_id))
@router.post("/interpret")
def interpret(
payload: InterpretInput, request: Request, principal: SmartWritePrincipal
) -> StreamingResponse:
prepared = _call(request, "prepare_interpret", principal, payload.reading_id)
def body() -> Iterator[bytes]:
for event in request.app.state.container.heaven.stream_interpret(prepared):
yield (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode(
"utf-8"
)
return StreamingResponse(
body(),
media_type="application/x-ndjson",
headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"},
)
def _call(request: Request, method: str, *args):
try:
return getattr(request.app.state.container.heaven, method)(*args)
except HeavenError as exc:
raise AppError("heaven_unavailable", str(exc), 409) from exc
except MarketDataUnavailable as exc:
raise AppError("market_data_unavailable", str(exc), 503) from exc
except LLMGatewayError as exc:
status = 403 if exc.code in {"membership_required", "quota_exhausted"} else 503
raise AppError(exc.code, str(exc), status) from exc