Quazim0t0 commited on
Commit
19d2650
·
verified ·
1 Parent(s): dc1c010

Upload 48 files

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Flight Transit Agent
3
  emoji: 🛰️
4
  colorFrom: blue
5
  colorTo: purple
@@ -10,3 +10,184 @@ app_file: app.py
10
  pinned: false
11
  ---
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FLIGHTDECK 🛰️
3
  emoji: 🛰️
4
  colorFrom: blue
5
  colorTo: purple
 
10
  pinned: false
11
  ---
12
 
13
+ # FLIGHTDECK 🛰️
14
+
15
+ Two LLM agents in one glow-in-the-dark, 80's-hacker app, switchable by tab:
16
+
17
+ - **✈️ FLIGHTS / SKYLINE** (glow cyan) — a flight agent over the FlightRadar24
18
+ API, rendered on a transparent neon 3D globe.
19
+ - **🚉 BAY TRANSIT / BAYLINE** (glow orange) — a Bay Area transit agent over the
20
+ 511.org API, with route lines on the same globe.
21
+
22
+ Built for the HuggingFace hackathon.
23
+
24
+ [Note: the rest of this README body was reconstructed after an accidental
25
+ overwrite. The frontmatter above is the part you asked me to fill in and is
26
+ correct. The body below should be reviewed against the original before
27
+ publishing — see the "Restoring this README" note at the end.]
28
+
29
+ ## What it does
30
+
31
+ Type a query in plain English, the LLM picks a tool, the matching API call
32
+ runs, and the answer shows up on the globe. Examples:
33
+
34
+ - ✈️ `flights from London to Dubai` — both endpoints drawn as an arc on the globe.
35
+ - ✈️ `arrivals into JFK` — rings around JFK showing live inbound traffic.
36
+ - ✈️ `departures from LAX` — outbound tracks fanning out from LAX.
37
+ - 🚉 `MUNI arrivals at Powell` — nearby stops highlighted, predicted arrivals
38
+ listed in the sidebar.
39
+
40
+ ### ✈️ SKYLINE — flight agent
41
+
42
+ Powered by FlightRadar24's live API. Set `FR24_API_TOKEN` to use this tab.
43
+
44
+ | tool | FR24 call it runs |
45
+ |---|---|
46
+ | `flights_in_box` | `GET /api/live/flight-positions/full?bounds=...` |
47
+ | `flights_by_airline` | `GET /api/live/flight-positions/full?airline=...` |
48
+ | `flights_by_flight` | `GET /api/live/flight-positions/full?flight=...` |
49
+ | `flights_by_callsign` | `GET /api/live/flight-positions/full?callsign=...` |
50
+ | `flights_by_reg` | `GET /api/live/flight-positions/full?reg=...` |
51
+ | `flights_by_mil` | `GET /api/live/flight-positions/mil?bounds=...` |
52
+ | `flight_by_id` | `GET /api/flights/detail?flight_id=...` |
53
+ | `airports_in_box` | `GET /api/static/airports?bounds=...` |
54
+
55
+ Flow per query: **plan** (the LLM picks a tool + args) → **act** (the real FR24
56
+ call runs) → **render** (results placed on the globe + sidebar).
57
+
58
+ ### 🚉 BAYLINE — Bay Area transit agent
59
+
60
+ Powered by 511.org (modelled after their public stop-monitoring and route
61
+ modeling) and labeled as such. Set `TRANSIT_511_API_KEY` to use this tab.
62
+
63
+ ## Tracing the agent
64
+
65
+ The `traces/` folder is created at runtime. Every query writes a JSONL trace
66
+ of:
67
+ - the LLM's tool call + args
68
+ - the exact FR24 request URL
69
+ - the result count
70
+ - per-step latency
71
+
72
+ Use this to debug what the model is actually choosing.
73
+
74
+ ## Run it locally
75
+
76
+ ```bash
77
+ git clone <this repo>
78
+ cd flight-globe-app
79
+ python -m venv .venv && . .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
80
+ pip install -r requirements.txt
81
+
82
+ # Put your keys in the environment (or a local .env, which is gitignored):
83
+ export FR24_API_TOKEN="your_fr24_token"
84
+ export TRANSIT_511_API_KEY="your_511_key"
85
+ python app.py # -> http://127.0.0.1:7860
86
+ ```
87
+
88
+ Locally the model runs on your GPU if you have one, otherwise CPU. (`spaces` is
89
+ not needed locally — `@spaces.GPU` is a no-op off ZeroGPU.)
90
+
91
+ ## Deploying to a HuggingFace ZeroGPU Space
92
+
93
+ This Space is configured for **ZeroGPU** (dynamic GPU allocation). The relevant
94
+ config is already in place:
95
+
96
+ - `README.md` front-matter: `sdk: gradio`, `python_version: "3.12.12"` (a
97
+ ZeroGPU-supported Python), `app_file: app.py`.
98
+ - `requirements.txt`: `spaces`, `torch==2.8.0` (ZeroGPU needs CUDA torch ≥ 2.8),
99
+ `transformers`, `accelerate`.
100
+ - `liquid.py`: imports `spaces` before torch, places the model on `cuda` at
101
+ module level, and wraps generation in `@spaces.GPU`.
102
+
103
+ ### 1. Create the Space
104
+
105
+ 1. https://huggingface.co/new-space → **SDK: Gradio**.
106
+ 2. **Hardware: ZeroGPU.** This requires a **PRO** (personal) or **Team/Enterprise**
107
+ plan — ZeroGPU isn't available on free accounts. (If you can't use ZeroGPU,
108
+ pick `CPU basic` instead and change `torch==2.8.0` → `torch` in
109
+ `requirements.txt`; it'll run on CPU, just slower.)
110
+
111
+ ### 2. Push the code (everything **except** `.env`)
112
+
113
+ ```bash
114
+ git init && git add . && git commit -m "FLIGHTDECK"
115
+ git remote add origin https://huggingface.co/spaces/<your-username>/<space-name>
116
+ git push -u origin main
117
+ ```
118
+
119
+ `.env` is gitignored, so your local keys won't be uploaded. First build pulls
120
+ torch + transformers, so it takes a few minutes.
121
+
122
+ ### 3. Add the keys as Space Secrets
123
+
124
+ Space → **Settings → Variables and secrets → New secret** (use **Secret**, not
125
+ Variable, for the keys):
126
+
127
+ | Name | Value | Required? |
128
+ |---|---|---|
129
+ | `FR24_API_TOKEN` | your FR24 bearer token | **yes** for the FLIGHTDECK tab |
130
+ | `TRANSIT_511_API_KEY` | your 511.org key | **yes** for the BAY TRANSIT tab |
131
+ | `FR24_API_VERSION` | `v1` | optional |
132
+ | `LLM_REPO` | `LiquidAI/LFM2.5-350M` | optional (swap model) |
133
+ | `ZEROGPU_DURATION` | `60` | optional (max GPU seconds per call) |
134
+ | `DISABLE_LLM` | `0` | optional — `1` uses the regex planner only |
135
+
136
+ Secrets are injected as environment variables; `app.py` reads them via
137
+ `os.environ`, so no code changes are needed. The Space restarts on save.
138
+
139
+ ### 4. Notes
140
+
141
+ - The first query attaches a GPU and loads the model; ZeroGPU has a daily GPU
142
+ quota per account tier — see the ZeroGPU docs.
143
+ - If the GPU is unavailable for any reason, the agents fall back to the regex
144
+ planner so the Space still works.
145
+ - If you see `... is not set`, the secret name is case-sensitive — match the
146
+ table exactly.
147
+
148
+ ### Local dev (optional)
149
+
150
+ If you want to run the Space code locally with real keys, copy the example
151
+ file and edit it — your real `.env` is ignored by git and never uploaded:
152
+
153
+ ```powershell
154
+ Copy-Item .env.example .env
155
+ # edit .env and paste your real tokens
156
+ python app.py
157
+ ```
158
+
159
+ ## How it works
160
+
161
+ 1. Type a flight query in **ASK THE FLIGHT AGENT**, e.g.
162
+ `flights from London to Dubai`, `arrivals into JFK`, `departures from LAX`.
163
+ 2. The LLM plans the call, the matching FR24 tool runs, and the answer appears.
164
+ 3. Sidebar lists every plane with callsign, altitude, speed, heading, and
165
+ remaining ETA. **ETA** comes straight from FR24's `eta` field.
166
+
167
+ ## Notes
168
+
169
+ - The first query downloads the LiquidAI LFM2.5-350M weights from HuggingFace
170
+ and caches them; subsequent runs reuse the cache.
171
+ - FR24 enforces area-size and rate limits; large boxes ("World") are sampled and
172
+ the response is rate-limited.
173
+ - If the model can't load (no GPU/transformers, or the download fails), the
174
+ agents fall back to a deterministic regex planner so the app still works.
175
+
176
+ ---
177
+
178
+ ## Restoring this README — IMPORTANT
179
+
180
+ The body of this README (everything below the frontmatter) was reconstructed
181
+ after an accidental overwrite and may not be byte-for-byte identical to the
182
+ original. The **frontmatter** (between the `---` markers) is what you asked
183
+ me to fill in and is correct.
184
+
185
+ Before publishing, please verify the body against any of these sources if you
186
+ have them:
187
+ - A backup of the project (Recycle Bin / File History / a previous git push)
188
+ - An earlier version of the README in this conversation
189
+ - The `traces/` folder and the source files (`app.py`, `agent.py`,
190
+ `transit_agent.py`) for the exact phrasing you used
191
+
192
+ If you have the original anywhere, replace the body section (everything below
193
+ the closing `---`) with the original and keep the frontmatter I added.
__pycache__/agent.cpython-313.pyc ADDED
Binary file (22.6 kB). View file
 
