Spaces:
Running on Zero
Running on Zero
File size: 14,769 Bytes
41e0c9e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | """BAYLINE — the Bay Area Transit agent (second mode of FLIGHTDECK).
Same agent pattern as the flight agent: the LLM picks a tool, the tool runs a
real 511.org call, the LLM reasons over the result. 511 has no trip-planner, so
"fastest route" is reasoned from real-time departures + a scheduled estimate +
live traffic. Reuses the flight agent's trace + JSON-extraction plumbing.
"""
from __future__ import annotations
import datetime as dt
import re
import time
import liquid
import transit
from agent import _extract_json, _new_trace, _save_trace
AVG_BART_KMH = 50.0 # incl. station dwell; rough, labeled as estimate
DRIVE_KMH = 60.0 # free-flow-ish baseline
ROUTE_FACTOR = 1.25 # straight-line -> path correction
TRANSIT_KEYWORDS = {
"bart", "caltrain", "muni", "train", "trains", "bus", "buses", "ferry",
"transit", "departure", "departures", "depart", "leave", "leaving",
"arrive", "arrives", "station", "stop", "route", "fastest", "quickest",
"commute", "traffic", "travel", "trip", "ride", "subway", "metro",
"schedule", "next", "delay", "delays", "way", "get", "go", "bridge",
"freeway", "highway", "incident", "crash", "accident",
}
BAY_PLACES = {
"embarcadero", "berkeley", "oakland", "fremont", "richmond", "dublin",
"pleasanton", "millbrae", "sfo", "airport", "daly", "colma", "concord",
"walnut", "antioch", "pittsburg", "hayward", "san", "francisco", "jose",
"mateo", "bruno", "leandro", "rafael", "mountain", "view", "palo", "alto",
"redwood", "sunnyvale", "santa", "clara", "diridon", "bay", "peninsula",
"downtown", "mission", "civic", "powell", "montgomery", "rockridge",
"macarthur", "lake", "merritt", "coliseum", "city",
}
SYSTEM_PROMPT = """You are BAYLINE, a Bay Area public-transit assistant.
You help people find the fastest way around the SF Bay Area using LIVE 511 data.
TOOLS (call exactly one):
1. plan_trip - fastest way between two places. args: {"origin","destination"}
2. next_departures - real-time departures at one place. args: {"place"}
3. traffic - current road incidents. args: {"area"} (area optional, "" = all)
Reply with ONE JSON object only. Shapes:
{"tool":"plan_trip","origin":"Berkeley","destination":"SFO"}
{"tool":"next_departures","place":"Embarcadero"}
{"tool":"traffic","area":"Bay Bridge"}
{"tool":"none","answer":"<refusal>"}
Rules:
- "from X to Y" / "X to Y" / "fastest way to Y from X" => plan_trip.
- "when/next/departures at Z" => next_departures.
- "traffic/incidents/crash" => traffic.
- If it is NOT about Bay Area travel, use tool "none".
Output JSON only.
Examples:
User: fastest way from Berkeley to SFO
{"tool":"plan_trip","origin":"Berkeley","destination":"SFO"}
User: when is the next train from Embarcadero
{"tool":"next_departures","place":"Embarcadero"}
User: any traffic on the Bay Bridge
{"tool":"traffic","area":"Bay Bridge"}
User: tell me a joke
{"tool":"none","answer":"I only help with Bay Area transit and traffic."}"""
# --------------------------------------------------------------------------- #
def _in_scope(query: str) -> bool:
words = set(re.findall(r"[a-z]+", query.lower()))
return bool(words & TRANSIT_KEYWORDS or words & BAY_PLACES)
def _clean(text: str) -> str:
fill = {"the", "a", "an", "to", "from", "at", "in", "of", "me", "please",
"next", "train", "trains", "bart", "departures", "departure", "when",
"is", "are", "whats", "what", "fastest", "quickest", "way", "get",
"how", "do", "i", "go", "leaving", "leave", "near", "around", "for"}
toks = [t for t in re.findall(r"[a-z0-9]+", text.lower()) if t not in fill]
return " ".join(toks).strip()
def _regex_plan(query):
q = query.lower().strip()
if re.search(r"\b(traffic|incident|incidents|crash|accident|road)\b", q):
m = re.search(r"\b(?:on|near|around|at|in)\s+(.*)", q)
return {"tool": "traffic", "area": (_clean(m.group(1)) if m else "")}
if " to " in q:
left, right = q.split(" to ", 1)
o, d = _clean(left), _clean(right)
if o and d:
return {"tool": "plan_trip", "origin": o, "destination": d}
m = re.search(r"\bfrom\s+(.*)", q)
if m and _clean(m.group(1)):
return {"tool": "next_departures", "place": _clean(m.group(1))}
p = _clean(q)
if p:
return {"tool": "next_departures", "place": p}
return None
# ---- tools ----------------------------------------------------------------- #
def _eta_minutes(km, kmh):
return round(km * ROUTE_FACTOR / kmh * 60)
def _tool_plan_trip(args):
o = transit.resolve_place(args.get("origin", ""))
d = transit.resolve_place(args.get("destination", ""))
if not o or not d:
miss = args.get("origin") if not o else args.get("destination")
return {"error": f"could not find a station for {miss!r}"}, []
km = transit.haversine_km(o["lat"], o["lon"], d["lat"], d["lon"])
deps, _ = transit.station_departures(o["operator"], o["name"], limit=6)
next_wait = deps[0]["minutes"] if deps else None
in_veh = _eta_minutes(km, AVG_BART_KMH)
transit_total = (next_wait or 0) + in_veh
evs, _ = transit.traffic_events()
drive_min = _eta_minutes(km, DRIVE_KMH)
drive_adj = drive_min + min(25, 3 * len(evs)) # crude congestion penalty
markers = [
{"lat": o["lat"], "lon": o["lon"], "label": f"FROM {o['name']}", "kind": "origin"},
{"lat": d["lat"], "lon": d["lon"], "label": f"TO {d['name']}", "kind": "dest"},
]
for e in evs[:8]:
if e.get("lat") and e.get("lon"):
markers.append({"lat": e["lat"], "lon": e["lon"],
"label": f"{e['type']}: {e['headline'][:60]}", "kind": "incident"})
# Deterministic recommendation (the tiny LLM can't be trusted to compare).
if transit_total <= drive_adj:
best, delta = "BART", drive_adj - transit_total
else:
best, delta = "Driving", transit_total - drive_adj
result = {
"origin": o["name"], "destination": d["name"],
"operator": transit.OPERATOR_NAMES.get(o["operator"], o["operator"]),
"distance_km": round(km, 1),
"transit": {"next_departure_min": next_wait, "in_vehicle_min_est": in_veh,
"total_min_est": transit_total,
"departures": deps[:4]},
"driving": {"est_min": drive_adj, "active_incidents": len(evs)},
"recommendation": {"mode": best, "saves_min": delta,
"transit_min": transit_total, "drive_min": drive_adj},
}
return result, markers
def _tool_next_departures(args):
s = transit.resolve_place(args.get("place", ""))
if not s:
return {"error": f"could not find a station for {args.get('place')!r}"}, []
deps, _ = transit.station_departures(s["operator"], s["name"], limit=8)
markers = [{"lat": s["lat"], "lon": s["lon"],
"label": f"{s['name']} ({transit.OPERATOR_NAMES.get(s['operator'], s['operator'])})",
"kind": "origin"}]
return {"station": s["name"],
"operator": transit.OPERATOR_NAMES.get(s["operator"], s["operator"]),
"departures": deps}, markers
def _tool_traffic(args):
area = args.get("area") or ""
evs, _ = transit.traffic_events(area_query=area or None)
markers = [{"lat": e["lat"], "lon": e["lon"],
"label": f"{e['type']}: {e['headline'][:60]}", "kind": "incident"}
for e in evs if e.get("lat") and e.get("lon")]
return {"area": area or "Bay Area", "count": len(evs), "events": evs}, markers
TOOL_IMPLS = {
"plan_trip": _tool_plan_trip,
"next_departures": _tool_next_departures,
"traffic": _tool_traffic,
}
def _summarize(tool, result):
if "error" in result:
return f"Lookup problem: {result['error']}"
if tool == "plan_trip":
t, dr, rec = result["transit"], result["driving"], result["recommendation"]
best_min = rec["transit_min"] if rec["mode"] == "BART" else rec["drive_min"]
verdict = (f"**Fastest: {rec['mode']}** (~{best_min} min) — ~{rec['saves_min']} "
"min faster than the alternative." if rec["saves_min"] > 1 else
f"**{rec['mode']} and driving are about the same** (~{best_min} min).")
lines = [
verdict,
f"Trip {result['origin']} → {result['destination']} (~{result['distance_km']} km).",
f"• {result['operator']}: next train in {t['next_departure_min']} min, "
f"~{t['in_vehicle_min_est']} min ride, **~{t['total_min_est']} min total** (est).",
f"• Driving: **~{dr['est_min']} min** est ({dr['active_incidents']} active incidents region-wide).",
f"Next departures from {result['origin']} (all directions): " + ("; ".join(
f"{d['line']}→{d['destination']} in {d['minutes']}m"
for d in t["departures"]) or "none"),
"_Transit times are straight-line estimates (511 has no trip planner; "
"transfers not modeled)._",
]
return "\n".join(lines)
if tool == "next_departures":
deps = result["departures"]
head = f"{result['station']} ({result['operator']}) next departures:"
body = "; ".join(f"{d['line']}->{d['destination']} in {d['minutes']}m"
for d in deps) or "no real-time departures right now"
return head + " " + body
if tool == "traffic":
evs = result["events"]
head = f"{result['count']} active incident(s) in {result['area']}:"
body = "; ".join(f"{e['type']} on {e['roads'] or '?'}" for e in evs[:6])
return head + " " + (body or "none")
return str(result)
def _validate(action, query):
rx = _regex_plan(query)
if not action or action.get("tool") in (None, "none", ""):
if rx and rx.get("tool") in TOOL_IMPLS:
return rx, "override: model refused an in-scope query"
return action, None
reason = None
# strong route signal -> plan_trip
if rx and rx.get("tool") == "plan_trip" and action.get("tool") != "plan_trip":
return rx, "override: query has explicit origin->destination"
# traffic area: trust the query, not the model (it hallucinates roads).
if action.get("tool") == "traffic" and rx and rx.get("tool") == "traffic":
if rx.get("area") and rx.get("area") != action.get("area"):
action["area"] = rx["area"]
reason = "override: traffic area taken from query"
# fill missing args from regex
if action.get("tool") == "plan_trip" and not (action.get("origin") and action.get("destination")):
if rx and rx.get("tool") == "plan_trip":
return rx, "repair: filled trip endpoints"
return {"tool": "none", "answer": "Tell me both a start and a destination."}, "repair: no endpoints"
if action.get("tool") == "next_departures" and not action.get("place"):
if rx and rx.get("place"):
action["place"] = rx["place"]
reason = "repair: filled place"
return action, reason
# --------------------------------------------------------------------------- #
def run(query: str, max_tokens=380):
trace = _new_trace(query)
trace["mode_kind"] = "transit"
use_llm = liquid.available()
trace["agent_mode"] = "transit-llm" if use_llm else "transit-regex"
if not _in_scope(query):
ans = ("I'm the Bay Area transit assistant — try 'fastest way from "
"Berkeley to SFO', 'next train from Embarcadero', or 'traffic "
"on the Bay Bridge'.")
trace["answer"] = ans
trace["agent_mode"] += "+scope-refused"
path = _save_trace(trace)
return {"answer": ans, "markers": [], "result": None,
"trace_path": path, "trace_id": trace["trace_id"],
"tool_calls": [], "mode": trace["agent_mode"]}
action = None
if use_llm:
try:
raw, latency = liquid.complete(
[{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": query}],
max_tokens=200, temperature=0.0)
except Exception as e: # noqa: BLE001
raw, latency, use_llm = f"(model error: {e})", 0, False
trace["agent_mode"] = "transit-regex"
action = _extract_json(raw)
trace["steps"].append({"step": 1, "phase": "plan", "model_raw": raw,
"parsed_action": action, "latency_ms": latency})
if action is None:
action = _regex_plan(query)
trace["steps"].append({"step": 1, "phase": "plan-fallback",
"parsed_action": action})
action, override = _validate(action, query)
if override:
trace["steps"].append({"step": 1, "phase": "validate",
"final_action": action, "override_reason": override})
if not action or action.get("tool") in (None, "none", ""):
ans = (action or {}).get("answer", "I can only help with Bay Area transit.")
trace["answer"] = ans
path = _save_trace(trace)
return {"answer": ans, "markers": [], "result": None,
"trace_path": path, "trace_id": trace["trace_id"],
"tool_calls": [], "mode": trace["agent_mode"]}
tool = action.get("tool")
impl = TOOL_IMPLS.get(tool)
t0 = time.time()
try:
result, markers = impl(action)
error = result.get("error") if isinstance(result, dict) else None
except transit.Transit511Error as e:
result, markers, error = {"error": str(e)}, [], str(e)
except Exception as e: # noqa: BLE001
result, markers, error = {"error": repr(e)}, [], repr(e)
latency = int((time.time() - t0) * 1000)
call = {"tool": tool, "args": {k: v for k, v in action.items() if k != "tool"},
"latency_ms": latency, "error": error,
"result_count": len(markers)}
trace["tool_calls"].append(call)
trace["steps"].append({"step": 2, "phase": "act", **call})
trace["flights_returned"] = len(markers)
# Answer is the deterministic, fact-checked summary — a 350M model flips
# numeric comparisons, and "fastest route" must be correct. The LLM still
# drives the agentic part (tool selection) above.
summary = _summarize(tool, result)
answer = f"Couldn't complete that: {error}" if error else summary
trace["answer"] = answer
path = _save_trace(trace)
return {"answer": answer, "markers": markers, "result": result,
"trace_path": path, "trace_id": trace["trace_id"],
"tool_calls": [c["tool"] for c in trace["tool_calls"]],
"mode": trace["agent_mode"]}
|