broadfield-dev commited on
Commit
aef5dad
·
verified ·
1 Parent(s): 6ab835c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +433 -475
app.py CHANGED
@@ -1,283 +1,222 @@
1
  #!/usr/bin/env python3
2
  """
3
- Overthinker — Gradio Server Backend
 
 
 
 
 
 
 
4
 
5
- Dual Prompt Architecture:
6
- - Input nodes -> prompts to generate OPTIONS/CHOICES/DECISIONS
7
- - Outcome nodes -> prompts to generate OUTCOMES/CONSEQUENCES
8
- - Model: nvidia/nemotron-3-nano-30b-a3b via OpenRouter
9
  Port: 7860
10
  """
11
 
12
  import os
13
  import json
14
- import sys
15
  import uuid
16
- import random
17
- import re
18
- import html as html_mod
19
- from datetime import datetime
20
- from typing import Optional, List, Dict, Any
21
-
22
- import requests
23
- from dotenv import load_dotenv
24
 
25
  from gradio import Server
26
  from fastapi import HTTPException
27
- from starlette.responses import HTMLResponse
28
-
29
- load_dotenv()
30
-
31
- OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
32
 
33
  # ---------------------------------------------------------------------------
34
  # Application Setup
35
  # ---------------------------------------------------------------------------
36
  app = Server()
37
  PORT = 7860
38
- DEFAULT_MODEL = "nvidia/nemotron-3-nano-30b-a3b"
39
-
40
- # ---------------------------------------------------------------------------
41
- # Node Manager
42
- # ---------------------------------------------------------------------------
43
- class NodeManager:
44
- def __init__(self):
45
- self.trees: Dict[str, Dict[str, Any]] = {}
46
-
47
- def create_tree(self, tree_id: str, root_node: dict) -> None:
48
- self.trees[tree_id] = root_node
49
-
50
- def get_tree(self, tree_id: str) -> Optional[dict]:
51
- return self.trees.get(tree_id)
52
-
53
- def get_node(self, tree_id: str, node_id: str) -> Optional[dict]:
54
- tree = self.trees.get(tree_id)
55
- if not tree:
56
- return None
57
- return self._find_node(tree, node_id)
58
-
59
- def _find_node(self, node: dict, node_id: str) -> Optional[dict]:
60
- if node.get('id') == node_id:
61
- return node
62
- for child in node.get('children', []):
63
- result = self._find_node(child, node_id)
64
- if result:
65
- return result
66
- return None
67
 
68
- def add_children(self, tree_id: str, parent_id: str, children: List[dict]) -> bool:
69
- parent = self.get_node(tree_id, parent_id)
70
- if parent is None:
71
- return False
72
- if 'children' not in parent:
73
- parent['children'] = []
74
- parent['children'].extend(children)
75
- return True
76
-
77
- def delete_tree(self, tree_id: str) -> None:
78
- if tree_id in self.trees:
79
- del self.trees[tree_id]
80
-
81
- def to_dict(self, tree_id: str) -> Optional[dict]:
82
- return self.trees.get(tree_id)
83
-
84
- def get_path(self, tree_id: str, node_id: str) -> List[dict]:
85
- tree = self.trees.get(tree_id)
86
- if not tree:
87
- return []
88
- path = []
89
- self._find_path(tree, node_id, path)
90
- return path
91
-
92
- def _find_path(self, node: dict, target_id: str, path: List[dict]) -> bool:
93
- path.append(node)
94
- if node.get('id') == target_id:
95
- return True
96
- for child in node.get('children', []):
97
- if self._find_path(child, target_id, path):
98
- return True
99
- path.pop()
100
- return False
101
 
102
- node_manager = NodeManager()
 
103
 
104
  # ---------------------------------------------------------------------------
105
- # History Manager
106
  # ---------------------------------------------------------------------------
107
- class HistoryManager:
108
- def __init__(self):
109
- self.history: Dict[str, Dict] = {}
110
-
111
- def init_tree(self, tree_id: str) -> None:
112
- self.history[tree_id] = {'undo': [], 'redo': []}
113
-
114
- def push_state(self, tree_id: str, state: dict) -> None:
115
- if tree_id not in self.history:
116
- self.init_tree(tree_id)
117
- self.history[tree_id]['undo'].append(state)
118
- self.history[tree_id]['redo'] = []
119
-
120
- def undo(self, tree_id: str) -> Optional[dict]:
121
- if tree_id not in self.history or not self.history[tree_id]['undo']:
122
- return None
123
- state = self.history[tree_id]['undo'].pop()
124
- self.history[tree_id]['redo'].append(state)
125
- return state
126
-
127
- def redo(self, tree_id: str) -> Optional[dict]:
128
- if tree_id not in self.history or not self.history[tree_id]['redo']:
129
- return None
130
- state = self.history[tree_id]['redo'].pop()
131
- self.history[tree_id]['undo'].append(state)
132
- return state
133
-
134
- history_manager = HistoryManager()
135
 
136
- # ---------------------------------------------------------------------------
137
- # LLM API Call — OpenRouter
138
- # ---------------------------------------------------------------------------
139
- def call_api(prompt: str, system_prompt: str = "You are a helpful assistant that generates decision trees.") -> Optional[str]:
140
- if not OPENROUTER_API_KEY:
141
- print("[OpenRouter Error] No API key configured")
142
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
 
 
 
 
 
 
 
 
 
 
 
144
  try:
