Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files
app.py
CHANGED
|
@@ -47,6 +47,7 @@ from matchday.agent_trace import ( # noqa: E402
|
|
| 47 |
)
|
| 48 |
from matchday.intent import parse_intent # noqa: E402
|
| 49 |
from matchday.models import TripRequest # noqa: E402
|
|
|
|
| 50 |
from matchday.prompts import EXPLANATION_HINT # noqa: E402
|
| 51 |
from matchday.render import render_full # noqa: E402
|
| 52 |
from matchday.trip_tool import build_trip_packages, format_for_nemotron # noqa: E402
|
|
@@ -125,6 +126,78 @@ def _notice_status(result, *keywords: str) -> str:
|
|
| 125 |
return "fallback" if any(k in blob for k in keywords) else "done"
|
| 126 |
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
def _finalize_trace(trace: AgentTrace, trip, result, built_by: str) -> None:
|
| 129 |
"""Populate the final intent/grounding/evidence/ranking/outcome on the trace.
|
| 130 |
|
|
@@ -242,6 +315,26 @@ async def plan_trip(user_text: str) -> str:
|
|
| 242 |
yield _ev(type="progress", step="read", status="done", text="Read your request")
|
| 243 |
yield _ev(type="progress", step="extract", status="running", text="Understanding your trip")
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
agent = None
|
| 246 |
if USE_AGENT:
|
| 247 |
try:
|
|
|
|
| 47 |
)
|
| 48 |
from matchday.intent import parse_intent # noqa: E402
|
| 49 |
from matchday.models import TripRequest # noqa: E402
|
| 50 |
+
from matchday.wc2026 import resolve_match # noqa: E402
|
| 51 |
from matchday.prompts import EXPLANATION_HINT # noqa: E402
|
| 52 |
from matchday.render import render_full # noqa: E402
|
| 53 |
from matchday.trip_tool import build_trip_packages, format_for_nemotron # noqa: E402
|
|
|
|
| 126 |
return "fallback" if any(k in blob for k in keywords) else "done"
|
| 127 |
|
| 128 |
|
| 129 |
+
def _precheck_unrecognized_match(user_text: str):
|
| 130 |
+
"""Generic pre-agent fixture validator (grounding honesty, option 1).
|
| 131 |
+
|
| 132 |
+
Deterministically parse the request and ground the named match against the
|
| 133 |
+
verified 2026 fixture table BEFORE the agent picks a tool. If the user named
|
| 134 |
+
a matchup that isn't a real 2026 fixture, return ``(refusal_note, trip)`` so
|
| 135 |
+
``plan_trip`` can refuse honestly with the closest real alternatives and stop
|
| 136 |
+
— without ever invoking the agent loop.
|
| 137 |
+
|
| 138 |
+
Why this exists: Nemotron routes its own tools and, for some non-fixture
|
| 139 |
+
matchups (e.g. "Canada vs Morocco"), can non-deterministically choose
|
| 140 |
+
``clarify`` over ``build_trip_packages``. When it does, the grounding-refusal
|
| 141 |
+
path (the "isn't a 2026 fixture … Canada plays: …" note produced inside the
|
| 142 |
+
build tool) never runs, so the demo promises a refusal it never delivers.
|
| 143 |
+
Grounding the match deterministically up front guarantees every non-fixture
|
| 144 |
+
match is refused honestly, regardless of how the model routes.
|
| 145 |
+
|
| 146 |
+
Returns ``(note, trip_request)`` when a match is named AND unrecognized;
|
| 147 |
+
``None`` otherwise (no match named, parse failed, or the match IS real —
|
| 148 |
+
proceed to the normal agent path). Never raises.
|
| 149 |
+
"""
|
| 150 |
+
try:
|
| 151 |
+
parsed = parse_intent(user_text)
|
| 152 |
+
except Exception: # noqa: BLE001 — must never break the turn
|
| 153 |
+
return None
|
| 154 |
+
trip = getattr(parsed, "trip_request", None)
|
| 155 |
+
match_name = (getattr(trip, "match_name", "") or "") if trip is not None else ""
|
| 156 |
+
if not match_name or match_name == "the match": # _find_match's fallback sentinel
|
| 157 |
+
return None
|
| 158 |
+
match_name = _clean_match_name(match_name) # drop trailing month ("Morocco June" -> "Morocco")
|
| 159 |
+
try:
|
| 160 |
+
res = resolve_match(match_name)
|
| 161 |
+
except Exception: # noqa: BLE001
|
| 162 |
+
return None
|
| 163 |
+
if res.recognized or not res.note:
|
| 164 |
+
return None
|
| 165 |
+
try: # carry the CLEANED name onto the trip so the trace drawer matches the note
|
| 166 |
+
trip = trip.model_copy(update={"match_name": match_name})
|
| 167 |
+
except Exception: # noqa: BLE001
|
| 168 |
+
pass
|
| 169 |
+
return res.note, trip
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
_MONTH_TOKENS = {
|
| 173 |
+
"january", "february", "march", "april", "may", "june",
|
| 174 |
+
"july", "august", "september", "october", "november", "december",
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _clean_match_name(name: str) -> str:
|
| 179 |
+
"""Strip a trailing month token from each team in an 'A vs B' match name.
|
| 180 |
+
|
| 181 |
+
``parse_intent``'s ``_find_match`` greedily appends the next capitalized word
|
| 182 |
+
to a team name, so 'Canada vs Morocco June 18' parses to 'Canada vs Morocco
|
| 183 |
+
June' — the month leaks into the team and would surface in the refusal note
|
| 184 |
+
as "Morocco June plays: Brazil". Trimming trailing month tokens restores the
|
| 185 |
+
real team names for a clean note. Conservative: only strips trailing month
|
| 186 |
+
tokens, leaves everything else intact (multi-word teams unaffected).
|
| 187 |
+
"""
|
| 188 |
+
if " vs " not in name:
|
| 189 |
+
return name
|
| 190 |
+
|
| 191 |
+
def _strip(trial: str) -> str:
|
| 192 |
+
parts = trial.split()
|
| 193 |
+
while parts and parts[-1].lower().rstrip(".") in _MONTH_TOKENS:
|
| 194 |
+
parts.pop()
|
| 195 |
+
return " ".join(parts)
|
| 196 |
+
|
| 197 |
+
a, b = name.split(" vs ", 1)
|
| 198 |
+
return f"{_strip(a)} vs {_strip(b)}"
|
| 199 |
+
|
| 200 |
+
|
| 201 |
def _finalize_trace(trace: AgentTrace, trip, result, built_by: str) -> None:
|
| 202 |
"""Populate the final intent/grounding/evidence/ranking/outcome on the trace.
|
| 203 |
|
|
|
|
| 315 |
yield _ev(type="progress", step="read", status="done", text="Read your request")
|
| 316 |
yield _ev(type="progress", step="extract", status="running", text="Understanding your trip")
|
| 317 |
|
| 318 |
+
# ── Generic pre-agent fixture validator (grounding honesty). Ground the named
|
| 319 |
+
# match deterministically BEFORE the agent picks a tool: a non-real 2026
|
| 320 |
+
# fixture is refused with the closest real alternatives and we stop, so the
|
| 321 |
+
# refusal never depends on Nemotron choosing build_trip_packages over clarify.
|
| 322 |
+
_pre = _precheck_unrecognized_match(user_text)
|
| 323 |
+
if _pre is not None:
|
| 324 |
+
_refusal_note, _pre_trip = _pre
|
| 325 |
+
trace.set_intent(_pre_trip)
|
| 326 |
+
trace.set_grounding(recognized=False, note=_refusal_note)
|
| 327 |
+
trace.set_outcome(
|
| 328 |
+
mode="deterministic", status="clarify",
|
| 329 |
+
notes=["Pre-agent fixture check: named match is not a real 2026 fixture."],
|
| 330 |
+
model=_TRACE_MODEL, rounds=0,
|
| 331 |
+
)
|
| 332 |
+
yield _ev(type="trace", data=trace.to_dict()) # grounding-refusal proof
|
| 333 |
+
yield _ev(type="progress", step="extract", status="done", text="Match checked")
|
| 334 |
+
yield _ev(type="progress", step="ready", status="fallback", text="Match not found")
|
| 335 |
+
yield _ev(type="clarify", text=_refusal_note)
|
| 336 |
+
return
|
| 337 |
+
|
| 338 |
agent = None
|
| 339 |
if USE_AGENT:
|
| 340 |
try:
|