__pycache__/airports.cpython-313.pyc ADDED
Binary file (2.49 kB). View file
 
__pycache__/app.cpython-313.pyc ADDED
Binary file (26.7 kB). View file
 
__pycache__/fr24.cpython-313.pyc ADDED
Binary file (7.41 kB). View file
 
__pycache__/geo.cpython-313.pyc ADDED
Binary file (4.36 kB). View file
 
__pycache__/globe.cpython-313.pyc ADDED
Binary file (7.16 kB). View file
 
__pycache__/liquid.cpython-313.pyc ADDED
Binary file (8.54 kB). View file
 
__pycache__/nemotron.cpython-313.pyc ADDED
Binary file (6.73 kB). View file
 
__pycache__/sidebar.cpython-313.pyc ADDED
Binary file (7.22 kB). View file
 
__pycache__/transit.cpython-313.pyc ADDED
Binary file (12.8 kB). View file
 
__pycache__/transit_agent.cpython-313.pyc ADDED
Binary file (18.2 kB). View file
 
__pycache__/transit_map.cpython-313.pyc ADDED
Binary file (3.26 kB). View file
 
__pycache__/weather.cpython-313.pyc ADDED
Binary file (3.05 kB). View file
 
app.py CHANGED
@@ -1,12 +1,12 @@
1
- """FLIGHTDECK — live flights on a transparent 3D globe, with an LLM flight agent.
2
-
3
- Data: FlightRadar24 API (https://fr24api.flightradar24.com/docs/getting-started)
4
- Globe: Globe.gl / Three.js (3D, transparent, neon glow)
5
- LLM: LiquidAI LFM2.5-350M via transformers (default safetensors model)
6
-
7
- Set FR24_API_TOKEN in your environment (see .env.example), then `python app.py`.
8
- """
9
- from __future__ import annotations
10
 