145
- headers = {
146
- 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
147
- 'Content-Type': 'application/json',
148
- 'HTTP-Referer': 'http://localhost:7860',
149
- 'X-Title': 'Overthinker'
150
- }
151
- data = {
152
- 'model': DEFAULT_MODEL,
153
- 'messages': [
154
- {'role': 'system', 'content': system_prompt},
155
- {'role': 'user', 'content': prompt}
156
- ],
157
- 'temperature': 0.8,
158
- 'max_tokens': 2048
159
- }
160
- response = requests.post(
161
- 'https://openrouter.ai/api/v1/chat/completions',
162
- headers=headers,
163
- json=data,
164
- timeout=30
165
- )
166
- if response.status_code == 200:
167
- result = response.json()
168
- return result['choices'][0]['message']['content']
169
- else:
170
- print(f"[OpenRouter Error] {response.status_code}: {response.text}")
171
- except Exception as e:
172
- print(f"[OpenRouter Exception] {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  # ---------------------------------------------------------------------------
177
- # Fallback Generators
178
  # ---------------------------------------------------------------------------
179
- def _fallback_options(decision: str, context: str = "") -> dict:
180
- """Generate fallback children that read as options/choices."""
181
- import random
182
- options = [
183
- f"Approach this by seeking more information",
184
- f"Consider consulting with trusted advisors",
185
- f"Take a gradual step-by-step approach",
186
- f"Weigh the pros and cons carefully",
187
- f"Commit to a trial period before deciding",
188
- f"Seek out alternative perspectives",
189
- f"Prioritize your core values in the decision",
190
- f"Create a contingency plan for risks",
191
- f"Focus on short-term wins first",
192
- f"Delegate aspects to reduce overwhelm"
193
- ]
194
- random.shuffle(options)
195
- children = []
196
- for i in range(min(3, len(options))):
197
- child = {
198
- 'id': f'local_{datetime.now().timestamp()}_{i}_{random.randint(100,999)}',
199
- 'label': options[i],
200
- 'description': f'A possible approach: {options[i].lower()}. Consider how this option aligns with your goals.',
201
- 'type': 'input',
202
- 'tips': [f'Evaluate this option in the context of your overall decision.'],
203
- 'children': []
204
- }
205
- children.append(child)
206
- return {'children': children}
207
-
208
-
209
- def _fallback_outcomes(decision: str, context: str = "") -> dict:
210
- """Generate fallback children that read as outcomes (positive, neutral, negative)."""
211
- import random
212
- positive_outcomes = [
213
- "Financial stability increases substantially",
214
- "Personal growth accelerates beyond expectations",
215
- "Relationships deepen and strengthen",
216
- "Professional opportunities multiply rapidly",
217
- "Your confidence and competence both grow",
218
- "A supportive community forms around you",
219
- "Health and well-being improve noticeably",
220
- "Creative energy flows more freely",
221
- "Unexpected doors open in your career",
222
- "Your resilience is strengthened by this path"
223
- ]
224
- neutral_outcomes = [
225
- "Your daily routine shifts gradually",
226
- "Your perspective on the situation evolves",
227
- "You gain new skills through necessity",
228
- "Your priorities naturally reorganize",
229
- "The outcome depends largely on external factors",
230
- "Your support network becomes more defined",
231
- "Both opportunities and risks emerge simultaneously",
232
- "Time reveals consequences that aren't immediately visible",
233
- "Your understanding of the situation deepens",
234
- "The path forward becomes clearer over time"
235
- ]
236
- negative_outcomes = [
237
- "Financial resources become stretched thin",
238
- "Relationships come under significant strain",
239
- "Your stress levels increase substantially",
240
- "Career progress slows or stalls temporarily",
241
- "Health issues may arise from increased pressure",
242
- "Your support network may dwindle",
243
- "Opportunity costs become more apparent",
244
- "Self-doubt and uncertainty intensify",
245
- "Work-life balance deteriorates further",
246
- "The decision leads to unforeseen complications"
247
- ]
248
- pos = random.choice(positive_outcomes)
249
- neu = random.choice(neutral_outcomes)
250
- neg = random.choice(negative_outcomes)
251
- sentiments = ['positive', 'neutral', 'negative']
252
- labels = [pos, neu, neg]
253
- desc_map = {
254
- 'positive': f'If this path unfolds favorably, {pos.lower()}. This represents a best-case scenario where your decision leads to growth and improvement.',
255
- 'neutral': f'On this path, {neu.lower()}. The outcome is neither clearly good nor bad — it requires careful monitoring.',
256
- 'negative': f'In a challenging scenario, {neg.lower()}. This represents potential risks and difficulties that may arise.'
257
- }
258
- tip_map = {
259
- 'positive': 'Nurture this positive outcome by staying engaged and proactive.',
260
- 'neutral': 'Monitor this neutral path closely — small changes can shift the outcome.',
261
- 'negative': 'Prepare contingency plans to mitigate this risk if it materializes.'
262
- }
263
- children = []
264
- for i in range(3):
265
- sentiment = sentiments[i]
266
- child = {
267
- 'id': f'local_{datetime.now().timestamp()}_{i}_{random.randint(100,999)}',
268
- 'label': labels[i],
269
- 'description': desc_map[sentiment],
270
- 'type': 'outcome',
271
- 'tips': [tip_map[sentiment]],
272
- 'children': [],
273
- 'sentiment': sentiment
274
- }
275
- children.append(child)
276
- return {'children': children}
277
 
278
- # ---------------------------------------------------------------------------
279
- # Prompt Builders — Dual prompts
280
- # ---------------------------------------------------------------------------
281
  def build_root_prompt(decision: str) -> str:
282
  return f'''You are an AI that helps people explore decisions by generating decision trees.
283
 
@@ -289,86 +228,114 @@ Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
289
  {{
290
  "label": "A concise label for this decision tree (3-6 words)",
291
  "description": "A 1-2 sentence description of this decision context",
292
- "type": "root",
293
  "tips": ["One actionable tip for approaching this decision"]
294
  }}'''
295
 
296
-
297
- def build_options_prompt(decision_label: str, decision_desc: str, count: int, comment: str = "") -> str:
298
- """
299
- Prompt for generating OPTIONS/CHOICES/DECISIONS (for input-type nodes).
300
- """
301
- comment_section = f'\nUser context/comment: "{comment}"' if comment else ''
302
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
303
 
304
  Parent node: "{decision_label}"
305
- Description: "{decision_desc}"{comment_section}
 
 
306
 
307
- Generate EXACTLY {count} child nodes that represent different OPTIONS or CHOICES the person could take. Each child should be a distinct, realistic possibility an actionable decision they could make at this point.
308
 
309
- IMPORTANT: Frame each child as an OPTION or CHOICE, not as an outcome. For example:
310
- - GOOD: "Consult a financial advisor" (describes an action/choice)
311
- - BAD: "Financial situation improves" (describes an outcome — DO NOT use here)
312
 
313
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
314
  {{
315
  "children": [
316
  {{
317
  "id": "child_1",
318
- "label": "Short option label (3-6 words) describing the choice",
319
- "description": "1-2 sentence description of this option and its potential implications",
320
- "type": "input",
321
- "tips": ["One practical tip for evaluating this option"]
322
  }},
323
- ...repeat for each child...
324
  ]
325
  }}
326
 
327
  Ensure children have unique IDs like child_1, child_2, etc.'''
328
 
329
-
330
- def build_outcomes_prompt(decision_label: str, decision_desc: str, count: int, comment: str = "") -> str:
331
- """
332
- Prompt for generating OUTCOMES/CONSEQUENCES (for outcome-type nodes).
333
- Requests a diverse range: positive, neutral, and negative outcomes.
334
- """
335
- comment_section = f'\nUser context/comment: "{comment}"' if comment else ''
336
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
337
 
338
  Parent node: "{decision_label}"
339
- Description: "{decision_desc}"{comment_section}
340
 
341
- Generate EXACTLY {count} child nodes that represent a DIVERSE RANGE of possible OUTCOMES or CONSEQUENCES that could naturally follow from this decision path. Include a MIX of positive, neutral, and negative outcomes so the user sees the full spectrum of possibilities.
342
 
343
- - Positive outcomes: beneficial results, improvements, gains
344
- - Neutral outcomes: mixed results, neither clearly good nor bad, trade-offs
345
- - Negative outcomes: risks, challenges, downsides
346
 
347
- IMPORTANT: Frame each child as an OUTCOME or CONSEQUENCE, not as a choice someone makes. For example:
348
- - GOOD: "Financial stability improves" (describes a result)
349
- - BAD: "Consider financial planning" (describes a choice — DO NOT do this)
350
 
351
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
352
  {{
353
  "children": [
354
  {{
355
  "id": "child_1",
356
- "label": "Short outcome label (3-6 words) describing the result",
357
- "description": "1-2 sentence description of this possible consequence",
358
- "type": "outcome",
359
- "tips": ["One practical tip for navigating this outcome"]
360
  }},
361
- ...repeat for each child...
362
  ]
363
  }}
364
 
365
- Ensure children have unique IDs like child_1, child_2, etc. Make sure the first child is a POSITIVE outcome, the second is NEUTRAL, and the third is NEGATIVE to guarantee diversity.'''
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
  def parse_json_response(text: str) -> Optional[dict]:
369
  if not text:
370
  return None
371
  text = text.strip()
 
372
  text = re.sub(r'```json\s*', '', text)
373
  text = re.sub(r'```\s*', '', text)
374
  text = text.strip()
@@ -384,7 +351,7 @@ def parse_json_response(text: str) -> Optional[dict]:
384
  return None
385
 
386
  # ---------------------------------------------------------------------------
387
- # Routes
388
  # ---------------------------------------------------------------------------
389
 
390
  @app.get("/")
@@ -395,224 +362,213 @@ async def index():
395
  return HTMLResponse(content=f.read(), status_code=200)
396
  return HTMLResponse(content="<h1>Overthinker</h1><p>index.html not found</p>", status_code=404)
397
 
 
 
 
 
 
 
 
 
398
 
399
  @app.post("/create_tree")
400
  async def create_tree(request: dict):
401
- decision = request.get('decision', 'Make a decision')
402
- tree_id = f'tree_{datetime.now().timestamp()}'
 
 
 
403
  prompt = build_root_prompt(decision)
404
  ai_response = call_api(prompt)
405
  parsed = parse_json_response(ai_response) if ai_response else None
406
- if parsed:
407
- root_node = {
408
- 'id': tree_id,
409
- 'label': parsed.get('label', f'Overthinking: {decision[:40]}'),
410
- 'description': parsed.get('description', f'You are overthinking: {decision}'),
411
- 'type': 'root',
412
- 'tips': parsed.get('tips', ['Start by exploring the first set of options.']),
413
- 'children': [],
414
- }
415
- else:
416
- root_node = {
417
- 'id': tree_id,
418
- 'label': f'Overthinking: {decision[:40]}',
419
- 'description': f'You are overthinking: {decision}',
420
- 'type': 'root',
421
- 'tips': ['Start by exploring the first set of options.'],
422
- 'children': [],
423
- }
424
- node_manager.create_tree(tree_id, root_node)
425
- history_manager.init_tree(tree_id)
426
- history_manager.push_state(tree_id, {'action': 'create', 'tree': root_node})
427
- return {'tree_id': tree_id, 'root_node': root_node}
428
-
429
-
430
- @app.get("/get_children")
431
- async def get_children(node_id: str, count: int = 3, node_type: str = "outcome", comment: Optional[str] = None):
432
- # Find which tree contains this node
433
- tree_id = None
434
- for tid in list(node_manager.trees.keys()):
435
- node = node_manager.get_node(tid, node_id)
436
- if node:
437
- tree_id = tid
438
- break
439
- if not tree_id:
440
  raise HTTPException(status_code=404, detail="Node not found")
441
- parent = node_manager.get_node(tree_id, node_id)
442
- if not parent:
443
- raise HTTPException(status_code=404, detail="Parent node not found")
 
 
 
 
444
 
445
- # Determine what type of children to generate (input -> options; outcome -> outcomes)
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
447
  next_type = next_type_map.get(node_type, 'outcome')
448
-
449
  parent_label = parent.get('label', 'Unknown')
450
  parent_desc = parent.get('description', '')
451
-
452
- # Select the appropriate prompt and fallback based on next_type
453
  if next_type == 'input':
454
- prompt = build_options_prompt(parent_label, parent_desc, count, comment or "")
455
- fallback_func = _fallback_options
456
  else:
457
- prompt = build_outcomes_prompt(parent_label, parent_desc, count, comment or "")
458
- fallback_func = _fallback_outcomes
459
-
460
  ai_response = call_api(prompt)
461
  parsed = parse_json_response(ai_response) if ai_response else None
462
-
463
- if parsed and 'children' in parsed:
464
- children = parsed['children']
465
- for i, child in enumerate(children):
466
- if not child.get('id') or child['id'] == f'child_{i+1}':
467
- child['id'] = f'{node_id}_child_{i}_{uuid.uuid4().hex[:8]}'
468
- child['type'] = next_type
469
- if 'tips' not in child or not child['tips']:
470
- child['tips'] = [f'Consider this {next_type} as a possible next step.']
471
- if 'children' not in child:
472
- child['children'] = []
473
- node_manager.add_children(tree_id, node_id, children)
474
- history_manager.push_state(tree_id, {'action': 'expand', 'node_id': node_id, 'children': children})
475
- return {'children': children}
476
-
477
- # Fallback
478
- fallback = fallback_func(parent_label, parent_desc)
479
- children = fallback.get('children', [])
480
- for i, child in enumerate(children):
481
- child['id'] = f'{node_id}_child_{i}_{uuid.uuid4().hex[:8]}'
482
- child['type'] = next_type
483
- node_manager.add_children(tree_id, node_id, children)
484
- history_manager.push_state(tree_id, {'action': 'expand', 'node_id': node_id, 'children': children})
485
- return {'children': children}
486
-
487
 
488
  @app.post("/add_options")
489
  async def add_options(request: dict):
490
- node_id = request.get('node_id', '')
 
491
  count = request.get('count', 3)
492
- tree_id = None
493
- for tid in list(node_manager.trees.keys()):
494
- node = node_manager.get_node(tid, node_id)
495
- if node:
496
- tree_id = tid
497
- break
498
- if not tree_id:
499
  raise HTTPException(status_code=404, detail="Node not found")
500
- parent = node_manager.get_node(tree_id, node_id)
501
- if not parent:
502
- raise HTTPException(status_code=404, detail="Parent node not found")
503
-
504
  parent_label = parent.get('label', 'Unknown')
505
  parent_desc = parent.get('description', '')
506
- node_type = parent.get('type', 'root')
507
- next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
508
- next_type = next_type_map.get(node_type, 'outcome')
509
-
510
  if next_type == 'input':
511
- prompt = build_options_prompt(parent_label, parent_desc, count)
512
- fallback_func = _fallback_options
513
  else:
514
- prompt = build_outcomes_prompt(parent_label, parent_desc, count)
515
- fallback_func = _fallback_outcomes
516
-
517
  ai_response = call_api(prompt)
518
  parsed = parse_json_response(ai_response) if ai_response else None
519
-
520
- if parsed and 'children' in parsed:
521
- children = parsed['children']
522
- for i, child in enumerate(children):
523
- if not child.get('id') or child['id'] == f'child_{i+1}':
524
- child['id'] = f'{node_id}_extra_{i}_{uuid.uuid4().hex[:8]}'
525
- child['type'] = next_type
526
- if 'tips' not in child or not child['tips']:
527
- child['tips'] = [f'Additional {next_type} to consider.']
528
- if 'children' not in child:
529
- child['children'] = []
530
- node_manager.add_children(tree_id, node_id, children)
531
- history_manager.push_state(tree_id, {'action': 'add', 'node_id': node_id, 'children': children})
532
- return {'children': children}
533
-
534
- fallback = fallback_func(parent_label, parent_desc)
535
- children = fallback.get('children', [])
536
- for i, child in enumerate(children):
537
- child['id'] = f'{node_id}_extra_{i}_{uuid.uuid4().hex[:8]}'
538
- child['type'] = next_type
539
- node_manager.add_children(tree_id, node_id, children)
540
- history_manager.push_state(tree_id, {'action': 'add', 'node_id': node_id, 'children': children})
541
- return {'children': children}
542
-
543
-
544
- @app.get("/get_tree")
545
- async def get_tree_get(tree_id: Optional[str] = None):
546
- if not tree_id:
547
- for tid in list(node_manager.trees.keys()):
548
- tree_id = tid
549
- break
550
- if not tree_id or tree_id not in node_manager.trees:
551
- raise HTTPException(status_code=404, detail="Tree not found")
552
- return node_manager.to_dict(tree_id)
553
-
554
-
555
- @app.post("/get_tree")
556
- async def get_tree_post(request: dict):
557
- tree_id = request.get('tree_id', '')
558
- if not tree_id:
559
- for tid in list(node_manager.trees.keys()):
560
- tree_id = tid
561
- break
562
- if not tree_id or tree_id not in node_manager.trees:
563
- raise HTTPException(status_code=404, detail="Tree not found")
564
- return node_manager.to_dict(tree_id)
565
-
566
-
567
- @app.post("/undo")
568
- async def undo(request: dict):
569
- tree_id = request.get('tree_id', '')
570
- if not tree_id:
571
- return {'error': 'No tree ID provided'}
572
- state = history_manager.undo(tree_id)
573
- if not state:
574
- return {'error': 'Nothing to undo'}
575
- return {'success': True, 'message': 'Undo performed'}
576
-
577
-
578
- @app.post("/redo")
579
- async def redo(request: dict):
580
- tree_id = request.get('tree_id', '')
581
- if not tree_id:
582
- return {'error': 'No tree ID provided'}
583
- state = history_manager.redo(tree_id)
584
- if not state:
585
- return {'error': 'Nothing to redo'}
586
- return {'success': True, 'message': 'Redo performed'}
587
-
588
 
589
  @app.post("/export_json")
590
  async def export_json(request: dict):
591
- tree_id = request.get('tree_id', '')
592
- if not tree_id or tree_id not in node_manager.trees:
593
- raise HTTPException(status_code=404, detail="Tree not found")
594
- tree = node_manager.to_dict(tree_id)
595
- return tree
596
-
 
 
 
 
 
 
 
597
 
598
  @app.post("/export_path_json")
599
  async def export_path_json(request: dict):
600
- tree_id = request.get('tree_id', '')
601
- node_id = request.get('node_id', '')
602
- if not tree_id or not node_id:
603
- return {'error': 'Missing tree_id or node_id'}
604
- path = node_manager.get_path(tree_id, node_id)
605
- return {'path': path}
606
-
607
 
608
  @app.post("/export_path_md")
609
  async def export_path_md(request: dict):
610
- tree_id = request.get('tree_id', '')
611
- node_id = request.get('node_id', '')
612
- if not tree_id or not node_id:
613
- raise HTTPException(status_code=400, detail="Missing tree_id or node_id")
614
- path = node_manager.get_path(tree_id, node_id)
615
- md = '# \U0001f9e0 Overthinker \u2014 Decision Path\n\n'
616
  for i, node in enumerate(path):
617
  indent = ' ' * i
618
  emoji = {'root': '\U0001f333', 'input': '\U0001f9e0', 'outcome': '\U0001f4ca'}.get(node.get('type', ''), '\U0001f4cc')
@@ -622,20 +578,22 @@ async def export_path_md(request: dict):
622
  if node.get('tips') and len(node['tips']) > 0:
623
  md += f'{indent} > \U0001f4a1 {node["tips"][0]}\n'
624
  md += '\n'
625
- return md
626
 
627
  # ---------------------------------------------------------------------------
628
  # Launch
629
  # ---------------------------------------------------------------------------
630
  if __name__ == "__main__":
631
- print(f"\U0001f9e0 Overthinker \u2014 Starting on port {PORT}")
 
632
  print(f"\U0001f916 Model: {DEFAULT_MODEL}")
633
  print(f"\U0001f310 Open http://localhost:{PORT} in your browser")
634
  if not OPENROUTER_API_KEY:
635
- print("\u26a0\ufe0f No OPENROUTER_API_KEY found. Add it to your .env file. Fallback generation will be used.")
636
- print(f"\U0001f4da API docs available at http://localhost:{PORT}/docs")
 
637
  app.launch(
638
  server_port=PORT,
639
  show_error=True,
640
  share=False
641
- )
 
1
  #!/usr/bin/env python3
2
  """
3
+ Overthinker v28 — Gradio.Server Backend with SQLite Session Isolation + HF Trace Upload
4
+
5
+ Changes from v27:
6
+ - Default model reverted to nvidia/nemotron-3-nano-30b-a3b
7
+ - Added /upload_trace endpoint that serializes full tree from SQLite and pushes to HF dataset
8
+ - Dataset repository configured via HF_DATASET_REPO env var
9
+ - HF token via HF_TOKEN env var
10
+ - All other features preserved: SQLite per-session, full path context, no fallbacks, no undo/redo
11
 
 
 
 
 
12
  Port: 7860
13
  """
14
 
15
  import os
16
  import json
 
17
  import uuid
18
+ import sqlite3
19
+ from pathlib import Path
20
+ from typing import Optional, Dict, List, Any
 
 
 
 
 
21
 
22
  from gradio import Server
23
  from fastapi import HTTPException
24
+ from starlette.responses import HTMLResponse, PlainTextResponse
 
 
 
 
25
 
26
  # ---------------------------------------------------------------------------
27
  # Application Setup
28
  # ---------------------------------------------------------------------------
29
  app = Server()
30
  PORT = 7860
31
+ DATA_DIR = Path("data")
32
+ DATA_DIR.mkdir(exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
35
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
36
+ DEFAULT_MODEL = "nvidia/nemotron-3-nano-30b-a3b"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ HF_TOKEN = os.getenv('HF_TOKEN', '')
39
+ HF_DATASET_REPO = os.getenv('HF_DATASET_REPO', '')
40
 
41
  # ---------------------------------------------------------------------------
42
+ # Database Helpers
43
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ def get_db_path(session_id: str) -> Path:
46
+ return DATA_DIR / f"session_{session_id}.db"
47
+
48
+ def init_session(session_id: str):
49
+ db_path = get_db_path(session_id)
50
+ if db_path.exists():
51
+ return
52
+ conn = sqlite3.connect(str(db_path))
53
+ conn.execute("""
54
+ CREATE TABLE nodes (
55
+ id TEXT PRIMARY KEY,
56
+ parent_id TEXT,
57
+ type TEXT NOT NULL,
58
+ label TEXT NOT NULL,
59
+ description TEXT DEFAULT '',
60
+ emoji TEXT DEFAULT '\U0001f539',
61
+ tips TEXT DEFAULT '[]',
62
+ order_index INTEGER DEFAULT 0,
63
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
64
+ )
65
+ """)
66
+ root_id = str(uuid.uuid4())
67
+ conn.execute(
68
+ "INSERT INTO nodes (id, parent_id, type, label, description, emoji) VALUES (?, ?, ?, ?, ?, ?)",
69
+ (root_id, None, "root", "What decision do you want to explore?", "", "\U0001f333")
70
+ )
71
+ conn.commit()
72
+ conn.close()
73
 
74
+ def get_node_db(session_id: str, node_id: str) -> Optional[Dict]:
75
+ db_path = get_db_path(session_id)
76
+ if not db_path.exists():
77
+ return None
78
+ conn = sqlite3.connect(str(db_path))
79
+ conn.row_factory = sqlite3.Row
80
+ row = conn.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone()
81
+ conn.close()
82
+ if row is None:
83
+ return None
84
+ result = dict(row)
85
  try:
86
+ result['tips'] = json.loads(result.get('tips', '[]'))
87
+ except:
88
+ result['tips'] = []
89
+ return result
90
+
91
+ def get_children_db(session_id: str, parent_id: str) -> List[Dict]:
92
+ db_path = get_db_path(session_id)
93
+ if not db_path.exists():
94
+ return []
95
+ conn = sqlite3.connect(str(db_path))
96
+ conn.row_factory = sqlite3.Row
97
+ rows = conn.execute(
98
+ "SELECT * FROM nodes WHERE parent_id=? ORDER BY order_index",
99
+ (parent_id,)
100
+ ).fetchall()
101
+ conn.close()
102
+ result = []
103
+ for row in rows:
104
+ d = dict(row)
105
+ try:
106
+ d['tips'] = json.loads(d.get('tips', '[]'))
107
+ except:
108
+ d['tips'] = []
109
+ result.append(d)
110
+ return result
111
+
112
+ def add_node_db(session_id: str, parent_id: str, node_type: str, label: str,
113
+ description: str = "", emoji: str = "\U0001f539",
114
+ tips: list = None, order_index: int = 0) -> Dict:
115
+ node_id = str(uuid.uuid4())
116
+ tips_json = json.dumps(tips or [])
117
+ db_path = get_db_path(session_id)
118
+ conn = sqlite3.connect(str(db_path))
119
+ conn.execute(
120
+ "INSERT INTO nodes (id, parent_id, type, label, description, emoji, tips, order_index) VALUES (?,?,?,?,?,?,?,?)",
121
+ (node_id, parent_id, node_type, label, description, emoji, tips_json, order_index)
122
+ )
123
+ conn.commit()
124
+ conn.close()
125
+ return {
126
+ "id": node_id,
127
+ "parent_id": parent_id,
128
+ "type": node_type,
129
+ "label": label,
130
+ "description": description,
131
+ "emoji": emoji,
132
+ "tips": tips or [],
133
+ "order_index": order_index
134
+ }
135
 
136
+ def update_root_db(session_id: str, label: str, description: str = ""):
137
+ db_path = get_db_path(session_id)
138
+ conn = sqlite3.connect(str(db_path))
139
+ conn.execute(
140
+ "UPDATE nodes SET label=?, description=? WHERE parent_id IS NULL",
141
+ (label, description)
142
+ )
143
+ conn.commit()
144
+ conn.close()
145
+
146
+ def get_path_db(session_id: str, node_id: str) -> List[Dict]:
147
+ path = []
148
+ current_id = node_id
149
+ while current_id:
150
+ node = get_node_db(session_id, current_id)
151
+ if node is None:
152
+ break
153
+ path.append(node)
154
+ current_id = node.get("parent_id")
155
+ path.reverse()
156
+ return path
157
+
158
+ def build_path_string(session_id: str, node_id: str) -> str:
159
+ nodes = get_path_db(session_id, node_id)
160
+ parts = []
161
+ for n in nodes:
162
+ t = n["type"]
163
+ label = n["label"]
164
+ if t == "root":
165
+ parts.append(f"[ROOT] {label}")
166
+ elif t == "input":
167
+ parts.append(f"[INPUT] {label}")
168
+ elif t == "outcome":
169
+ parts.append(f"[OUTCOME] {label}")
170
+ return " → ".join(parts)
171
+
172
+ def get_root_node(session_id: str) -> Optional[Dict]:
173
+ db_path = get_db_path(session_id)
174
+ if not db_path.exists():
175
+ return None
176
+ conn = sqlite3.connect(str(db_path))
177
+ conn.row_factory = sqlite3.Row
178
+ row = conn.execute("SELECT * FROM nodes WHERE parent_id IS NULL LIMIT 1").fetchone()
179
+ conn.close()
180
+ if row is None:
181
+ return None
182
+ result = dict(row)
183
+ try:
184
+ result['tips'] = json.loads(result.get('tips', '[]'))
185
+ except:
186
+ result['tips'] = []
187
+ return result
188
+
189
+ def get_all_node_ids(session_id: str) -> List[str]:
190
+ """Get IDs of all nodes in the tree (for full export)."""
191
+ db_path = get_db_path(session_id)
192
+ if not db_path.exists():
193
+ return []
194
+ conn = sqlite3.connect(str(db_path))
195
+ rows = conn.execute("SELECT id FROM nodes").fetchall()
196
+ conn.close()
197
+ return [r[0] for r in rows]
198
+
199
+ def build_tree_nested(session_id: str) -> Optional[Dict]:
200
+ """Build a nested tree structure from the SQLite DB."""
201
+ root = get_root_node(session_id)
202
+ if not root:
203
+ return None
204
+ def build_tree(node):
205
+ children = get_children_db(session_id, node['id'])
206
+ node_copy = dict(node)
207
+ if isinstance(node_copy.get('tips'), str):
208
+ try:
209
+ node_copy['tips'] = json.loads(node_copy['tips'])
210
+ except:
211
+ node_copy['tips'] = []
212
+ node_copy['children'] = [build_tree(c) for c in children]
213
+ return node_copy
214
+ return build_tree(root)
215
 
216
  # ---------------------------------------------------------------------------
217
+ # Prompt Builders (with path_context)
218
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
 
 
 
220
  def build_root_prompt(decision: str) -> str:
221
  return f'''You are an AI that helps people explore decisions by generating decision trees.
222
 
 
228
  {{
229
  "label": "A concise label for this decision tree (3-6 words)",
230
  "description": "A 1-2 sentence description of this decision context",
231
+ "emoji": "An emoji representing this decision",
232
  "tips": ["One actionable tip for approaching this decision"]
233
  }}'''
234
 
235
+ def build_options_prompt(decision_label: str, decision_desc: str, count: int, path_context: str, comment: str = "") -> str:
236
+ path_section = f'\nFull path from root to this node: "{path_context}"' if path_context else ''
237
+ comment_section = f'\nUser context: "{comment}"' if comment else ''
 
 
 
238
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
239
 
240
  Parent node: "{decision_label}"
241
+ Description: "{decision_desc}"{path_section}{comment_section}
242
+
243
+ Generate EXACTLY {count} child nodes that represent different OPTIONS or CHOICES the person could take.
244
 
245
+ IMPORTANT: Frame each child as an OPTION or CHOICE, not as an outcome.
246
 
247
+ Consider the full decision path above to ensure the options are contextually relevant.
 
 
248
 
249
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
250
  {{
251
  "children": [
252
  {{
253
  "id": "child_1",
254
+ "label": "Short option label (3-6 words)",
255
+ "description": "1-2 sentence description",
256
+ "emoji": "An emoji",
257
+ "tips": ["One practical tip"]
258
  }},
259
+ ...
260
  ]
261
  }}
262
 
263
  Ensure children have unique IDs like child_1, child_2, etc.'''
264
 
265
+ def build_outcomes_prompt(decision_label: str, decision_desc: str, count: int, path_context: str, comment: str = "") -> str:
266
+ path_section = f'\nFull path from root to this node: "{path_context}"' if path_context else ''
267
+ comment_section = f'\nUser context: "{comment}"' if comment else ''
 
 
 
 
268
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
269
 
270
  Parent node: "{decision_label}"
271
+ Description: "{decision_desc}"{path_section}{comment_section}
272
 
273
+ Generate EXACTLY {count} child nodes that represent a DIVERSE RANGE of possible OUTCOMES. Include a MIX of positive, neutral, and negative outcomes.
274
 
275
+ IMPORTANT: Frame each child as an OUTCOME or CONSEQUENCE, not as a choice someone makes.
 
 
276
 
277
+ Consider the full decision path above to ensure the outcomes are contextually relevant.
 
 
278
 
279
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
280
  {{
281
  "children": [
282
  {{
283
  "id": "child_1",
284
+ "label": "Short outcome label (3-6 words)",
285
+ "description": "1-2 sentence description",
286
+ "emoji": "An emoji",
287
+ "tips": ["One practical tip"]
288
  }},
289
+ ...
290
  ]
291
  }}
292
 
293
+ Ensure children have unique IDs. Make sure the first child is POSITIVE, the second is NEUTRAL, and the third is NEGATIVE.'''
294
 
295
+ # ---------------------------------------------------------------------------
296
+ # AI Call (using OpenRouter via requests)
297
+ # ---------------------------------------------------------------------------
298
+
299
+ def call_api(prompt: str, system_prompt: str = "You are a helpful assistant that generates decision trees.") -> Optional[str]:
300
+ if not OPENROUTER_API_KEY:
301
+ print("[OpenRouter Error] No API key configured")
302
+ return None
303
+ try:
304
+ headers = {
305
+ 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
306
+ 'Content-Type': 'application/json',
307
+ 'HTTP-Referer': 'http://localhost:7860',
308
+ 'X-Title': 'Overthinker'
309
+ }
310
+ data = {
311
+ 'model': DEFAULT_MODEL,
312
+ 'messages': [
313
+ {'role': 'system', 'content': system_prompt},
314
+ {'role': 'user', 'content': prompt}
315
+ ],
316
+ 'temperature': 0.8,
317
+ 'max_tokens': 2048
318
+ }
319
+ response = requests.post(
320
+ OPENROUTER_URL,
321
+ headers=headers,
322
+ json=data,
323
+ timeout=30
324
+ )
325
+ if response.status_code == 200:
326
+ result = response.json()
327
+ return result['choices'][0]['message']['content']
328
+ else:
329
+ print(f"[OpenRouter Error] {response.status_code}: {response.text}")
330
+ except Exception as e:
331
+ print(f"[OpenRouter Exception] {e}")
332
+ return None
333
 
334
  def parse_json_response(text: str) -> Optional[dict]:
335
  if not text:
336
  return None
337
  text = text.strip()
338
+ import re
339
  text = re.sub(r'```json\s*', '', text)
340
  text = re.sub(r'```\s*', '', text)
341
  text = text.strip()
 
351
  return None
352
 
353
  # ---------------------------------------------------------------------------
354
+ # Routes (All POST, no GET except for serving index)
355
  # ---------------------------------------------------------------------------
356
 
357
  @app.get("/")
 
362
  return HTMLResponse(content=f.read(), status_code=200)
363
  return HTMLResponse(content="<h1>Overthinker</h1><p>index.html not found</p>", status_code=404)
364
 
365
+ @app.post("/root")
366
+ async def create_root(request: dict):
367
+ session_id = request.get('session_id', str(uuid.uuid4()))
368
+ init_session(session_id)
369
+ root = get_root_node(session_id)
370
+ if root is None:
371
+ raise HTTPException(status_code=500, detail="Could not initialize session.")
372
+ return {"session_id": session_id, "node": root}
373
 
374
  @app.post("/create_tree")
375
  async def create_tree(request: dict):
376
+ session_id = request.get('session_id', str(uuid.uuid4()))
377
+ decision = request.get('decision', '')
378
+ if not decision:
379
+ raise HTTPException(status_code=400, detail="Decision text is required.")
380
+ init_session(session_id)
381
  prompt = build_root_prompt(decision)
382
  ai_response = call_api(prompt)
383
  parsed = parse_json_response(ai_response) if ai_response else None
384
+ if not parsed:
385
+ raise HTTPException(status_code=500, detail="Failed to generate root node. Please check your API key and try again.")
386
+ label = parsed.get('label', f'Overthinking: {decision[:40]}')
387
+ description = parsed.get('description', f'You are overthinking: {decision}')
388
+ emoji = parsed.get('emoji', '\U0001f333')
389
+ tips = parsed.get('tips', ['Start by exploring options.'])
390
+ update_root_db(session_id, label, description)
391
+ db_path = get_db_path(session_id)
392
+ conn = sqlite3.connect(str(db_path))
393
+ conn.execute("UPDATE nodes SET emoji=?, tips=? WHERE parent_id IS NULL", (emoji, json.dumps(tips)))
394
+ conn.commit()
395
+ conn.close()
396
+ root = get_root_node(session_id)
397
+ return {'session_id': session_id, 'node': root}
398
+
399
+ @app.post("/get_node")
400
+ async def get_node_endpoint(request: dict):
401
+ session_id = request.get('session_id')
402
+ node_id = request.get('node_id')
403
+ if not session_id or not node_id:
404
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
405
+ init_session(session_id)
406
+ node = get_node_db(session_id, node_id)
407
+ if node is None:
 
 
 
 
 
 
 
 
 
 
408
  raise HTTPException(status_code=404, detail="Node not found")
409
+ children = get_children_db(session_id, node_id)
410
+ path_context = build_path_string(session_id, node_id)
411
+ return {
412
+ 'node': node,
413
+ 'children': children,
414
+ 'path_context': path_context
415
+ }
416
 
417
+ @app.post("/get_children")
418
+ async def get_children(request: dict):
419
+ session_id = request.get('session_id')
420
+ node_id = request.get('node_id')
421
+ count = request.get('count', 3)
422
+ node_type = request.get('node_type', 'outcome')
423
+ comment = request.get('comment', '')
424
+ if not session_id or not node_id:
425
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
426
+ init_session(session_id)
427
+ parent = get_node_db(session_id, node_id)
428
+ if parent is None:
429
+ raise HTTPException(status_code=404, detail="Node not found")
430
+ path_context = build_path_string(session_id, node_id)
431
  next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
432
  next_type = next_type_map.get(node_type, 'outcome')
 
433
  parent_label = parent.get('label', 'Unknown')
434
  parent_desc = parent.get('description', '')
 
 
435
  if next_type == 'input':
436
+ prompt = build_options_prompt(parent_label, parent_desc, count, path_context, comment)
 
437
  else:
438
+ prompt = build_outcomes_prompt(parent_label, parent_desc, count, path_context, comment)
 
 
439
  ai_response = call_api(prompt)
440
  parsed = parse_json_response(ai_response) if ai_response else None
441
+ if not parsed or 'children' not in parsed or not isinstance(parsed['children'], list):
442
+ raise HTTPException(status_code=500, detail="Generation failed. Please check your API key and try again.")
443
+ children_data = parsed['children']
444
+ children = []
445
+ for i, child in enumerate(children_data):
446
+ label = child.get('label', 'Unknown')
447
+ description = child.get('description', '')
448
+ emoji = child.get('emoji', '\U0001f539')
449
+ tips = child.get('tips', [f'Consider this {next_type}.'])
450
+ existing = get_children_db(session_id, node_id)
451
+ existing_labels = [c['label'] for c in existing]
452
+ if label in existing_labels or label in [c['label'] for c in children]:
453
+ label = f"{label} ({i+1})"
454
+ child_node = add_node_db(session_id, node_id, next_type, label, description, emoji, tips, order_index=i)
455
+ child_node['type'] = next_type
456
+ children.append(child_node)
457
+ return {'children': children, 'next_type': next_type}
 
 
 
 
 
 
 
 
458
 
459
  @app.post("/add_options")
460
  async def add_options(request: dict):
461
+ session_id = request.get('session_id')
462
+ node_id = request.get('node_id')
463
  count = request.get('count', 3)
464
+ comment = request.get('comment', '')
465
+ if not session_id or not node_id:
466
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
467
+ init_session(session_id)
468
+ parent = get_node_db(session_id, node_id)
469
+ if parent is None:
 
470
  raise HTTPException(status_code=404, detail="Node not found")
471
+ path_context = build_path_string(session_id, node_id)
472
+ next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
473
+ next_type = next_type_map.get(parent.get('type', 'root'), 'outcome')
 
474
  parent_label = parent.get('label', 'Unknown')
475
  parent_desc = parent.get('description', '')
 
 
 
 
476
  if next_type == 'input':
477
+ prompt = build_options_prompt(parent_label, parent_desc, count, path_context, comment)
 
478
  else:
479
+ prompt = build_outcomes_prompt(parent_label, parent_desc, count, path_context, comment)
 
 
480
  ai_response = call_api(prompt)
481
  parsed = parse_json_response(ai_response) if ai_response else None
482
+ if not parsed or 'children' not in parsed or not isinstance(parsed['children'], list):
483
+ raise HTTPException(status_code=500, detail="Failed to add options. Please try again.")
484
+ children_data = parsed['children']
485
+ children = []
486
+ for i, child in enumerate(children_data):
487
+ label = child.get('label', 'Unknown')
488
+ description = child.get('description', '')
489
+ emoji = child.get('emoji', '\U0001f539')
490
+ tips = child.get('tips', [f'Additional {next_type}.'])
491
+ existing = get_children_db(session_id, node_id)
492
+ existing_labels = [c['label'] for c in existing]
493
+ if label in existing_labels or label in [c['label'] for c in children]:
494
+ label = f"{label} ({i+1})"
495
+ child_node = add_node_db(session_id, node_id, next_type, label, description, emoji, tips, order_index=i)
496
+ child_node['type'] = next_type
497
+ children.append(child_node)
498
+ return {'children': children, 'next_type': next_type}
499
+
500
+ @app.post("/upload_trace")
501
+ async def upload_trace(request: dict):
502
+ """Serialize the full tree from SQLite and push to HuggingFace dataset."""
503
+ session_id = request.get('session_id')
504
+ if not session_id:
505
+ raise HTTPException(status_code=400, detail="Missing session_id")
506
+
507
+ if not HF_TOKEN or not HF_DATASET_REPO:
508
+ raise HTTPException(status_code=500, detail="HF_TOKEN and HF_DATASET_REPO must be configured in environment.")
509
+
510
+ tree = build_tree_nested(session_id)
511
+ if tree is None:
512
+ raise HTTPException(status_code=404, detail="No tree found for this session.")
513
+
514
+ try:
515
+ from datasets import Dataset, concatenate_datasets, load_dataset
516
+ import pandas as pd
517
+
518
+ row = {
519
+ 'session_id': session_id,
520
+ 'tree_json': json.dumps(tree),
521
+ 'created_at': str(tree.get('created_at', ''))
522
+ }
523
+ df = pd.DataFrame([row])
524
+ new_dataset = Dataset.from_pandas(df)
525
+
526
+ try:
527
+ existing_dataset = load_dataset(HF_DATASET_REPO, split='train', token=HF_TOKEN)
528
+ combined = concatenate_datasets([existing_dataset, new_dataset])
529
+ except Exception:
530
+ combined = new_dataset
531
+
532
+ combined.push_to_hub(HF_DATASET_REPO, token=HF_TOKEN, private=False)
533
+
534
+ return {'status': 'success', 'message': 'Trace uploaded successfully!'}
535
+ except Exception as e:
536
+ print(f"[Upload Trace Error] {e}")
537
+ raise HTTPException(status_code=500, detail=f"Failed to upload trace: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  @app.post("/export_json")
540
  async def export_json(request: dict):
541
+ session_id = request.get('session_id')
542
+ if not session_id:
543
+ raise HTTPException(status_code=400, detail="Missing session_id")
544
+ root = get_root_node(session_id)
545
+ if not root:
546
+ raise HTTPException(status_code=404, detail="No tree found")
547
+ def build_tree(node):
548
+ children = get_children_db(session_id, node['id'])
549
+ node_copy = dict(node)
550
+ node_copy['children'] = [build_tree(c) for c in children]
551
+ return node_copy
552
+ full_tree = build_tree(root)
553
+ return full_tree
554
 
555
  @app.post("/export_path_json")
556
  async def export_path_json(request: dict):
557
+ session_id = request.get('session_id')
558
+ node_id = request.get('node_id')
559
+ if not session_id or not node_id:
560
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
561
+ path_nodes = get_path_db(session_id, node_id)
562
+ return {'path': path_nodes}
 
563
 
564
  @app.post("/export_path_md")
565
  async def export_path_md(request: dict):
566
+ session_id = request.get('session_id')
567
+ node_id = request.get('node_id')
568
+ if not session_id or not node_id:
569
+ raise HTTPException(status_code=400, detail="Missing session_id or node_id")
570
+ path = get_path_db(session_id, node_id)
571
+ md = '# \U0001f9e0 Overthinker Decision Path\n\n'
572
  for i, node in enumerate(path):
573
  indent = ' ' * i
574
  emoji = {'root': '\U0001f333', 'input': '\U0001f9e0', 'outcome': '\U0001f4ca'}.get(node.get('type', ''), '\U0001f4cc')
 
578
  if node.get('tips') and len(node['tips']) > 0:
579
  md += f'{indent} > \U0001f4a1 {node["tips"][0]}\n'
580
  md += '\n'
581
+ return PlainTextResponse(content=md, status_code=200)
582
 
583
  # ---------------------------------------------------------------------------
584
  # Launch
585
  # ---------------------------------------------------------------------------
586
  if __name__ == "__main__":
587
+ import requests # needed for sync calls inside async routes
588
+ print(f"\U0001f9e0 Overthinker v28 — SQLite Session Mode + HF Trace Upload on port {PORT}")
589
  print(f"\U0001f916 Model: {DEFAULT_MODEL}")
590
  print(f"\U0001f310 Open http://localhost:{PORT} in your browser")
591
  if not OPENROUTER_API_KEY:
592
+ print("\u26a0\ufe0f No OPENROUTER_API_KEY found. Add to .env or environment. Generation will fail.")
593
+ if not HF_TOKEN or not HF_DATASET_REPO:
594
+ print("\u26a0\ufe0f No HF_TOKEN or HF_DATASET_REPO set. Upload will fail.")
595
  app.launch(
596
  server_port=PORT,
597
  show_error=True,
598
  share=False
599
+ )