chess-tutor / src /chess_tutor /web /payload.py
github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
5.2 kB
"""Structured coaching payload for the web UI."""
from __future__ import annotations
from typing import Any
from chess_tutor.cohort.opening_families import bucket_display_name
from chess_tutor.playstyle_coaching.opening_climbers import position_heading
from chess_tutor.inference.output import format_elo_outlook_lines
from chess_tutor.inference.pipeline import InferenceResult
from chess_tutor.web.embedding import build_tsne_web_plots
from chess_tutor.web.narrative_sections import build_web_narrative_sections
from chess_tutor.web.puzzle_series import build_puzzle_series
from chess_tutor.web.sections import (
DEFAULT_WEB_UI_LAYOUT,
WebSectionId,
WebSectionSpec,
ensure_complete_layout,
section_enabled,
ui_layout_config,
)
from chess_tutor.web.style_elo_plot import build_style_scale
from chess_tutor.web.style_traits import build_style_traits
from chess_tutor.web.version import version_info
def _opening_cards(result: InferenceResult) -> list[dict[str, Any]]:
plan = result.actionable_plan
if plan is None or not plan.opening_insights:
return []
cards: list[dict[str, Any]] = []
for rank, ins in enumerate(plan.opening_insights, start=1):
title = position_heading(
rank,
position_label=ins.position_label,
bucket=ins.bucket,
)
cards.append(
{
"rank": rank,
"bucket": title,
"context": bucket_display_name(ins.bucket) if ins.bucket else None,
"position_label": ins.position_label,
"you_play": ins.user_move_san,
"you_play_moves": ins.user_move_uci,
"you_play_share": ins.user_move_at_position_share,
"sample_share": ins.user_current_share,
"bucket_n_games": ins.position_visit_count or ins.bucket_n_games,
"recommended": ins.recommended_move_san,
"recommended_moves": ins.recommended_move_uci,
"recommended_label": ins.recommended_opening or ins.family_label,
"climber_switch_rate": ins.climber_switch_rate,
}
)
return cards
def build_web_payload(
result: InferenceResult,
*,
corpus_id: str = "standard_600",
layout: tuple[WebSectionSpec, ...] | None = None,
) -> dict[str, Any]:
"""JSON-serializable report for the local web UI."""
resolved_layout = ensure_complete_layout(
layout if layout is not None else DEFAULT_WEB_UI_LAYOUT
)
ctx = result.coaching_report.context
outlook_lines = format_elo_outlook_lines(result, ctx)
narrative = result.coaching_narrative.strip()
meta = (
dict(result.actionable_plan.metadata)
if result.actionable_plan is not None
else {}
)
payload: dict[str, Any] = {
"ui_layout": ui_layout_config(sections=resolved_layout),
"header": {
"username": result.username,
"site": result.site,
"player_elo": result.player_elo,
"native_player_elo": result.native_player_elo,
"elo_bracket": ctx.elo_bracket,
"n_games": result.n_games_downloaded,
"policy": result.coaching_policy,
},
"rating_outlook": outlook_lines,
"narrative_markdown": narrative,
"lichess_study_url": meta.get("lichess_study_url"),
"lichess_study_error": meta.get("lichess_study_error"),
"version": version_info(),
}
if section_enabled(WebSectionId.NARRATIVE, layout=resolved_layout):
payload["narrative_sections"] = build_web_narrative_sections(narrative)
else:
payload["narrative_sections"] = []
if section_enabled(WebSectionId.OPENINGS, layout=resolved_layout):
payload["opening_cards"] = _opening_cards(result)
else:
payload["opening_cards"] = []
if section_enabled(WebSectionId.PUZZLES, layout=resolved_layout):
payload["puzzle_series"] = build_puzzle_series(result.actionable_plan)
else:
payload["puzzle_series"] = []
if section_enabled(WebSectionId.TSNE_PLOT, layout=resolved_layout) or section_enabled(
WebSectionId.ELO_TSNE_PLOT, layout=resolved_layout
):
embedding_plots = build_tsne_web_plots(result, corpus_id=corpus_id)
payload["tsne_plot"] = (
embedding_plots["tsne_plot"]
if section_enabled(WebSectionId.TSNE_PLOT, layout=resolved_layout)
else None
)
payload["elo_tsne_plot"] = (
embedding_plots["elo_tsne_plot"]
if section_enabled(WebSectionId.ELO_TSNE_PLOT, layout=resolved_layout)
else None
)
else:
payload["tsne_plot"] = None
payload["elo_tsne_plot"] = None
if section_enabled(WebSectionId.STYLE_SCALE, layout=resolved_layout):
payload["style_scale"] = build_style_scale(result, corpus_id=corpus_id)
else:
payload["style_scale"] = None
if section_enabled(WebSectionId.STYLE_TRAITS, layout=resolved_layout):
payload["style_traits"] = build_style_traits(result)
else:
payload["style_traits"] = []
return payload