World Cup Agent commited on
Commit
88b45ef
·
1 Parent(s): 238f6db

fix: remove jinja2, render html directly

Browse files
Files changed (1) hide show
  1. app.py +230 -85
app.py CHANGED
@@ -1,136 +1,281 @@
1
  """World Cup Prediction Agent — Web UI.
2
 
3
- FastAPI app deployed on Render. Reads flat files from disk.
4
- Start with: uvicorn app:app --host 0.0.0.0 --port $PORT
5
  """
6
 
7
  import json
8
- import re
9
  from datetime import datetime, timezone
10
  from pathlib import Path
11
 
12
  from fastapi import FastAPI, HTTPException
13
  from fastapi.responses import HTMLResponse
14
- from fastapi.templating import Jinja2Templates
15
- from starlette.requests import Request
16
 
17
  BASE = Path(__file__).parent
18
- templates = Jinja2Templates(directory=str(BASE / "templates"))
19
-
20
  app = FastAPI(title="World Cup Prediction Agent")
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- def _load_results() -> list[dict]:
24
- p = BASE / "results.json"
25
  if not p.exists():
26
- return []
27
  try:
28
  return json.loads(p.read_text())
29
  except Exception:
30
- return []
31
-
32
 
33
- def _load_strategy() -> str:
34
- p = BASE / "strategy.md"
35
  return p.read_text() if p.exists() else ""
36
 
 
 
 
 
 
37
 
38
- def _load_schedule() -> list[dict]:
39
- p = BASE / "schedule.json"
40
- if not p.exists():
41
- return []
42
- try:
43
- return json.loads(p.read_text())
44
- except Exception:
45
- return []
46
-
47
-
48
- def _record_stats(results: list[dict]) -> dict:
49
- completed = [r for r in results if r.get("correct") is not None]
50
- wins = sum(1 for r in completed if r.get("correct"))
51
- total = len(completed)
52
- return {
53
- "total": total,
54
- "wins": wins,
55
- "losses": total - wins,
56
- "pct": f"{wins/total*100:.0f}%" if total else "—",
57
- }
58
-
59
-
60
- def _upcoming(schedule: list[dict], results: list[dict]) -> list[dict]:
61
  now = datetime.now(timezone.utc)
62
  predicted = {(r["home"], r["away"]) for r in results}
63
- upcoming = []
64
  for f in schedule:
65
  k = f.get("kickoff_utc", "")
66
- if not k:
67
- continue
68
  try:
69
  dt = datetime.fromisoformat(k.replace("Z", "+00:00"))
70
- except ValueError:
71
  continue
72
  if dt > now and (f["home"], f["away"]) not in predicted:
73
- upcoming.append({**f, "kickoff_dt": dt})
74
- return sorted(upcoming, key=lambda x: x["kickoff_dt"])[:10]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
 
77
  @app.get("/", response_class=HTMLResponse)
