Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- matchday/agent.py +4 -3
- matchday/prompts.py +27 -6
- matchday/wc2026.py +41 -6
matchday/agent.py
CHANGED
|
@@ -78,9 +78,10 @@ TOOL_SPECS: list[dict[str, Any]] = [
|
|
| 78 |
"function": {
|
| 79 |
"name": "web_search",
|
| 80 |
"description": (
|
| 81 |
-
"Search the web for supplemental FIFA/Vancouver context
|
| 82 |
-
"times, venue policies
|
| 83 |
-
"weather
|
|
|
|
| 84 |
),
|
| 85 |
"parameters": WebSearchArgs.model_json_schema(),
|
| 86 |
},
|
|
|
|
| 78 |
"function": {
|
| 79 |
"name": "web_search",
|
| 80 |
"description": (
|
| 81 |
+
"Search the web for supplemental FIFA/Vancouver context and "
|
| 82 |
+
"current local facts (kick-off times, venue policies, transit, "
|
| 83 |
+
"current weather right now). NOT for prices, flights, hotels, or "
|
| 84 |
+
"the match-day weather FORECAST — those come from build_trip_packages."
|
| 85 |
),
|
| 86 |
"parameters": WebSearchArgs.model_json_schema(),
|
| 87 |
},
|
matchday/prompts.py
CHANGED
|
@@ -77,6 +77,18 @@ Tool-use rules:
|
|
| 77 |
- UNDERSTAND FIRST, THEN ACT. Read what the traveler actually wants before
|
| 78 |
calling a tool. Capture their intent: the origin city/airport, the match,
|
| 79 |
the dates, the budget, the vibe.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
- If the request is clear (you can infer an origin, a match, and at least one
|
| 81 |
date), call `build_trip_packages` right away — it runs all searches in
|
| 82 |
parallel and returns 3 scored packages. A match written as "playoff winner",
|
|
@@ -90,12 +102,21 @@ Tool-use rules:
|
|
| 90 |
the user asks about another city or a non-World-Cup trip, do NOT plan it —
|
| 91 |
say you specialize in Vancouver 2026 World Cup trips and offer to plan one
|
| 92 |
of those instead.
|
| 93 |
-
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
- Do not call a tool you already called with identical arguments this turn.
|
| 100 |
- When you have the packages, write a concise comparison: name each package's
|
| 101 |
strength (Cheapest / Safest Arrival / Closest to Stadium), cite the real
|
|
|
|
| 77 |
- UNDERSTAND FIRST, THEN ACT. Read what the traveler actually wants before
|
| 78 |
calling a tool. Capture their intent: the origin city/airport, the match,
|
| 79 |
the dates, the budget, the vibe.
|
| 80 |
+
- CLASSIFY THE INTENT, then act once. Pick exactly one path:
|
| 81 |
+
(a) TRIP — origin + match + date are present or inferable -> build_trip_packages.
|
| 82 |
+
(b) INCOMPLETE TRIP — a trip is intended but origin OR date is genuinely missing
|
| 83 |
+
-> clarify with ONE specific question offering concrete options.
|
| 84 |
+
(c) FACTUAL / LOCAL question — a question ABOUT the World Cup or Vancouver
|
| 85 |
+
(today's date, the BC Place schedule, kickoff time, how to get to the
|
| 86 |
+
stadium, the CURRENT weather right now, local info) with NO intent to book
|
| 87 |
+
-> answer it directly (see the "FACTUAL / LOCAL questions" rule below); do
|
| 88 |
+
not call build_trip_packages or ask for trip details. "What's the weather
|
| 89 |
+
today in Vancouver?" and "what's today's date?" are (c), not (a).
|
| 90 |
+
(d) OUT OF SCOPE — another destination or a non-World-Cup trip -> decline and
|
| 91 |
+
offer a Vancouver World Cup trip.
|
| 92 |
- If the request is clear (you can infer an origin, a match, and at least one
|
| 93 |
date), call `build_trip_packages` right away — it runs all searches in
|
| 94 |
parallel and returns 3 scored packages. A match written as "playoff winner",
|
|
|
|
| 102 |
the user asks about another city or a non-World-Cup trip, do NOT plan it —
|
| 103 |
say you specialize in Vancouver 2026 World Cup trips and offer to plan one
|
| 104 |
of those instead.
|
| 105 |
+
- FACTUAL / LOCAL questions (intent (c) above): answer directly and briefly
|
| 106 |
+
(1-3 lines). If you already have the answer — today's date is in your
|
| 107 |
+
instructions; an evening kickoff defaults to 7:00 PM Pacific — answer with NO
|
| 108 |
+
tool call. If you need data (the BC Place schedule, a kickoff time, transit,
|
| 109 |
+
the CURRENT weather right now), call `web_search` (preferring authoritative
|
| 110 |
+
sources — fifa.com / vancouverfwc26.ca / bcplace.com / translink.ca for
|
| 111 |
+
schedule, venue and transit; weather.gc.ca / Environment Canada for current
|
| 112 |
+
weather) and answer from the result. Do
|
| 113 |
+
not ask for trip details a factual question does not need, and do not force
|
| 114 |
+
build_trip_packages for a question that isn't a trip request.
|
| 115 |
+
- TRIP weather, prices, flights and hotels (the match-day FORECAST, airfare, a
|
| 116 |
+
hotel rate) come ONLY from `build_trip_packages` — never from web_search. So
|
| 117 |
+
"what's the weather TODAY in Vancouver?" (current, now) -> web_search;
|
| 118 |
+
"what's the weather for the Canada vs Qatar game?" (a trip forecast) ->
|
| 119 |
+
build_trip_packages. web_search is never used for trip pricing or trip weather.
|
| 120 |
- Do not call a tool you already called with identical arguments this turn.
|
| 121 |
- When you have the packages, write a concise comparison: name each package's
|
| 122 |
strength (Cheapest / Safest Arrival / Closest to Stadium), cite the real
|
matchday/wc2026.py
CHANGED
|
@@ -60,10 +60,21 @@ _FIXTURES: list[Fixture] = [
|
|
| 60 |
Fixture("Bosnia and Herzegovina vs Qatar", date(2026, 6, 24), "Lumen Field", "Seattle", "", "B"),
|
| 61 |
# Group C — Brazil's group (Brazil, Morocco, Scotland, Haiti)
|
| 62 |
Fixture("Brazil vs Morocco", date(2026, 6, 13), "MetLife Stadium", "New York / New Jersey", "18:00 ET", "C"),
|
| 63 |
-
#
|
| 64 |
Fixture("Australia vs Türkiye", date(2026, 6, 13), "BC Place", "Vancouver", "21:00 PT", "D"),
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
]
|
| 68 |
|
| 69 |
# Spoken team-name aliases → canonical token used for matching. Lets "Bosnia"
|
|
@@ -87,13 +98,31 @@ def _norm_team(raw: str) -> str:
|
|
| 87 |
return _TEAM_ALIASES.get(t, t)
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
def _split_match(match_name: str) -> list[str]:
|
| 91 |
-
"""Split 'A vs B' into normalized team tokens (any order).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
import re
|
| 93 |
-
parts = re.split(r"\b(?:vs?\.?|versus|[-–]
|
| 94 |
if len(parts) != 2:
|
| 95 |
return []
|
| 96 |
-
return [_norm_team(parts[0]), _norm_team(parts[1])]
|
| 97 |
|
| 98 |
|
| 99 |
def _teams_match(query_teams: list[str], fixture: Fixture) -> bool:
|
|
@@ -107,6 +136,12 @@ def _teams_match(query_teams: list[str], fixture: Fixture) -> bool:
|
|
| 107 |
for f in ft:
|
| 108 |
if q == f or (len(q) >= 3 and (q in f or f in q)):
|
| 109 |
return f
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
return None
|
| 111 |
a, b = hit(query_teams[0]), hit(query_teams[1])
|
| 112 |
return bool(a and b and a != b)
|
|
|
|
| 60 |
Fixture("Bosnia and Herzegovina vs Qatar", date(2026, 6, 24), "Lumen Field", "Seattle", "", "B"),
|
| 61 |
# Group C — Brazil's group (Brazil, Morocco, Scotland, Haiti)
|
| 62 |
Fixture("Brazil vs Morocco", date(2026, 6, 13), "MetLife Stadium", "New York / New Jersey", "18:00 ET", "C"),
|
| 63 |
+
# Group D opener at BC Place (Vancouver) — verified Jun 13.
|
| 64 |
Fixture("Australia vs Türkiye", date(2026, 6, 13), "BC Place", "Vancouver", "21:00 PT", "D"),
|
| 65 |
+
# Group G (Belgium / Egypt / IR Iran / New Zealand) — NZ's two BC Place games.
|
| 66 |
+
# NZ vs Egypt is SUNDAY Jun 21 (FIFA match M40), NOT Jun 22 — verified against
|
| 67 |
+
# the official FIFA Hospitality Vancouver schedule + Wikipedia Group G
|
| 68 |
+
# (re-checked 2026-06-15). The prior "Jun 22" entry was off by one day, which
|
| 69 |
+
# is exactly the "wrong match data" a user saw. NZ vs Belgium is M64, Fri Jun 26.
|
| 70 |
+
Fixture("New Zealand vs Egypt", date(2026, 6, 21), "BC Place", "Vancouver", "", "G"),
|
| 71 |
+
Fixture("New Zealand vs Belgium", date(2026, 6, 26), "BC Place", "Vancouver", "", "G"),
|
| 72 |
+
# Group J (Argentina's group). Argentina vs Austria is a REAL 2026 fixture but
|
| 73 |
+
# at AT&T Stadium (Dallas/Arlington), NOT Vancouver — corrected from a stale
|
| 74 |
+
# "BC Place / Group G" entry (neither team is in Group G). Kept (truthful) so a
|
| 75 |
+
# user who names it gets an honest "that's in Dallas, not Vancouver" redirect
|
| 76 |
+
# instead of a false "isn't a 2026 fixture".
|
| 77 |
+
Fixture("Argentina vs Austria", date(2026, 6, 22), "AT&T Stadium", "Dallas", "13:00 ET", "J"),
|
| 78 |
]
|
| 79 |
|
| 80 |
# Spoken team-name aliases → canonical token used for matching. Lets "Bosnia"
|
|
|
|
| 98 |
return _TEAM_ALIASES.get(t, t)
|
| 99 |
|
| 100 |
|
| 101 |
+
def _trim_team_suffix(side: str) -> str:
|
| 102 |
+
"""Cut a team string at the first standalone dash / 'at' that introduces a
|
| 103 |
+
trailing venue — e.g. 'New Zealand – BC Place' -> 'New Zealand'. Lets a user
|
| 104 |
+
name a match *with* its stadium ('Egypt vs New Zealand – BC Place') still
|
| 105 |
+
resolve instead of being falsely reported as 'not a fixture' (the exact
|
| 106 |
+
'I added the stadium and it refused' bug)."""
|
| 107 |
+
import re
|
| 108 |
+
side = re.split(r"\s+[-–—]\s+", side, maxsplit=1)[0] # ' – BC Place'
|
| 109 |
+
side = re.split(r"\s+(?:at|@)\s+", side, maxsplit=1, flags=re.IGNORECASE)[0] # ' at BC Place'
|
| 110 |
+
return side.strip()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
def _split_match(match_name: str) -> list[str]:
|
| 114 |
+
"""Split 'A vs B' into normalized team tokens (any order).
|
| 115 |
+
|
| 116 |
+
Each side is trimmed of a trailing venue suffix before normalization, so
|
| 117 |
+
'Egypt vs New Zealand – BC Place' yields ['egypt','newzealand'] rather than a
|
| 118 |
+
contaminated token ('new zealand – bc place') that fails to match the fixture
|
| 119 |
+
table. Separators accepted: 'vs' / 'v' / 'versus', or a space-padded dash.
|
| 120 |
+
"""
|
| 121 |
import re
|
| 122 |
+
parts = re.split(r"\b(?:vs?\.?|versus)\b|\s+[-–—]\s+", match_name, maxsplit=1, flags=re.IGNORECASE)
|
| 123 |
if len(parts) != 2:
|
| 124 |
return []
|
| 125 |
+
return [_norm_team(_trim_team_suffix(parts[0])), _norm_team(_trim_team_suffix(parts[1]))]
|
| 126 |
|
| 127 |
|
| 128 |
def _teams_match(query_teams: list[str], fixture: Fixture) -> bool:
|
|
|
|
| 136 |
for f in ft:
|
| 137 |
if q == f or (len(q) >= 3 and (q in f or f in q)):
|
| 138 |
return f
|
| 139 |
+
# space-insensitive containment: a venue suffix absorbed into the
|
| 140 |
+
# team (e.g. the matcher seeing 'New Zealand BC' from '…BC Place')
|
| 141 |
+
# still matches the canonical 'newzealand'. len(f)>=4 avoids
|
| 142 |
+
# short-token false positives.
|
| 143 |
+
if len(f) >= 4 and f in q.replace(" ", ""):
|
| 144 |
+
return f
|
| 145 |
return None
|
| 146 |
a, b = hit(query_teams[0]), hit(query_teams[1])
|
| 147 |
return bool(a and b and a != b)
|