broadfield-dev commited on
Commit
79eb227
·
verified ·
1 Parent(s): 4e925ce

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +591 -0
app.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Overthinker — Gradio.Server Backend with SQLite Session Isolation + HF Trace Upload
4
+
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import uuid
10
+ import sqlite3
11
+ from pathlib import Path
12
+ from typing import Optional, Dict, List, Any
13
+
14
+ from gradio import Server
15
+ from fastapi import HTTPException
16
+ from starlette.responses import HTMLResponse, PlainTextResponse
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Application Setup
20
+ # ---------------------------------------------------------------------------
21
+ app = Server()
22
+ PORT = 7860
23
+ DATA_DIR = Path("data")
24
+ DATA_DIR.mkdir(exist_ok=True)
25
+
26
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
27
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
28
+ DEFAULT_MODEL = "nvidia/nemotron-3-nano-30b-a3b"
29
+
30
+ HF_TOKEN = os.getenv('HF_TOKEN', '')
31
+ HF_DATASET_REPO = os.getenv('HF_DATASET_REPO', 'build-small-hackathon/Overthinker-traces')
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Database Helpers
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def get_db_path(session_id: str) -> Path:
38
+ return DATA_DIR / f"session_{session_id}.db"
39
+
40
+ def init_session(session_id: str):
41
+ db_path = get_db_path(session_id)
42
+ if db_path.exists():
43
+ return
44
+ conn = sqlite3.connect(str(db_path))
45
+ conn.execute("""
46
+ CREATE TABLE nodes (
47
+ id TEXT PRIMARY KEY,
48
+ parent_id TEXT,
49
+ type TEXT NOT NULL,
50
+ label TEXT NOT NULL,
51
+ description TEXT DEFAULT '',
52
+ emoji TEXT DEFAULT '\U0001f539',
53
+ tips TEXT DEFAULT '[]',
54
+ order_index INTEGER DEFAULT 0,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
+ )
57
+ """)
58
+ root_id = str(uuid.uuid4())
59
+ conn.execute(
60
+ "INSERT INTO nodes (id, parent_id, type, label, description, emoji) VALUES (?, ?, ?, ?, ?, ?)",
61
+ (root_id, None, "root", "What decision do you want to explore?", "", "\U0001f333")
62
+ )
63
+ conn.commit()
64
+ conn.close()
65
+
66
+ def get_node_db(session_id: str, node_id: str) -> Optional[Dict]:
67
+ db_path = get_db_path(session_id)
68
+ if not db_path.exists():
69
+ return None
70
+ conn = sqlite3.connect(str(db_path))
71
+ conn.row_factory = sqlite3.Row
72
+ row = conn.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone()
73
+ conn.close()
74
+ if row is None:
75
+ return None
76
+ result = dict(row)
77
+ try:
78
+ result['tips'] = json.loads(result.get('tips', '[]'))
79
+ except:
80
+ result['tips'] = []
81
+ return result
82
+
83
+ def get_children_db(session_id: str, parent_id: str) -> List[Dict]:
84
+ db_path = get_db_path(session_id)
85
+ if not db_path.exists():
86
+ return []
87
+ conn = sqlite3.connect(str(db_path))
88
+ conn.row_factory = sqlite3.Row
89
+ rows = conn.execute(
90
+ "SELECT * FROM nodes WHERE parent_id=? ORDER BY order_index",
91
+ (parent_id,)
92
+ ).fetchall()
93
+ conn.close()
94
+ result = []
95
+ for row in rows:
96
+ d = dict(row)
97
+ try:
98
+ d['tips'] = json.loads(d.get('tips', '[]'))
99
+ except:
100
+ d['tips'] = []
101
+ result.append(d)
102
+ return result
103
+
104
+ def add_node_db(session_id: str, parent_id: str, node_type: str, label: str,
105
+ description: str = "", emoji: str = "\U0001f539",
106
+ tips: list = None, order_index: int = 0) -> Dict:
107
+ node_id = str(uuid.uuid4())
108
+ tips_json = json.dumps(tips or [])
109
+ db_path = get_db_path(session_id)
110
+ conn = sqlite3.connect(str(db_path))
111
+ conn.execute(
112
+ "INSERT INTO nodes (id, parent_id, type, label, description, emoji, tips, order_index) VALUES (?,?,?,?,?,?,?,?)",
113
+ (node_id, parent_id, node_type, label, description, emoji, tips_json, order_index)
114
+ )
115
+ conn.commit()
116
+ conn.close()
117
+ return {
118
+ "id": node_id,
119
+ "parent_id": parent_id,
120
+ "type": node_type,
121
+ "label": label,
122
+ "description": description,
123
+ "emoji": emoji,
124
+ "tips": tips or [],
125
+ "order_index": order_index
126
+ }
127
+
128
+ def update_root_db(session_id: str, label: str, description: str = ""):
129
+ db_path = get_db_path(session_id)
130
+ conn = sqlite3.connect(str(db_path))
131
+ conn.execute(
132
+ "UPDATE nodes SET label=?, description=? WHERE parent_id IS NULL",
133
+ (label, description)
134
+ )
135
+ conn.commit()
136
+ conn.close()
137
+
138
+ def get_path_db(session_id: str, node_id: str) -> List[Dict]:
139
+ path = []
140
+ current_id = node_id
141
+ while current_id:
142
+ node = get_node_db(session_id, current_id)
143
+ if node is None:
144
+ break
145
+ path.append(node)
146
+ current_id = node.get("parent_id")
147
+ path.reverse()
148
+ return path
149
+
150
+ def build_path_string(session_id: str, node_id: str) -> str:
151
+ nodes = get_path_db(session_id, node_id)
152
+ parts = []
153
+ for n in nodes:
154
+ t = n["type"]
155
+ label = n["label"]
156
+ if t == "root":
157
+ parts.append(f"[ROOT] {label}")
158
+ elif t == "input":
159
+ parts.append(f"[INPUT] {label}")
160
+ elif t == "outcome":
161
+ parts.append(f"[OUTCOME] {label}")
162
+ return " → ".join(parts)
163
+
164
+ def get_root_node(session_id: str) -> Optional[Dict]:
165
+ db_path = get_db_path(session_id)
166
+ if not db_path.exists():
167
+ return None
168
+ conn = sqlite3.connect(str(db_path))
169
+ conn.row_factory = sqlite3.Row
170
+ row = conn.execute("SELECT * FROM nodes WHERE parent_id IS NULL LIMIT 1").fetchone()
171
+ conn.close()
172
+ if row is None:
173
+ return None
174
+ result = dict(row)
175
+ try:
176
+ result['tips'] = json.loads(result.get('tips', '[]'))
177
+ except:
178
+ result['tips'] = []
179
+ return result
180
+
181
+ def get_all_node_ids(session_id: str) -> List[str]:
182
+ """Get IDs of all nodes in the tree (for full export)."""
183
+ db_path = get_db_path(session_id)
184
+ if not db_path.exists():
185
+ return []
186
+ conn = sqlite3.connect(str(db_path))
187
+ rows = conn.execute("SELECT id FROM nodes").fetchall()
188
+ conn.close()
189
+ return [r[0] for r in rows]
190
+
191
+ def build_tree_nested(session_id: str) -> Optional[Dict]:
192
+ """Build a nested tree structure from the SQLite DB."""
193
+ root = get_root_node(session_id)
194
+ if not root:
195
+ return None
196
+ def build_tree(node):
197
+ children = get_children_db(session_id, node['id'])
198
+ node_copy = dict(node)
199
+ if isinstance(node_copy.get('tips'), str):
200
+ try:
201
+ node_copy['tips'] = json.loads(node_copy['tips'])
202
+ except:
203
+ node_copy['tips'] = []
204
+ node_copy['children'] = [build_tree(c) for c in children]
205
+ return node_copy
206
+ return build_tree(root)
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Prompt Builders (with path_context)
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def build_root_prompt(decision: str) -> str:
213
+ return f'''You are an AI that helps people explore decisions by generating decision trees.
214
+
215
+ Generate a ROOT decision node for the following decision:
216
+
217
+ "{decision}"
218
+
219
+ Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
220
+ {{
221
+ "label": "A concise label for this decision tree (3-6 words)",
222
+ "description": "A 1-2 sentence description of this decision context",
223
+ "emoji": "An emoji representing this decision",
224
+ "tips": ["One actionable tip for approaching this decision"]
225
+ }}'''
226
+
227
+ def build_options_prompt(decision_label: str, decision_desc: str, count: int, path_context: str, comment: str = "") -> str:
228
+ path_section = f'\nFull path from root to this node: "{path_context}"' if path_context else ''
229
+ comment_section = f'\nUser context: "{comment}"' if comment else ''
230
+ return f'''You are an AI that helps explore decisions by generating decision tree branches.
231
+
232
+ Parent node: "{decision_label}"
233
+ Description: "{decision_desc}"{path_section}{comment_section}
234
+
235
+ Generate EXACTLY {count} child nodes that represent different OPTIONS or CHOICES the person could take.
236
+
237
+ IMPORTANT: Frame each child as an OPTION or CHOICE, not as an outcome.
238
+
239
+ Consider the full decision path above to ensure the options are contextually relevant.
240
+
241
+ Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
242
+ {{
243
+ "children": [
244
+ {{
245
+ "id": "child_1",
246
+ "label": "Short option label (3-6 words)",
247
+ "description": "1-2 sentence description",
248
+ "emoji": "An emoji",
249
+ "tips": ["One practical tip"]
250
+ }},
251
+ ...
252
+ ]
253
+ }}
254
+
255
+ Ensure children have unique IDs like child_1, child_2, etc.'''
256
+
257
+ def build_outcomes_prompt(decision_label: str, decision_desc: str, count: int, path_context: str, comment: str = "") -> str:
258
+ path_section = f'\nFull path from root to this node: "{path_context}"' if path_context else ''
259
+ comment_section = f'\nUser context: "{comment}"' if comment else ''
260
+ return f'''You are an AI that helps explore decisions by generating decision tree branches.
261
+
262
+ Parent node: "{decision_label}"
263
+ Description: "{decision_desc}"{path_section}{comment_section}
264
+
265
+ Generate EXACTLY {count} child nodes that represent a DIVERSE RANGE of possible OUTCOMES. Include a MIX of positive, neutral, and negative outcomes.
266
+
267
+ IMPORTANT: Frame each child as an OUTCOME or CONSEQUENCE, not as a choice someone makes.
268
+
269
+ Consider the full decision path above to ensure the outcomes are contextually relevant.
270
+
271
+ Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
272
+ {{
273
+ "children": [
274
+ {{
275
+ "id": "child_1",
276
+ "label": "Short outcome label (3-6 words)",
277
+ "description": "1-2 sentence description",
278
+ "emoji": "An emoji",
279
+ "tips": ["One practical tip"]
280
+ }},
281
+ ...
282
+ ]
283
+ }}
284
+
285
+ Ensure children have unique IDs. Make sure the first child is POSITIVE, the second is NEUTRAL, and the third is NEGATIVE.'''
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # AI Call (using OpenRouter via requests)
289
+ # ---------------------------------------------------------------------------
290
+
291
+ def call_api(prompt: str, system_prompt: str = "You are a helpful assistant that generates decision trees.") -> Optional[str]:
292
+ if not OPENROUTER_API_KEY:
293
+ print("[OpenRouter Error] No API key configured")
294
+ return None
295
+ try:
296
+ headers = {
297
+ 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
298
+ 'Content-Type': 'application/json',
299
+ 'HTTP-Referer': 'http://localhost:7860',
300
+ 'X-Title': 'Overthinker'
301
+ }
302
+ data = {
303
+ 'model': DEFAULT_MODEL,
304
+ 'messages': [
305
+ {'role': 'system', 'content': system_prompt},
306
+ {'role': 'user', 'content': prompt}
307
+ ],
308
+ 'temperature': 0.8,
309
+ 'max_tokens': 2048
310
+ }
311
+ response = requests.post(
312
+ OPENROUTER_URL,
313
+ headers=headers,
314
+ json=data,
315
+ timeout=30
316
+ )
317
+ if response.status_code == 200:
318
+ result = response.json()
319
+ return result['choices'][0]['message']['content']
320
+ else:
321
+ print(f"[OpenRouter Error] {response.status_code}: {response.text}")
322
+ except Exception as e:
323
+ print(f"[OpenRouter Exception] {e}")
324
+ return None
325
+
326
+ def parse_json_response(text: str) -> Optional[dict]:
327
+ if not text:
328
+ return None
329
+ text = text.strip()
330
+ import re
331
+ text = re.sub(r'```json\s*', '', text)
332
+ text = re.sub(r'```\s*', '', text)
333
+ text = text.strip()
334
+ start = text.find('{')
335
+ end = text.rfind('}')
336
+ if start >= 0 and end > start:
337
+ text = text[start:end+1]
338
+ try:
339
+ return json.loads(text)
340
+ except json.JSONDecodeError as e:
341
+ print(f"[JSON Parse Error] {e}")
342
+ print(f"[Raw text] {text[:500]}")
343
+ return None
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # Routes (All POST, no GET except for serving index)
347
+ # ---------------------------------------------------------------------------
348
+
349
+ @app.get("/")
350
+ async def index():
351
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates", "index.html")
352
+ if os.path.exists(html_path):
353
+ with open(html_path, "r", encoding="utf-8") as f:
354
+ return HTMLResponse(content=f.read(), status_code=200)
355
+ return HTMLResponse(content="<h1>Overthinker</h1><p>index.html not found</p>", status_code=404)
356
+
357
+ @app.post("/root")
358
+ async def create_root(request: dict):
359
+ session_id = request.get('session_id', str(uuid.uuid4()))
360
+ init_session(session_id)
361
+ root = get_root_node(session_id)
362
+ if root is None:
363
+ raise HTTPException(status_code=500, detail="Could not initialize session.")
364
+ return {"session_id": session_id, "node": root}
365
+
366
+ @app.post("/create_tree")
367
+ async def create_tree(request: dict):
368
+ session_id = request.get('session_id', str(uuid.uuid4()))
369
+ decision = request.get('decision', '')
370
+ if not decision:
371
+ raise HTTPException(status_code=400, detail="Decision text is required.")
372
+ init_session(session_id)
373
+ prompt = build_root_prompt(decision)
374
+ ai_response = call_api(prompt)
375
+ parsed = parse_json_response(ai_response) if ai_response else None
376
+ if not parsed:
377
+ raise HTTPException(status_code=500, detail="Failed to generate root node. Please check your API key and try again.")
378
+ label = parsed.get('label', f'Overthinking: {decision[:40]}')
379
+ description = parsed.get('description', f'You are overthinking: {decision}')
380
+ emoji = parsed.get('emoji', '\U0001f333')
381
+ tips = parsed.get('tips', ['Start by exploring options.'])
382
+ update_root_db(session_id, label, description)
383
+ db_path = get_db_path(session_id)
384
+ conn = sqlite3.connect(str(db_path))
385
+ conn.execute("UPDATE nodes SET emoji=?, tips=? WHERE parent_id IS NULL", (emoji, json.dumps(tips)))
386
+ conn.commit()
387
+ conn.close()
388
+ root = get_root_node(session_id)
389
+ return {'session_id': session_id, 'node': root}
390
+
391
+ @app.post("/get_node")
392
+ async def get_node_endpoint(request: dict):
393
+ session_id = request.get('session_id')
394
+ node_id = request.get('node_id')
395
+ if not session_id or not node_id:
396
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
397
+ init_session(session_id)
398
+ node = get_node_db(session_id, node_id)
399
+ if node is None:
400
+ raise HTTPException(status_code=404, detail="Node not found")
401
+ children = get_children_db(session_id, node_id)
402
+ path_context = build_path_string(session_id, node_id)
403
+ return {
404
+ 'node': node,
405
+ 'children': children,
406
+ 'path_context': path_context
407
+ }
408
+
409
+ @app.post("/get_children")
410
+ async def get_children(request: dict):
411
+ session_id = request.get('session_id')
412
+ node_id = request.get('node_id')
413
+ count = request.get('count', 3)
414
+ node_type = request.get('node_type', 'outcome')
415
+ comment = request.get('comment', '')
416
+ if not session_id or not node_id:
417
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
418
+ init_session(session_id)
419
+ parent = get_node_db(session_id, node_id)
420
+ if parent is None:
421
+ raise HTTPException(status_code=404, detail="Node not found")
422
+ path_context = build_path_string(session_id, node_id)
423
+ next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
424
+ next_type = next_type_map.get(node_type, 'outcome')
425
+ parent_label = parent.get('label', 'Unknown')
426
+ parent_desc = parent.get('description', '')
427
+ if next_type == 'input':
428
+ prompt = build_options_prompt(parent_label, parent_desc, count, path_context, comment)
429
+ else:
430
+ prompt = build_outcomes_prompt(parent_label, parent_desc, count, path_context, comment)
431
+ ai_response = call_api(prompt)
432
+ parsed = parse_json_response(ai_response) if ai_response else None
433
+ if not parsed or 'children' not in parsed or not isinstance(parsed['children'], list):
434
+ raise HTTPException(status_code=500, detail="Generation failed. Please check your API key and try again.")
435
+ children_data = parsed['children']
436
+ children = []
437
+ for i, child in enumerate(children_data):
438
+ label = child.get('label', 'Unknown')
439
+ description = child.get('description', '')
440
+ emoji = child.get('emoji', '\U0001f539')
441
+ tips = child.get('tips', [f'Consider this {next_type}.'])
442
+ existing = get_children_db(session_id, node_id)
443
+ existing_labels = [c['label'] for c in existing]
444
+ if label in existing_labels or label in [c['label'] for c in children]:
445
+ label = f"{label} ({i+1})"
446
+ child_node = add_node_db(session_id, node_id, next_type, label, description, emoji, tips, order_index=i)
447
+ child_node['type'] = next_type
448
+ children.append(child_node)
449
+ return {'children': children, 'next_type': next_type}
450
+
451
+ @app.post("/add_options")
452
+ async def add_options(request: dict):
453
+ session_id = request.get('session_id')
454
+ node_id = request.get('node_id')
455
+ count = request.get('count', 3)
456
+ comment = request.get('comment', '')
457
+ if not session_id or not node_id:
458
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
459
+ init_session(session_id)
460
+ parent = get_node_db(session_id, node_id)
461
+ if parent is None:
462
+ raise HTTPException(status_code=404, detail="Node not found")
463
+ path_context = build_path_string(session_id, node_id)
464
+ next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
465
+ next_type = next_type_map.get(parent.get('type', 'root'), 'outcome')
466
+ parent_label = parent.get('label', 'Unknown')
467
+ parent_desc = parent.get('description', '')
468
+ if next_type == 'input':
469
+ prompt = build_options_prompt(parent_label, parent_desc, count, path_context, comment)
470
+ else:
471
+ prompt = build_outcomes_prompt(parent_label, parent_desc, count, path_context, comment)
472
+ ai_response = call_api(prompt)
473
+ parsed = parse_json_response(ai_response) if ai_response else None
474
+ if not parsed or 'children' not in parsed or not isinstance(parsed['children'], list):
475
+ raise HTTPException(status_code=500, detail="Failed to add options. Please try again.")
476
+ children_data = parsed['children']
477
+ children = []
478
+ for i, child in enumerate(children_data):
479
+ label = child.get('label', 'Unknown')
480
+ description = child.get('description', '')
481
+ emoji = child.get('emoji', '\U0001f539')
482
+ tips = child.get('tips', [f'Additional {next_type}.'])
483
+ existing = get_children_db(session_id, node_id)
484
+ existing_labels = [c['label'] for c in existing]
485
+ if label in existing_labels or label in [c['label'] for c in children]:
486
+ label = f"{label} ({i+1})"
487
+ child_node = add_node_db(session_id, node_id, next_type, label, description, emoji, tips, order_index=i)
488
+ child_node['type'] = next_type
489
+ children.append(child_node)
490
+ return {'children': children, 'next_type': next_type}
491
+
492
+ @app.post("/upload_trace")
493
+ async def upload_trace(request: dict):
494
+ """Serialize the full tree from SQLite and push to HuggingFace dataset."""
495
+ session_id = request.get('session_id')
496
+ if not session_id:
497
+ raise HTTPException(status_code=400, detail="Missing session_id")
498
+
499
+ if not HF_TOKEN or not HF_DATASET_REPO:
500
+ raise HTTPException(status_code=500, detail="HF_TOKEN and HF_DATASET_REPO must be configured in environment.")
501
+
502
+ tree = build_tree_nested(session_id)
503
+ if tree is None:
504
+ raise HTTPException(status_code=404, detail="No tree found for this session.")
505
+
506
+ try:
507
+ from datasets import Dataset, concatenate_datasets, load_dataset
508
+ import pandas as pd
509
+
510
+ row = {
511
+ 'session_id': session_id,
512
+ 'tree_json': json.dumps(tree),
513
+ 'created_at': str(tree.get('created_at', ''))
514
+ }
515
+ df = pd.DataFrame([row])
516
+ new_dataset = Dataset.from_pandas(df)
517
+
518
+ try:
519
+ existing_dataset = load_dataset(HF_DATASET_REPO, split='train', token=HF_TOKEN)
520
+ combined = concatenate_datasets([existing_dataset, new_dataset])
521
+ except Exception:
522
+ combined = new_dataset
523
+
524
+ combined.push_to_hub(HF_DATASET_REPO, token=HF_TOKEN, private=False)
525
+
526
+ return {'status': 'success', 'message': 'Trace uploaded successfully!'}
527
+ except Exception as e:
528
+ print(f"[Upload Trace Error] {e}")
529
+ raise HTTPException(status_code=500, detail=f"Failed to upload trace: {str(e)}")
530
+
531
+ @app.post("/export_json")
532
+ async def export_json(request: dict):
533
+ session_id = request.get('session_id')
534
+ if not session_id:
535
+ raise HTTPException(status_code=400, detail="Missing session_id")
536
+ root = get_root_node(session_id)
537
+ if not root:
538
+ raise HTTPException(status_code=404, detail="No tree found")
539
+ def build_tree(node):
540
+ children = get_children_db(session_id, node['id'])
541
+ node_copy = dict(node)
542
+ node_copy['children'] = [build_tree(c) for c in children]
543
+ return node_copy
544
+ full_tree = build_tree(root)
545
+ return full_tree
546
+
547
+ @app.post("/export_path_json")
548
+ async def export_path_json(request: dict):
549
+ session_id = request.get('session_id')
550
+ node_id = request.get('node_id')
551
+ if not session_id or not node_id:
552
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
553
+ path_nodes = get_path_db(session_id, node_id)
554
+ return {'path': path_nodes}
555
+
556
+ @app.post("/export_path_md")
557
+ async def export_path_md(request: dict):
558
+ session_id = request.get('session_id')
559
+ node_id = request.get('node_id')
560
+ if not session_id or not node_id:
561
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
562
+ path = get_path_db(session_id, node_id)
563
+ md = '# \U0001f9e0 Overthinker — Decision Path\n\n'
564
+ for i, node in enumerate(path):
565
+ indent = ' ' * i
566
+ emoji = {'root': '\U0001f333', 'input': '\U0001f9e0', 'outcome': '\U0001f4ca'}.get(node.get('type', ''), '\U0001f4cc')
567
+ md += f'{indent}{emoji} **{node.get("label", "")}**\n'
568
+ if node.get('description'):
569
+ md += f'{indent} > {node.get("description", "")}\n'
570
+ if node.get('tips') and len(node['tips']) > 0:
571
+ md += f'{indent} > \U0001f4a1 {node["tips"][0]}\n'
572
+ md += '\n'
573
+ return PlainTextResponse(content=md, status_code=200)
574
+
575
+ # ---------------------------------------------------------------------------
576
+ # Launch
577
+ # ---------------------------------------------------------------------------
578
+ if __name__ == "__main__":
579
+ import requests # needed for sync calls inside async routes
580
+ print(f"\U0001f9e0 Overthinker — SQLite Session Mode + HF Trace Upload on port {PORT}")
581
+ print(f"\U0001f916 Model: {DEFAULT_MODEL}")
582
+ print(f"\U0001f310 Open http://localhost:{PORT} in your browser")
583
+ if not OPENROUTER_API_KEY:
584
+ print("\u26a0\ufe0f No OPENROUTER_API_KEY found. Add to .env or environment. Generation will fail.")
585
+ if not HF_TOKEN or not HF_DATASET_REPO:
586
+ print("\u26a0\ufe0f No HF_TOKEN or HF_DATASET_REPO set. Upload will fail.")
587
+ app.launch(
588
+ server_port=PORT,
589
+ show_error=True,
590
+ share=False
591
+ )