"""Hermes/Agent reusable tools. These wrap the deterministic pipeline so an agent can execute natural-language commands like "Run tomorrow's Australian gallops analysis" or "Analyse Randwick Race 7" without re-implementing logic. Each function is a thin, well-documented wrapper returning plain data. Prediction math stays deterministic (see model/); the agent adds orchestration + explanation. """ from __future__ import annotations import logging from datetime import datetime, timezone from pathlib import Path from typing import Optional from .config import settings from .api.formfav_client import FormFavClient from .api.validator import validate_race_form from .data.normalizer import normalise_race from .data.storage import Storage from .model.analyse_race import analyse_race from .model.scoring import load_weights from .jobs.daily_prediction import tomorrow_australia from .reporting.dashboard import write_outputs logger = logging.getLogger("trifecta_bro.tools") def get_tomorrow_meetings(date: str | None = None) -> list[dict]: """Discover tomorrow's AU gallops meetings via FormFav.""" date = date or tomorrow_australia() client = FormFavClient() raw = client.get_meetings(date) meetings = [m for m in raw if m.get("country") == settings.country and not m.get("abandoned")] return [{"track": m.get("track"), "slug": m.get("slug"), "races": m.get("races", [])} for m in meetings] def get_race_form(date: str, track_slug: str, race: int) -> dict: """Fetch normalised race form for one race.""" client = FormFavClient() raw = client.get_race_form(date, track_slug, race) validate_race_form(raw) return normalise_race(raw).model_dump() def analyse_race_tool(date: str, track_slug: str, race: int) -> dict: """Full analysis for a single race (deterministic).""" client = FormFavClient() raw = client.get_race_form(date, track_slug, race) validate_race_form(raw) race_model = normalise_race(raw) return analyse_race(race_model, weights=load_weights()) def rank_runners(date: str, track_slug: str, race: int) -> list[dict]: a = analyse_race_tool(date, track_slug, race) return a.get("ranked", []) def generate_trifecta(date: str, track_slug: str, race: int) -> dict: a = analyse_race_tool(date, track_slug, race) return {"trifecta": a.get("trifecta"), "alternatives": a.get("alternatives"), "confidence": a.get("confidence")} def save_prediction(date: str, track_slug: str, race: int) -> dict: a = analyse_race_tool(date, track_slug, race) with Storage() as s: s.save_prediction({ "date": date, "track": a["track"], "track_slug": a["track_slug"], "race_number": a["race_number"], "model_version": settings.model_version, "generated_at": datetime.now(timezone.utc).isoformat(), "top3": a.get("top3"), "trifecta": a.get("trifecta"), "alternatives": a.get("alternatives"), "confidence": a["confidence"]["category"], "confidence_score": a["confidence"]["score"], "ranked_runners": [{"no": r["number"], "name": r["name"], "score": r["score"]} for r in a["ranked"]], "data_completeness": a.get("data_completeness"), }) return {"saved": True, "track": a["track"], "race": a["race_number"]} def run_daily_analysis(date: str | None = None, publish: bool = False) -> dict: """Run the complete pipeline for a date and return report paths.""" from ..jobs.daily_prediction import run as _run return _run(date=date) def why_ranked_first(date: str, track_slug: str, race: int) -> str: """Explain in plain language why the top runner ranked first.""" a = analyse_race_tool(date, track_slug, race) if a.get("skippable"): return f"Race skipped: {a.get('reason')}" top = a["ranked"][0] subs = top["sub_scores"] factors = sorted(((k, v) for k, v in subs.items() if v > 0), key=lambda x: x[1], reverse=True) parts = [f"Runner #{top['number']} {top['name']} ranks first (score {top['score']}) because:" f" strong {', '.join(k for k, _ in factors[:3])}."] return " ".join(parts)