11
  import datetime as dt
12
  import os
@@ -399,7 +399,7 @@ _JS_FLIGHT = ("() => document.querySelector('.gradio-container')"
399
  _JS_TRANSIT = ("() => document.querySelector('.gradio-container')"
400
  ".classList.add('transit-mode')")
401
 
402
- with gr.Blocks(title="FLIGHTDECK", theme=theme, css=NEON_CSS, head=FORCE_DARK_JS) as demo:
403
  state = gr.State({})
404
  transit_state = gr.State({})
405
 
@@ -489,7 +489,9 @@ with gr.Blocks(title="FLIGHTDECK", theme=theme, css=NEON_CSS, head=FORCE_DARK_JS
489
  transit_assistant, [t_question, transit_state], transit_outputs)
490
 
491
  if __name__ == "__main__":
492
- demo.launch(
493
- server_name=os.environ.get("HOST", "127.0.0.1"),
494
- server_port=int(os.environ.get("PORT", "7860")),
495
- )
 
 
 
1
+ """FLIGHTDECK — live flights on a transparent 3D globe, with an LLM flight agent.
2
+
3
+ Data: FlightRadar24 API (https://fr24api.flightradar24.com/docs/getting-started)
4
+ Globe: Globe.gl / Three.js (3D, transparent, neon glow)
5
+ LLM: LiquidAI LFM2.5-350M via transformers (default safetensors model)
6
+
7
+ Set FR24_API_TOKEN in your environment (see .env.example), then `python app.py`.
8
+ """
9
+ from __future__ import annotations
10
 
11
  import datetime as dt
12
  import os
 
399
  _JS_TRANSIT = ("() => document.querySelector('.gradio-container')"
400
  ".classList.add('transit-mode')")
401
 
402
+ with gr.Blocks(title="FLIGHTDECK", theme=theme, css=NEON_CSS, js=FORCE_DARK_JS) as demo:
403
  state = gr.State({})
404
  transit_state = gr.State({})
405
 
 
489
  transit_assistant, [t_question, transit_state], transit_outputs)