78
- async def dashboard(request: Request):
79
- results = list(reversed(_load_results()))
80
- strategy = _load_strategy()
81
- schedule = _load_schedule()
82
- stats = _record_stats(results)
83
- upcoming = _upcoming(schedule, _load_results())
84
- return templates.TemplateResponse(
85
- "index.html",
86
- {
87
- "request": request,
88
- "results": results,
89
- "strategy": strategy,
90
- "stats": stats,
91
- "upcoming": upcoming,
92
- },
93
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
 
96
  @app.get("/match/{match_key}", response_class=HTMLResponse)
97
- async def match_detail(request: Request, match_key: str):
98
  home, _, away = match_key.partition("_")
99
  home, away = home.upper(), away.upper()
100
- results = _load_results()
101
- entry = next(
102
- (r for r in reversed(results) if r.get("home") == home and r.get("away") == away),
103
- None,
104
- )
105
  if not entry:
106
  raise HTTPException(status_code=404, detail=f"No prediction found for {home} vs {away}")
107
- return templates.TemplateResponse(
108
- "index.html",
109
- {"request": request, "detail": entry, "results": [], "strategy": "", "stats": {}, "upcoming": []},
110
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
 
113
  @app.get("/strategy", response_class=HTMLResponse)
114
- async def strategy_page(request: Request):
115
- strategy = _load_strategy()
116
- results = _load_results()
117
- history = [
118
- {"match": r.get("match"), "kickoff": r.get("kickoff"), "snapshot": r.get("strategy_snapshot", "")}
119
- for r in results
120
- if r.get("strategy_snapshot") is not None
121
- ]
122
- return templates.TemplateResponse(
123
- "index.html",
124
- {"request": request, "strategy_page": True, "strategy": strategy, "strategy_history": history,
125
- "results": [], "stats": {}, "upcoming": []},
126
- )
 
 
 
 
127
 
128
 
129
  @app.get("/api/results")
130
  async def api_results():
131
- return _load_results()
132
-
133
 
134
  @app.get("/api/schedule")
135
  async def api_schedule():
136
- return _load_schedule()
 
1
  """World Cup Prediction Agent — Web UI.
2
 
3
+ FastAPI app deployed on HF Spaces. Renders HTML directly (no Jinja2).
 
4
  """
5
 
6
  import json
 
7
  from datetime import datetime, timezone
8
  from pathlib import Path
9
 
10
  from fastapi import FastAPI, HTTPException
11
  from fastapi.responses import HTMLResponse
 
 
12
 
13
  BASE = Path(__file__).parent
 
 
14
  app = FastAPI(title="World Cup Prediction Agent")
15
 
16
+ CSS = """
17
+ <style>
18
+ * { box-sizing: border-box; margin: 0; padding: 0; }
19
+ body { background: #0d0d0d; color: #e0e0e0; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 14px; line-height: 1.6; }
20
+ a { color: #4dabf7; text-decoration: none; }
21
+ a:hover { text-decoration: underline; }
22
+ .container { max-width: 900px; margin: 0 auto; padding: 24px 16px; }
23
+ header { border-bottom: 1px solid #333; padding-bottom: 16px; margin-bottom: 24px; }
24
+ header h1 { font-size: 20px; color: #fff; }
25
+ .meta { display: flex; gap: 24px; margin-top: 8px; color: #888; font-size: 12px; }
26
+ .meta span { color: #e0e0e0; }
27
+ h2 { font-size: 13px; color: #aaa; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; border-bottom: 1px solid #222; padding-bottom: 6px; }
28
+ .section { margin-bottom: 32px; }
29
+ .match-card { background: #111; border: 1px solid #2a2a2a; border-radius: 6px; margin-bottom: 16px; overflow: hidden; }
30
+ .match-header { padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; background: #161616; border-bottom: 1px solid #222; }
31
+ .match-title { font-size: 15px; font-weight: bold; color: #fff; }
32
+ .match-time { font-size: 11px; color: #555; }
33
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: bold; }
34
+ .badge.correct { background: #1a3a1a; color: #4caf50; border: 1px solid #4caf50; }
35
+ .badge.incorrect { background: #3a1a1a; color: #f44336; border: 1px solid #f44336; }
36
+ .badge.pending { background: #1a1a3a; color: #888; border: 1px solid #444; }
37
+ .match-body { padding: 12px 16px; }
38
+ .pred-row { display: flex; gap: 24px; margin-bottom: 10px; flex-wrap: wrap; }
39
+ .pred-item .label { font-size: 10px; color: #555; text-transform: uppercase; }
40
+ .pred-item .value { font-size: 14px; color: #e0e0e0; }
41
+ .pred-item .value.pick { font-size: 16px; font-weight: bold; color: #fff; }
42
+ .reasoning { font-size: 12px; color: #888; font-style: italic; margin-bottom: 10px; }
43
+ .x402-label { font-size: 10px; color: #555; text-transform: uppercase; margin: 8px 0 4px; }
44
+ table.x402 { width: 100%; border-collapse: collapse; font-size: 12px; }
45
+ table.x402 th { text-align: left; color: #555; font-weight: normal; padding: 3px 6px; border-bottom: 1px solid #222; }
46
+ table.x402 td { padding: 4px 6px; color: #aaa; border-bottom: 1px solid #1a1a1a; vertical-align: top; }
47
+ table.x402 td.url { color: #4dabf7; font-size: 11px; word-break: break-all; max-width: 300px; }
48
+ table.x402 td.cost { color: #f9a825; white-space: nowrap; }
49
+ .no-data { font-size: 12px; color: #444; font-style: italic; }
50
+ .result-bar { margin-top: 10px; padding: 8px 12px; border-radius: 4px; font-size: 13px; }
51
+ .result-bar.correct { background: #0d1f0d; border-left: 3px solid #4caf50; }
52
+ .result-bar.incorrect { background: #1f0d0d; border-left: 3px solid #f44336; }
53
+ .result-score { font-weight: bold; color: #fff; }
54
+ .eval-text { font-size: 12px; color: #666; margin-top: 6px; }
55
+ .detail-link { font-size: 11px; color: #555; margin-top: 8px; display: block; }
56
+ .upcoming-item { background: #161616; border: 1px solid #2a2a2a; border-radius: 4px; padding: 8px 12px; display: flex; justify-content: space-between; margin-bottom: 6px; }
57
+ .strategy-box { background: #111; border: 1px solid #2a2a2a; border-radius: 6px; padding: 16px; }
58
+ pre { white-space: pre-wrap; word-wrap: break-word; color: #bbb; font-size: 13px; }
59
+ .full-text { font-size: 12px; color: #aaa; white-space: pre-wrap; background: #0a0a0a; border: 1px solid #1e1e1e; border-radius: 4px; padding: 12px; max-height: 400px; overflow-y: auto; }
60
+ .nav { margin-bottom: 20px; font-size: 13px; }
61
+ </style>
62
+ """
63
 
64
+ def _load(filename, default):
65
+ p = BASE / filename
66
  if not p.exists():
67
+ return default
68
  try:
69
  return json.loads(p.read_text())
70
  except Exception:
71
+ return default
 
72
 
73
+ def _load_text(filename):
74
+ p = BASE / filename
75
  return p.read_text() if p.exists() else ""
76
 
77
+ def _stats(results):
78
+ done = [r for r in results if r.get("correct") is not None]
79
+ w = sum(1 for r in done if r.get("correct"))
80
+ t = len(done)
81
+ return w, t - w, f"{w/t*100:.0f}%" if t else "—"
82
 
83
+ def _upcoming(schedule, results):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  now = datetime.now(timezone.utc)
85
  predicted = {(r["home"], r["away"]) for r in results}
86
+ out = []
87
  for f in schedule:
88
  k = f.get("kickoff_utc", "")
 
 
89
  try:
90
  dt = datetime.fromisoformat(k.replace("Z", "+00:00"))
91
+ except Exception:
92
  continue
93
  if dt > now and (f["home"], f["away"]) not in predicted:
94
+ out.append((dt, f))
95
+ return [f for _, f in sorted(out, key=lambda x: x[0])[:8]]
96
+
97
+ def _badge(r):
98
+ if r.get("correct") is True:
99
+ return '<span class="badge correct">✓ CORRECT</span>'
100
+ if r.get("correct") is False:
101
+ return '<span class="badge incorrect">✗ INCORRECT</span>'
102
+ return '<span class="badge pending">PENDING</span>'
103
+
104
+ def _esc(s):
105
+ return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
106
+
107
+ def _page(body, title="⚽ World Cup Prediction Agent"):
108
+ results = _load("results.json", [])
109
+ w, l, pct = _stats(results)
110
+ nav = '<div class="nav"><a href="/">Dashboard</a> · <a href="/strategy">Strategy</a></div>'
111
+ header = f"""
112
+ <header>
113
+ <h1>⚽ World Cup Prediction Agent</h1>
114
+ <div class="meta">
115
+ <div>Record: <span>{w}W–{l}L ({pct})</span></div>
116
+ <div>Matches predicted: <span>{len(results)}</span></div>
117
+ </div>
118
+ </header>"""
119
+ return HTMLResponse(f"<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>{title}</title>{CSS}</head><body><div class='container'>{header}{nav}{body}</div></body></html>")
120
 
121
 
122
  @app.get("/", response_class=HTMLResponse)
123
+ async def dashboard():
124
+ results = list(reversed(_load("results.json", [])))
125
+ schedule = _load("schedule.json", [])
126
+ strategy = _load_text("strategy.md")
127
+ upcoming = _upcoming(schedule, _load("results.json", []))
128
+
129
+ html = ""
130
+
131
+ # Upcoming
132
+ if upcoming:
133
+ items = ""
134
+ for f in upcoming:
135
+ items += f"""<div class="upcoming-item">
136
+ <div><b>{_esc(f['home'])} vs {_esc(f['away'])}</b> <span style="color:#555;font-size:11px;margin-left:8px;">{_esc(f.get('stage',''))}</span></div>
137
+ <div style="color:#666;font-size:12px;">{f['kickoff_utc'][:16].replace('T',' ')} UTC</div>
138
+ </div>"""
139
+ html += f'<div class="section"><h2>Upcoming</h2>{items}</div>'
140
+
141
+ # Past matches
142
+ if results:
143
+ cards = ""
144
+ for r in results:
145
+ pred = r.get("prediction", {})
146
+ res = r.get("result")
147
+ used = r.get("services_used", [])
148
+ planned = r.get("services_planned", [])
149
+
150
+ x402_html = ""
151
+ if used:
152
+ rows = "".join(f'<tr><td class="url">{_esc(s["source"])}</td><td class="cost">${s["cost"]:.4f}</td><td>{_esc(s.get("reason",""))}</td></tr>' for s in used)
153
+ x402_html = f'<div class="x402-label">x402 data purchased</div><table class="x402"><tr><th>Endpoint</th><th>Cost</th><th>Reason</th></tr>{rows}</table>'
154
+ elif planned:
155
+ x402_html = f'<div class="no-data">{len(planned)} endpoint(s) considered, none purchased.</div>'
156
+ else:
157
+ x402_html = '<div class="no-data">No x402 data for this match.</div>'
158
+
159
+ result_html = ""
160
+ if res:
161
+ cls = "correct" if r.get("correct") else "incorrect"
162
+ eval_snippet = _esc((r.get("evaluation") or "")[:180])
163
+ result_html = f'''<div class="result-bar {cls}">
164
+ Result: <span class="result-score">{_esc(r["home"])} {res["home_score"]}–{res["away_score"]} {_esc(r["away"])}</span>
165
+ {f'<div class="eval-text">{eval_snippet}{"…" if len(r.get("evaluation",""))>180 else ""}</div>' if eval_snippet else ""}
166
+ </div>'''
167
+
168
+ cards += f"""<div class="match-card">
169
+ <div class="match-header">
170
+ <div>
171
+ <div class="match-title">{_esc(r["match"])}</div>
172
+ <div class="match-time">{r.get("kickoff","")[:16].replace("T"," ")} UTC</div>
173
+ </div>
174
+ {_badge(r)}
175
+ </div>
176
+ <div class="match-body">
177
+ <div class="pred-row">
178
+ <div class="pred-item"><div class="label">Pick</div><div class="value pick">{_esc(pred.get("pick","?")).upper()}</div></div>
179
+ <div class="pred-item"><div class="label">Confidence</div><div class="value">{pred.get("confidence","?")}/10</div></div>
180
+ <div class="pred-item"><div class="label">Bet</div><div class="value">${pred.get("bet","?")}</div></div>
181
+ <div class="pred-item"><div class="label">Research cost</div><div class="value" style="color:#f9a825;">${r.get("research_cost",0):.4f}</div></div>
182
+ </div>
183
+ <div class="reasoning">"{_esc(pred.get("reasoning",""))}"</div>
184
+ {x402_html}
185
+ {result_html}
186
+ <a href="/match/{_esc(r['home'])}_{_esc(r['away'])}" class="detail-link">Full reasoning & reflection →</a>
187
+ </div>
188
+ </div>"""
189
+ html += f'<div class="section"><h2>Predictions</h2>{cards}</div>'
190
+ else:
191
+ html += '<div style="color:#444;font-style:italic;padding:32px 0;">No predictions yet.</div>'
192
+
193
+ # Strategy snippet
194
+ if strategy:
195
+ snippet = _esc(strategy[:500])
196
+ more = ' <a href="/strategy">… read more</a>' if len(strategy) > 500 else ""
197
+ html += f'<div class="section"><h2>Current Strategy <a href="/strategy" style="font-size:11px;color:#555;font-weight:normal;margin-left:8px;">full history →</a></h2><div class="strategy-box"><pre>{snippet}{more}</pre></div></div>'
198
+
199
+ return _page(html)
200
 
201
 
202
  @app.get("/match/{match_key}", response_class=HTMLResponse)
203
+ async def match_detail(match_key: str):
204
  home, _, away = match_key.partition("_")
205
  home, away = home.upper(), away.upper()
206
+ results = _load("results.json", [])
207
+ entry = next((r for r in reversed(results) if r.get("home") == home and r.get("away") == away), None)
 
 
 
208
  if not entry:
209
  raise HTTPException(status_code=404, detail=f"No prediction found for {home} vs {away}")
210
+
211
+ pred = entry.get("prediction", {})
212
+ res = entry.get("result")
213
+ used = entry.get("services_used", [])
214
+ planned = entry.get("services_planned", [])
215
+
216
+ result_html = ""
217
+ if res:
218
+ cls = "correct" if entry.get("correct") else "incorrect"
219
+ result_html = f'<div class="result-bar {cls}" style="margin-bottom:16px;"><span class="result-score">{_esc(home)} {res["home_score"]}–{res["away_score"]} {_esc(away)}</span> &nbsp;{_badge(entry)}</div>'
220
+
221
+ used_urls = {s["source"] for s in used}
222
+ plan_rows = ""
223
+ for s in planned:
224
+ status = '✓ bought' if s["url"] in used_urls else '<span style="color:#555">skipped</span>'
225
+ plan_rows += f'<tr><td class="url">{_esc(s["url"])}</td><td class="cost">${s.get("cost",0):.4f}</td><td>{status}</td><td>{_esc(s.get("reason",""))}</td></tr>'
226
+
227
+ x402_section = ""
228
+ if planned:
229
+ x402_section = f'<div class="section"><h2>x402 Research — ${entry.get("research_cost",0):.4f} spent</h2><table class="x402"><tr><th>Endpoint</th><th>Cost</th><th>Status</th><th>Reason</th></tr>{plan_rows}</table></div>'
230
+
231
+ eval_html = f'<div class="section"><h2>Reflection & Evaluation</h2><div class="full-text">{_esc(entry.get("evaluation","(not yet reflected)"))}</div></div>' if entry.get("evaluation") else ""
232
+ snapshot_html = f'<div class="section"><h2>Strategy at Prediction Time</h2><div class="full-text">{_esc(entry.get("strategy_snapshot","(empty)"))}</div></div>' if entry.get("strategy_snapshot") is not None else ""
233
+
234
+ body = f"""
235
+ <div style="margin-bottom:12px;"><a href="/" style="color:#555;font-size:12px;">← Back</a></div>
236
+ <h2 style="font-size:18px;color:#fff;border:none;margin-bottom:8px;">{_esc(entry["match"])}</h2>
237
+ <div style="color:#555;font-size:12px;margin-bottom:16px;">{entry.get("kickoff","")[:16].replace("T"," ")} UTC</div>
238
+ {result_html}
239
+ <div class="section"><h2>Prediction</h2>
240
+ <div class="pred-row">
241
+ <div class="pred-item"><div class="label">Pick</div><div class="value pick">{_esc(pred.get("pick","?")).upper()}</div></div>
242
+ <div class="pred-item"><div class="label">Confidence</div><div class="value">{pred.get("confidence","?")}/10</div></div>
243
+ <div class="pred-item"><div class="label">Bet</div><div class="value">${pred.get("bet","?")}</div></div>
244
+ </div>
245
+ <div class="reasoning">"{_esc(pred.get("reasoning",""))}"</div>
246
+ </div>
247
+ {x402_section}
248
+ <div class="section"><h2>Full Gemini Reasoning</h2><div class="full-text">{_esc(entry.get("full_reasoning","(none)"))}</div></div>
249
+ {eval_html}
250
+ {snapshot_html}"""
251
+
252
+ return _page(body, f"{home} vs {away} — World Cup Predictions")
253
 
254
 
255
  @app.get("/strategy", response_class=HTMLResponse)
256
+ async def strategy_page():
257
+ strategy = _load_text("strategy.md")
258
+ results = _load("results.json", [])
259
+ history = [(r.get("match"), r.get("kickoff","")[:10], r.get("strategy_snapshot","")) for r in results if r.get("strategy_snapshot") is not None]
260
+
261
+ current = f'<div class="strategy-box"><pre>{_esc(strategy) if strategy else "(no strategy yet)"}</pre></div>'
262
+
263
+ hist_html = ""
264
+ if history:
265
+ items = ""
266
+ for match, date, snap in reversed(history):
267
+ snippet = _esc((snap or "")[:300])
268
+ items += f'<div style="border-bottom:1px solid #1a1a1a;padding:10px 0;"><div style="font-size:12px;color:#888;margin-bottom:4px;">After {_esc(match)} ({date})</div><div style="font-size:11px;color:#555;">{snippet}{"…" if len(snap or "")>300 else ""}</div></div>'
269
+ hist_html = f'<div class="section"><h2>Strategy Evolution</h2>{items}</div>'
270
+
271
+ body = f'<div class="section"><h2>Current Strategy</h2>{current}</div>{hist_html}'
272
+ return _page(body, "Strategy — World Cup Predictions")
273
 
274
 
275
  @app.get("/api/results")
276
  async def api_results():
277
+ return _load("results.json", [])
 
278
 
279
  @app.get("/api/schedule")
280
  async def api_schedule():
281
+ return _load("schedule.json", [])