"""A - 近期状态切片: 近 N 场赛果 / 走势(form)。""" from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from sqlalchemy.orm import selectinload from src.db.base import AsyncSessionLocal from src.db.models import Match from src.llm.slices.common import MatchHeader, SliceResult, _is_stats_available, _outcome if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession async def form_slice(header: MatchHeader, *, limit: int = 5, before=None, db: AsyncSession | None = None) -> SliceResult: """A - 近期状态切片: 两队近 N 场赛果、关键事件、走势判断。before=match_date 用于回测。 db: 可选共享 session,避免每个切片独立建连(见 context_builder 模块 docstring)。 """ if db is not None: home_form = await _get_form(db, header.home_team_id, before=before, limit=limit) away_form = await _get_form(db, header.away_team_id, before=before, limit=limit) else: async with AsyncSessionLocal() as new_db: home_form = await _get_form(new_db, header.home_team_id, before=before, limit=limit) away_form = await _get_form(new_db, header.away_team_id, before=before, limit=limit) lines = [] n_scored = 0 # P0-1 修复:每场历史比赛必须根据「该队当时是主是客」判断 side, # 不能用本场 side 硬套 —— 否则客场输球会被算成主场赢球。 for label, name, form, team_id in ( ("主队", header.home_name, home_form, header.home_team_id), ("客队", header.away_name, away_form, header.away_team_id), ): lines.append(f"── {label}近况({name},近 {limit} 场) ──") if form: wins = draws = losses = 0 for fm in form: is_home = (fm.home_team_id == team_id) side = "home" if is_home else "away" o = _outcome(fm.home_goals, fm.away_goals, side) if o == "W": wins += 1 elif o == "D": draws += 1 else: losses += 1 if fm.home_goals is not None: n_scored += 1 score = f"{fm.home_goals}-{fm.away_goals}" if fm.home_goals is not None else "vs" xg = "" if fm.stats and _is_stats_available(fm.stats, before) and fm.stats.home_xg is not None: own = fm.stats.home_xg if is_home else fm.stats.away_xg xg = f" (xG {own:.1f})" opp = fm.away_team.name if is_home else fm.home_team.name lines.append(f" {o} {score} vs {opp}{xg}") lines.append(f" 近 {len(form)} 场: {wins}胜 {draws}平 {losses}负") else: lines.append(" 无数据") return SliceResult(text="\n".join(lines), has_data=n_scored > 0, n_records=n_scored) async def _get_form(db, team_id: int, before, *, limit: int = 5) -> list[Match]: """某队近 N 场(已完赛)。before=None 表示不限制(预测赛前的场景由调用方保证)。 必须预加载 stats / home_team / away_team:切片函数会读取这些关系, 而 async session 下惰性加载会抛 MissingGreenlet。 (models.py 已声明 lazy="selectin",此处显式声明以固化查询意图。) """ stmt = ( select(Match) .options( selectinload(Match.stats), selectinload(Match.home_team), selectinload(Match.away_team), ) .where(Match.match_status == "finished") .where(Match.score_status == "known") .where(Match.home_goals.is_not(None)) .where((Match.home_team_id == team_id) | (Match.away_team_id == team_id)) .order_by(Match.match_date.desc()) .limit(limit) ) if before is not None: stmt = stmt.where(Match.match_date < before) result = await db.execute(stmt) return list(result.scalars().all())