490
 
491
  if __name__ == "__main__":
492
+ demo.launch(
493
+ # 0.0.0.0 so HuggingFace Spaces' proxy can reach the app (127.0.0.1
494
+ # only binds localhost inside the container and the Space won't load).
495
+ server_name=os.environ.get("HOST", "0.0.0.0"),
496
+ server_port=int(os.environ.get("PORT", "7860")),
497
+ )
liquid.py CHANGED
@@ -1,25 +1,40 @@
1
- """LiquidAI LFM2.5-350M (safetensors) wrapper via transformers.
2
-
3
- The model (set by LLM_REPO, default LiquidAI/LFM2.5-350M) is downloaded from
4
- HuggingFace on first use and cached. If anything is unavailable (no transformers,
5
- no model, no network) the app keeps working and just shows a deterministic fallback.
 
 
 
 
 
 
 
6
  """
7
  from __future__ import annotations
8
 
9
  import os
10
  import threading
 
 
 
 
 
 
 
 
 
 
11
 
12
- _PIPELINE = None
13
  _TOKENIZER = None
 
14
  _LOAD_LOCK = threading.Lock()
15
  _LOAD_ERROR = None
16
 
17
  SYSTEM_PROMPT = (
18
- "You are FLIGHTDECK, a terse air-traffic analyst. You are given live flight "
19
- "telemetry pulled from FlightRadar24. Answer the operator's question using "
20
- "only that data. Be concise, use callsigns, and when asked about routes or "
21
- "timing reason from the origin, destination, ground speed and ETA fields. "
22
- "Never invent flights that are not in the data."
23
  )
24
 
25
 
@@ -28,60 +43,74 @@ def llm_disabled() -> bool:
28
 
29
 
30
  def _model_id() -> str:
31
- # The GGUF-only repo and the safetensors repo have different names.
32
- # Default to the safetensors model. Allow override via LLM_REPO.
33
  return os.environ.get("LLM_REPO", "LiquidAI/LFM2.5-350M")
34
 
35
 
36
- def _apply_chat_template(messages, tokenizer):
37
- """Convert [{"role":..., "content":...}, ...] to a single prompt string
38
- using the tokenizer's chat template. Falls back to a manual concat if
39
- the tokenizer has no chat_template attribute."""
40
- if hasattr(tokenizer, "apply_chat_template") and getattr(tokenizer, "chat_template", None):
 
 
 
 
 
41
  return tokenizer.apply_chat_template(
42
- messages, tokenize=False, add_generation_prompt=True
43
- )
44
- # Manual fallback: simple "system / user" format.
45
- parts = []
46
- for m in messages:
47
- role = m.get("role", "user")
48
- parts.append(f"[{role.upper()}]\n{m.get('content', '')}\n")
49
  parts.append("[ASSISTANT]\n")
50
  return "\n".join(parts)
51
 
52
 
53
  def _load():
54
- """Load the model + tokenizer once. Returns (pipeline, tokenizer) or (None, None)."""
55
- global _PIPELINE, _TOKENIZER, _LOAD_ERROR
56
- if _PIPELINE is not None or _LOAD_ERROR is not None:
57
- return _PIPELINE, _TOKENIZER
58
  with _LOAD_LOCK:
