refactor: Sprint 3 - 引入 UnitOfWork + Repository 架构
新增: - src/db/unit_of_work.py: UnitOfWork 事务封装 - src/db/repositories.py: Match/Team/League/Prediction Repository 重构: - 删除 src/data/match_lookup.py(由 Repository 替代) - 数据源(bzzoiro/understat/injuries)不再自行 commit - API 路由(ingest)改用 UnitOfWork - LLM 服务(predict/orchestrator/eval/backtest)改用 UnitOfWork 事务边界统一由调用方控制,数据层不再自行决定 commit。
This commit is contained in:
@@ -11,6 +11,7 @@ from dataclasses import dataclass
|
||||
from src.core.config import settings
|
||||
from src.db.base import AsyncSessionLocal
|
||||
from src.db.models import Match, Prediction
|
||||
from src.db.unit_of_work import get_uow
|
||||
from src.llm.agents.base import AgentReport, AgentSpec, load_agent_prompt
|
||||
from src.llm.context_builder import (
|
||||
MatchHeader,
|
||||
@@ -184,8 +185,9 @@ async def predict_match_multi(
|
||||
_reports_to_json(reports).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
# 4. 存库
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 4. 存库(使用 UnitOfWork)
|
||||
async with get_uow() as uow:
|
||||
db = uow.session
|
||||
m = await db.get(Match, match_id)
|
||||
if m is None:
|
||||
raise ValueError(f"match {match_id} not found")
|
||||
@@ -218,7 +220,7 @@ async def predict_match_multi(
|
||||
input_hash=input_hash,
|
||||
)
|
||||
db.add(pred)
|
||||
await db.commit()
|
||||
await uow.commit()
|
||||
await db.refresh(pred)
|
||||
|
||||
return MultiPredictResult(
|
||||
|
||||
+8
-8
@@ -11,8 +11,8 @@ from dataclasses import dataclass, field
|
||||
|
||||
from sqlalchemy import and_, select
|
||||
|
||||
from src.db.base import AsyncSessionLocal
|
||||
from src.db.models import League, Match, Prediction
|
||||
from src.db.models import League, Match
|
||||
from src.db.unit_of_work import get_uow
|
||||
from src.llm.eval import settle_prediction
|
||||
from src.llm.predict import predict_match
|
||||
|
||||
@@ -33,7 +33,7 @@ class BacktestMatchResult:
|
||||
pred_home: float | None
|
||||
pred_away: float | None
|
||||
pred_1x2: str | None
|
||||
confidence: float | None
|
||||
subjective_confidence: float | None
|
||||
correct_1x2: bool
|
||||
prediction_id: int
|
||||
|
||||
@@ -45,7 +45,7 @@ class BacktestSummary:
|
||||
scored: int
|
||||
accuracy_1x2: float | None = None
|
||||
avg_score_rmse: float | None = None
|
||||
avg_confidence: float | None = None
|
||||
avg_subjective_confidence: float | None = None
|
||||
calibration: list[dict] = field(default_factory=list)
|
||||
results: list[BacktestMatchResult] = field(default_factory=list)
|
||||
|
||||
@@ -108,9 +108,9 @@ async def run_backtest(
|
||||
Returns:
|
||||
BacktestSummary 含逐场结果 + 汇总统计
|
||||
"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
async with get_uow() as uow:
|
||||
matches = await _get_historical_matches(
|
||||
db, league_id=league_id, date_from=date_from, date_to=date_to, limit=limit
|
||||
uow.session, league_id=league_id, date_from=date_from, date_to=date_to, limit=limit
|
||||
)
|
||||
|
||||
summary = BacktestSummary(total=len(matches), scored=0)
|
||||
@@ -163,10 +163,10 @@ async def run_backtest(
|
||||
if errors:
|
||||
summary.avg_score_rmse = round(sum(errors) / len(errors), 2)
|
||||
|
||||
# 平均置信度
|
||||
# 平均主观置信度
|
||||
confs = [r.subjective_confidence for r in summary.results if r.subjective_confidence is not None]
|
||||
if confs:
|
||||
summary.avg_confidence = round(sum(confs) / len(confs), 2)
|
||||
summary.avg_subjective_confidence = round(sum(confs) / len(confs), 2)
|
||||
|
||||
# 校准:按置信度分桶,看实际准确率是否匹配
|
||||
summary.calibration = _compute_calibration(summary.results)
|
||||
|
||||
+7
-8
@@ -5,23 +5,22 @@ import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.db.base import AsyncSessionLocal
|
||||
from src.db.models import Prediction
|
||||
from src.db.unit_of_work import get_uow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def settle_prediction(prediction_id: int, home_goals: int, away_goals: int) -> Prediction:
|
||||
"""回填实际结果。"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
pred = await db.get(Prediction, prediction_id)
|
||||
async with get_uow() as uow:
|
||||
pred = await uow.session.get(Prediction, prediction_id)
|
||||
if pred is None:
|
||||
raise ValueError(f"prediction {prediction_id} not found")
|
||||
pred.actual_home_goals = home_goals
|
||||
pred.actual_away_goals = away_goals
|
||||
pred.settled = True
|
||||
await db.commit()
|
||||
await db.refresh(pred)
|
||||
await uow.commit()
|
||||
return pred
|
||||
|
||||
|
||||
@@ -36,12 +35,12 @@ def _actual_1x2(home: int, away: int) -> str:
|
||||
|
||||
async def get_eval_summary() -> dict:
|
||||
"""按 provider × 模型聚合评估。"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
async with get_uow() as uow:
|
||||
stmt = (
|
||||
select(Prediction)
|
||||
.where(Prediction.settled == True)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
result = await uow.session.execute(stmt)
|
||||
rows = list(result.scalars().all())
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -76,6 +75,6 @@ async def get_eval_summary() -> dict:
|
||||
"total": b["total"],
|
||||
"accuracy_1x2": round(acc, 1),
|
||||
"avg_score_rmse": round(avg_err, 2) if avg_err is not None else None,
|
||||
"avg_confidence": round(avg_conf, 2) if avg_conf is not None else None,
|
||||
"avg_subjective_confidence": round(avg_conf, 2) if avg_conf is not None else None,
|
||||
})
|
||||
return {"summary": summary}
|
||||
|
||||
+5
-2
@@ -13,6 +13,7 @@ from threading import Lock
|
||||
from src.core.config import settings
|
||||
from src.db.base import AsyncSessionLocal
|
||||
from src.db.models import Match, Prediction
|
||||
from src.db.unit_of_work import get_uow
|
||||
from src.llm.context_builder import build_context
|
||||
from src.llm.provider import LLMProvider, get_default_provider
|
||||
|
||||
@@ -143,8 +144,9 @@ async def _predict_single(
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"LLM 输出校验失败: {e}")
|
||||
|
||||
# 4. 存预测(独立 session,因为 context 用的是自己的 session)
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 4. 存预测(使用 UnitOfWork 统一事务)
|
||||
async with get_uow() as uow:
|
||||
db = uow.session
|
||||
# 验证 match 存在
|
||||
m = await db.get(Match, match_id)
|
||||
if m is None:
|
||||
@@ -188,4 +190,5 @@ async def _predict_single(
|
||||
|
||||
# 5. 写入缓存
|
||||
_set_cached(match_id, settings.LLM_PROVIDER, provider.model, version, result)
|
||||
await uow.commit()
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user