refactor: context_builder 按 slice 拆到 src/llm/slices/ 包
单文件拆分(仅搬迁无逻辑修改): - common.py 共享类型/头信息/_outcome/_is_stats_available - form.py form_slice + _get_form - h2h.py h2h_slice + _get_h2h - stats.py stats_slice(复用 form._get_form) - home_away.py home_away_slice + _get_home_away - standings.py standings_slice - aggregate.py build_context context_builder.py 改为纯 re-export 门面,公开签名不变。 同步修复测试 patch 目标(p0_home_away/h2h_perspective/multi_agent_cutoff) 与 regressions 源码断言(读 slices/*.py)。 全量测试 270 通过。
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""B - 攻防数据切片: 进球/射门/控球/xG 聚合(stats)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src.db.base import AsyncSessionLocal
|
||||
from src.llm.slices.common import MatchHeader, SliceResult, _is_stats_available
|
||||
from src.llm.slices.form import _get_form
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
async def stats_slice(header: MatchHeader, *, limit: int = 10, before=None, db: AsyncSession | None = None) -> SliceResult:
|
||||
"""B - 攻防数据切片: 进球、射门、控球,评估攻防强度。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 = [f"── 攻防数据(近 {limit} 场) ──"]
|
||||
n_total = 0
|
||||
# P0-1 修复:每场历史比赛必须根据「该队当时是主是客」判断 side,
|
||||
# 不能用本场 side 硬套 —— 否则进球/失球/xG 全部算反。
|
||||
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),
|
||||
):
|
||||
if form:
|
||||
gf = ga = shots = sot = poss = xg = xga = 0
|
||||
n = n_shots = n_poss = n_xg = 0
|
||||
for fm in form:
|
||||
if fm.home_goals is None: continue
|
||||
is_home = (fm.home_team_id == team_id)
|
||||
gf += fm.home_goals if is_home else fm.away_goals
|
||||
ga += fm.away_goals if is_home else fm.home_goals
|
||||
n += 1
|
||||
# 只使用 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 is_home else fm.stats.away_shots
|
||||
sot += fm.stats.home_shots_on_target if is_home else fm.stats.away_shots_on_target
|
||||
n_shots += 1
|
||||
if fm.stats.home_possession is not None:
|
||||
poss += fm.stats.home_possession if is_home else (100 - fm.stats.home_possession)
|
||||
n_poss += 1
|
||||
if fm.stats.home_xg is not None:
|
||||
xg += fm.stats.home_xg if is_home else fm.stats.away_xg
|
||||
xga += fm.stats.away_xg if is_home else fm.stats.home_xg
|
||||
n_xg += 1
|
||||
n_total += n
|
||||
if n > 0:
|
||||
lines.append(f" {label} {name}:")
|
||||
lines.append(f" 场均进球 {gf/n:.2f}, 场均失球 {ga/n:.2f}")
|
||||
if n_shots: lines.append(f" 场均射门 {shots/n_shots:.1f}, 射正 {sot/n_shots:.1f}")
|
||||
if n_poss: lines.append(f" 平均控球 {poss/n_poss:.1f}%")
|
||||
if n_xg: lines.append(f" 场均 xG {xg/n_xg:.2f}, 场均被 xG {xga/n_xg:.2f}")
|
||||
else:
|
||||
lines.append(f" {label} {name}: 无比分数据")
|
||||
else:
|
||||
lines.append(f" {label} {name}: 无数据")
|
||||
return SliceResult(text="\n".join(lines), has_data=n_total > 0, n_records=n_total)
|
||||
Reference in New Issue
Block a user