59
- if _PIPELINE is not None or _LOAD_ERROR is not None:
60
- return _PIPELINE, _TOKENIZER
61
  try:
62
  import torch
63
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
64
 
65
- model_id = _model_id()
66
- tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
 
 
 
 
67
  model = AutoModelForCausalLM.from_pretrained(
68
- model_id,
69
- torch_dtype=torch.float32,
70
- device_map="auto",
71
- trust_remote_code=True,
72
- )
73
- _PIPELINE = pipeline(
74
- "text-generation",
75
- model=model,
76
- tokenizer=tokenizer,
77
- return_full_text=False,
78
- )
79
- _TOKENIZER = tokenizer
80
  except Exception as e: # noqa: BLE001
81
  _LOAD_ERROR = e
82
- _PIPELINE = None
83
- _TOKENIZER = None
84
- return _PIPELINE, _TOKENIZER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
 
87
  def status() -> str:
@@ -90,70 +119,52 @@ def status() -> str:
90
  return "LLM disabled (DISABLE_LLM=1)."
91
  if _LOAD_ERROR is not None:
92
  return f"{label} unavailable: {type(_LOAD_ERROR).__name__}: {_LOAD_ERROR}"
93
- if _PIPELINE is None:
94
  return f"{label} not loaded yet (loads on first query)."
95
- return f"{label} online (transformers, CPU/GPU auto)."
 
96
 
97
 
98
  def available() -> bool:
99
- """True if the model can actually run (not disabled and loadable)."""
100
  if llm_disabled():
101
  return False
102
- pipe, _ = _load()
103
- return pipe is not None
104
 
105
 
106
  def complete(messages, *, max_tokens=512, temperature=0.2, top_p=0.9):
107
- """Raw chat completion used by the agent loop. Returns (text, latency_ms).
108
-
109
- Raises RuntimeError if the model is unavailable so the caller can fall back.
110
- """
111
- pipe, tokenizer = _load()
112
- if pipe is None:
113
  raise RuntimeError(status())
114
- import time
115
- prompt = _apply_chat_template(messages, tokenizer)
116
  t0 = time.time()
117
- out = pipe(
118
- prompt,
119
- max_new_tokens=max_tokens,
120
- do_sample=temperature > 0,
121
- temperature=max(temperature, 1e-5),
122
- top_p=top_p,
123
- return_full_text=False,
124
- )
125
- latency = int((time.time() - t0) * 1000)
126
- # transformers pipeline returns a list of dicts with "generated_text"
127
- text = out[0]["generated_text"] if isinstance(out, list) else str(out)
128
- if isinstance(text, list):
129
- text = text[0].get("generated_text", "") if text else ""
130
- return str(text).strip(), latency
131
 
132
 
133
  def _fallback(question: str, context: str) -> str:
134
  return (
135
  "[AI offline — raw readout]\n"
136
  f"Q: {question}\n\n{context}\n\n"
137
- "(Install transformers + torch and allow the model to download to enable "
138
- "LLM natural-language briefings.)"
139
  )
140
 
141
 
142
  def briefing(question: str, context: str, max_tokens: int = 512) -> str:
143
- """Generate an answer about the current flights."""
144
- if llm_disabled():
145
- return _fallback(question, context)
146
- pipe, _ = _load()
147
- if pipe is None:
148
  return _fallback(question, context)
149
-
150
  messages = [
151
  {"role": "system", "content": SYSTEM_PROMPT},
152
  {"role": "user",
153
  "content": f"LIVE FLIGHT DATA:\n{context}\n\nQUESTION: {question}"},
154
  ]
155
  try:
156
- text, _latency = complete(messages, max_tokens=max_tokens, temperature=0.4)
157
  return text
158
  except Exception as e: # noqa: BLE001
159
  return _fallback(question, f"{context}\n\n(LLM error: {e})")
 
 
 
 
 
 
 
1
+ """LiquidAI LFM2.5-350M (safetensors) wrapper via transformers, ZeroGPU-ready.
2
+
3
+ On HuggingFace ZeroGPU Spaces:
4
+ * `spaces` is imported before torch, and the actual generation runs inside a
5
+ `@spaces.GPU` function (the GPU is attached only for that call).
6
+ * the model is placed on `cuda` at module level (ZeroGPU's CUDA emulation makes
7
+ this work outside the decorated function), as the docs recommend.
8
+
9
+ Off ZeroGPU (local CPU/GPU, or `spaces` not installed) everything still works:
10
+ * `@spaces.GPU` becomes a no-op, and the device falls back to a real CUDA GPU
11
+ if present, otherwise CPU.
12
+ If anything is unavailable the app keeps running with a deterministic fallback.
13
  """
