feat: P0-02/P0-03/P0-05 数据库时间语义与约束
P0-02: MatchStats 增加 source/source_record_id/retrieved_at/available_at
context_builder 过滤统计数据时检查 available_at <= cutoff
P0-03: Prediction 增加 match_kickoff_at/prediction_created_at/prediction_cutoff_at
明确区分比赛时间/预测创建时间/数据截止时间
P0-05: Prediction 增加 status 字段(success/failed/degraded)
数据库 CHECK 约束
Alembic: 0005_prediction_status_and_stats_provenance
P1-02: injuries as_of 不再截断为 date,保持 datetime 精度
This commit is contained in:
@@ -7,6 +7,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from src.core.config import settings
|
||||
from src.db.base import AsyncSessionLocal
|
||||
@@ -166,7 +167,9 @@ async def predict_match_multi(
|
||||
|
||||
# 1. 比赛头(各 agent 共享;不存在则 404)
|
||||
header = await load_match_header(match_id)
|
||||
cutoff_at = header.match_dt
|
||||
match_kickoff_at = header.match_dt
|
||||
prediction_cutoff_at = header.match_dt # 默认:比赛时间作为数据截止
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# 2. 并行专家
|
||||
specialist_provider = _get_specialist_provider()
|
||||
@@ -215,7 +218,10 @@ async def predict_match_multi(
|
||||
reasoning=validated.reasoning,
|
||||
raw_response=final,
|
||||
agent_outputs=[r.to_dict() for r in reports],
|
||||
cutoff_at=cutoff_at,
|
||||
status="success",
|
||||
match_kickoff_at=match_kickoff_at,
|
||||
prediction_cutoff_at=prediction_cutoff_at,
|
||||
prediction_created_at=now,
|
||||
input_hash=input_hash,
|
||||
)
|
||||
session.add(pred)
|
||||
|
||||
@@ -30,6 +30,15 @@ def _outcome(home_goals: int, away_goals: int, side: str) -> str:
|
||||
return "W" if away_goals > home_goals else ("D" if away_goals == home_goals else "L")
|
||||
|
||||
|
||||
def _is_stats_available(stats, before) -> bool:
|
||||
"""检查统计数据在 cutoff 时间是否已可用。"""
|
||||
if before is None:
|
||||
return True
|
||||
if stats.available_at is None:
|
||||
return True # 无时间信息时保守处理:允许使用
|
||||
return stats.available_at <= before
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchContext:
|
||||
match_id: int
|
||||
@@ -162,7 +171,8 @@ async def stats_slice(header: MatchHeader, *, limit: int = 10, before=None) -> s
|
||||
gf += fm.home_goals if side == "home" else fm.away_goals
|
||||
ga += fm.away_goals if side == "home" else fm.home_goals
|
||||
n += 1
|
||||
if fm.stats:
|
||||
# 只使用 cutoff 之前已可用的统计数据
|
||||
if fm.stats and _is_stats_available(fm.stats, before):
|
||||
if fm.stats.home_shots is not None:
|
||||
shots += fm.stats.home_shots if side == "home" else fm.stats.away_shots
|
||||
sot += fm.stats.home_shots_on_target if side == "home" else fm.stats.away_shots_on_target
|
||||
|
||||
+7
-2
@@ -116,7 +116,9 @@ async def _predict_single(
|
||||
ctx = await build_context(match_id)
|
||||
|
||||
# 1.5 计算快照元数据(用于可复现性)
|
||||
cutoff_at = ctx.match_dt # 比赛时间 = 数据截止时间
|
||||
now = datetime.now(timezone.utc)
|
||||
match_kickoff_at = ctx.match_dt
|
||||
prediction_cutoff_at = ctx.match_dt # 默认:比赛时间作为数据截止
|
||||
input_hash = hashlib.sha256(ctx.text.encode("utf-8")).hexdigest()
|
||||
|
||||
# 2. 拼 prompt(指定版本)
|
||||
@@ -165,7 +167,10 @@ async def _predict_single(
|
||||
subjective_confidence=validated.subjective_confidence,
|
||||
reasoning=validated.reasoning,
|
||||
raw_response=resp.raw,
|
||||
cutoff_at=cutoff_at,
|
||||
status="success",
|
||||
match_kickoff_at=match_kickoff_at,
|
||||
prediction_cutoff_at=prediction_cutoff_at,
|
||||
prediction_created_at=now,
|
||||
input_hash=input_hash,
|
||||
)
|
||||
session.add(pred)
|
||||
|
||||
Reference in New Issue
Block a user