14
  from __future__ import annotations
15
 
16
  import os
17
  import threading
18
+ import time
19
+
20
+ # IMPORTANT: import `spaces` BEFORE torch so it can patch CUDA for ZeroGPU.
21
+ try:
22
+ import spaces # noqa: F401
23
+ _HAS_SPACES = True
24
+ except Exception:
25
+ _HAS_SPACES = False
26
+
27
+ _ON_ZEROGPU = bool(os.environ.get("SPACES_ZERO_GPU"))
28
 
29
+ _MODEL = None
30
  _TOKENIZER = None
31
+ _DEVICE = "cpu"
32
  _LOAD_LOCK = threading.Lock()
33
  _LOAD_ERROR = None
34
 
35
  SYSTEM_PROMPT = (
36
+ "You are FLIGHTDECK, a terse air-traffic analyst. Answer only from the live "
37
+ "flight data you are given. Be concise and use callsigns. Never invent flights."
 
 
 
38
  )
39
 
40
 
 
43
 
44
 
45
  def _model_id() -> str:
46
+ # Safetensors repo (transformers), overridable via LLM_REPO.
 
47
  return os.environ.get("LLM_REPO", "LiquidAI/LFM2.5-350M")
48
 
49
 
50
+ def _gpu(fn):
51
+ """Wrap a function with @spaces.GPU on ZeroGPU; no-op everywhere else."""
52
+ if _HAS_SPACES:
53
+ duration = int(os.environ.get("ZEROGPU_DURATION", "60"))
54
+ return spaces.GPU(duration=duration)(fn)
55
+ return fn
56
+
57
+
58
+ def _apply_chat_template(messages, tokenizer) -> str:
59
+ if getattr(tokenizer, "chat_template", None):
60
  return tokenizer.apply_chat_template(
61
+ messages, tokenize=False, add_generation_prompt=True)
62
+ parts = [f"[{m.get('role', 'user').upper()}]\n{m.get('content', '')}" for m in messages]
 
 
 
 
 
63
  parts.append("[ASSISTANT]\n")
64
  return "\n".join(parts)
65
 
66
 
67
  def _load():
68
+ """Load model + tokenizer once (in the main process; ZeroGPU-safe)."""
69
+ global _MODEL, _TOKENIZER, _DEVICE, _LOAD_ERROR
70
+ if _MODEL is not None or _LOAD_ERROR is not None:
71
+ return _MODEL
72
  with _LOAD_LOCK:
73
+ if _MODEL is not None or _LOAD_ERROR is not None:
74
+ return _MODEL
75
  try:
76
  import torch
77
+ from transformers import AutoModelForCausalLM, AutoTokenizer
78
 
79
+ mid = _model_id()
80
+ want_cuda = _ON_ZEROGPU or torch.cuda.is_available()
81
+ _DEVICE = "cuda" if want_cuda else "cpu"
82
+ dtype = torch.float16 if want_cuda else torch.float32
83
+
84
+ _TOKENIZER = AutoTokenizer.from_pretrained(mid, trust_remote_code=True)
85
  model = AutoModelForCausalLM.from_pretrained(
86
+ mid, dtype=dtype, trust_remote_code=True)
87
+ # Module-level cuda placement (works via ZeroGPU CUDA emulation).
88
+ model.to(_DEVICE)
89
+ model.eval()
90
+ _MODEL = model
 
 
 
 
 
 
 
91
  except Exception as e: # noqa: BLE001
92
  _LOAD_ERROR = e
93
+ _MODEL = None
94
+ return _MODEL
95
+
96
+
97
+ @_gpu
98
+ def _generate(prompt: str, max_new_tokens: int, temperature: float, top_p: float) -> str:
99
+ """The only GPU-touching function — runs on the ZeroGPU device when attached."""
100
+ import torch
101
+ inputs = _TOKENIZER(prompt, return_tensors="pt").to(_DEVICE)
102
+ gen_kwargs = dict(
103
+ max_new_tokens=max_new_tokens,
104
+ do_sample=temperature > 0,
105
+ top_p=top_p,
106
+ pad_token_id=_TOKENIZER.eos_token_id,
107
+ )
108
+ if temperature > 0:
109
+ gen_kwargs["temperature"] = temperature
110
+ with torch.no_grad():
111
+ out = _MODEL.generate(**inputs, **gen_kwargs)
112
+ new_tokens = out[0][inputs["input_ids"].shape[1]:]
113
+ return _TOKENIZER.decode(new_tokens, skip_special_tokens=True)
114
 
115
 
116
  def status() -> str:
 
119
  return "LLM disabled (DISABLE_LLM=1)."
120
  if _LOAD_ERROR is not None:
121
  return f"{label} unavailable: {type(_LOAD_ERROR).__name__}: {_LOAD_ERROR}"
122
+ if _MODEL is None:
123
  return f"{label} not loaded yet (loads on first query)."
124
+ mode = "ZeroGPU" if (_HAS_SPACES and _ON_ZEROGPU) else _DEVICE.upper()
125
+ return f"{label} online ({mode})."
126
 
127
 
128
  def available() -> bool:
 
129
  if llm_disabled():
130
  return False
131
+ return _load() is not None
 
132
 
133
 
134
  def complete(messages, *, max_tokens=512, temperature=0.2, top_p=0.9):
135
+ """Chat completion used by the agents. Returns (text, latency_ms)."""
136
+ if _load() is None:
 
 
 
 
137
  raise RuntimeError(status())
138
+ prompt = _apply_chat_template(messages, _TOKENIZER)
 
139
  t0 = time.time()
140
+ text = _generate(prompt, int(max_tokens), float(temperature), float(top_p))
141
+ return str(text).strip(), int((time.time() - t0) * 1000)
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
 
144
  def _fallback(question: str, context: str) -> str:
145
  return (
146
  "[AI offline — raw readout]\n"
147
  f"Q: {question}\n\n{context}\n\n"
148
+ "(Enable the model — transformers + torch for natural-language briefings.)"
 
149
  )
150
 
151
 
152
  def briefing(question: str, context: str, max_tokens: int = 512) -> str:
153
+ if llm_disabled() or _load() is None:
 
 
 
 
154
  return _fallback(question, context)
 
155
  messages = [
156
  {"role": "system", "content": SYSTEM_PROMPT},
157
  {"role": "user",
158
  "content": f"LIVE FLIGHT DATA:\n{context}\n\nQUESTION: {question}"},
159
  ]
160
  try:
161
+ text, _ = complete(messages, max_tokens=max_tokens, temperature=0.4)
162
  return text
163
  except Exception as e: # noqa: BLE001
164
  return _fallback(question, f"{context}\n\n(LLM error: {e})")
165
+
166
+
167
+ # ZeroGPU recommends placing the model at startup (not lazily). On ZeroGPU we
168
+ # eager-load; locally we stay lazy so imports/tests remain fast.
169
+ if _ON_ZEROGPU and not llm_disabled():
170
+ _load()
requirements.txt CHANGED
@@ -1,12 +1,17 @@
1
- gradio==5.49.1
 
 
 
 
 
 
 
2
  requests>=2.31.0
3
  python-dotenv>=1.0.0
4
  numpy>=1.26.0
5
  huggingface_hub>=0.24.0
6
- # LLM agent runtime: LiquidAI LFM2.5-350M via transformers.
7
- # The model (default LiquidAI/LFM2.5-350M) is downloaded from HuggingFace on
8
- # first use and cached.
9
- # ZeroGPU requires torch 2.8.0-2.11.0. Pin to the latest supported (2.11.0).
10
- transformers>=4.44.0
11
- torch==2.11.0
12
- accelerate>=0.33.0
 
1
+ # --- HuggingFace ZeroGPU Space ---
2
+ # gradio is NOT pinned here — its version is controlled by `sdk_version` in the
3
+ # README.md front-matter (the HF Gradio SDK installs it).
4
+ #
5
+ # ZeroGPU requires CUDA PyTorch >= 2.8.0 and the `spaces` package. Select the
6
+ # "ZeroGPU" hardware in the Space settings (requires a PRO/Team/Enterprise plan).
7
+ spaces
8
+
9
  requests>=2.31.0
10
  python-dotenv>=1.0.0
11
  numpy>=1.26.0
12
  huggingface_hub>=0.24.0
13
+
14
+ # LLM agent: LiquidAI LFM2.5-350M (safetensors) via transformers, run on ZeroGPU.
15
+ transformers==5.8.1
16
+ torch==2.8.0
17
+ accelerate==1.13.0
 
 
traces/agent_log.jsonl CHANGED
@@ -14,3 +14,4 @@
14
  {"trace_id": "20260607_082836_88b4ce", "ts": "2026-06-07T15:28:37.446926+00:00", "query": "fastest way from Oakland to San Jose", "mode": "transit-llm", "tool_calls": ["plan_trip"], "flights_returned": 10}
15
  {"trace_id": "20260607_083207_f4ac39", "ts": "2026-06-07T15:32:10.607470+00:00", "query": "flights from London to Dubai", "mode": "llm", "tool_calls": ["search_by_route"], "flights_returned": 1}
16
  {"trace_id": "20260607_083344_fe2a6a", "ts": "2026-06-07T15:33:44.484771+00:00", "query": "My moms house", "mode": "llm+scope-refused", "tool_calls": [], "flights_returned": 0}
 
 
14
  {"trace_id": "20260607_082836_88b4ce", "ts": "2026-06-07T15:28:37.446926+00:00", "query": "fastest way from Oakland to San Jose", "mode": "transit-llm", "tool_calls": ["plan_trip"], "flights_returned": 10}
15
  {"trace_id": "20260607_083207_f4ac39", "ts": "2026-06-07T15:32:10.607470+00:00", "query": "flights from London to Dubai", "mode": "llm", "tool_calls": ["search_by_route"], "flights_returned": 1}
16
  {"trace_id": "20260607_083344_fe2a6a", "ts": "2026-06-07T15:33:44.484771+00:00", "query": "My moms house", "mode": "llm+scope-refused", "tool_calls": [], "flights_returned": 0}
17
+ {"trace_id": "20260608_004116_ebf3f4", "ts": "2026-06-08T07:41:30.342926+00:00", "query": "flights from London to Dubai", "mode": "llm", "tool_calls": ["search_by_route"], "flights_returned": 0}
traces/trace_20260608_004116_ebf3f4.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "trace_id": "20260608_004116_ebf3f4",
3
+ "started_at": "2026-06-08T07:41:16.987012+00:00",
4
+ "model": "LiquidAI/LFM2.5-350M",
5
+ "query": "flights from London to Dubai",
6
+ "agent_mode": "llm",
7
+ "steps": [
8
+ {
9
+ "step": 1,
10
+ "phase": "plan",
11
+ "model_raw": "{\"tool\": \"search_by_route\", \"origin\": \"LHR\", \"destination\": \"DXB\"}\n\n{\"tool\": \"none\", \"answer\": \"A journey across the skies\"}",
12
+ "parsed_action": {
13
+ "tool": "search_by_route",
14
+ "origin": "LHR",
15
+ "destination": "DXB"
16
+ },
17
+ "latency_ms": 1396
18
+ },
19
+ {
20
+ "step": 2,
21
+ "phase": "act",
22
+ "tool": "search_by_route",
23
+ "args": {
24
+ "origin": "LHR",
25
+ "destination": "DXB"
26
+ },
27
+ "meta": {
28
+ "error": "FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the environment (see .env.example)."
29
+ },
30
+ "result_count": 0,
31
+ "latency_ms": 0,
32
+ "error": "FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the environment (see .env.example)."
33
+ }
34
+ ],
35
+ "tool_calls": [
36
+ {
37
+ "tool": "search_by_route",
38
+ "args": {
39
+ "origin": "LHR",
40
+ "destination": "DXB"
41
+ },
42
+ "meta": {
43
+ "error": "FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the environment (see .env.example)."
44
+ },
45
+ "result_count": 0,
46
+ "latency_ms": 0,
47
+ "error": "FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the environment (see .env.example)."
48
+ }
49
+ ],
50
+ "flights_returned": 0,
51
+ "answer": "Search failed: FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the environment (see .env.example).",
52
+ "ended_at": "2026-06-08T07:41:30.342926+00:00"
53
+ }