broadfield-dev commited on
Commit
7033a2e
Β·
verified Β·
1 Parent(s): c66d24e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -175
app.py CHANGED
@@ -1,16 +1,12 @@
1
  #!/usr/bin/env python3
2
  """
3
- Overthinker v24 β€” 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
9
  Port: 7860
10
-
11
- Toggle:
12
- - USE_HUGGINGFACE = True -> Uses HuggingFace Inference Client (HF_TOKEN from .env)
13
- - USE_HUGGINGFACE = False -> Uses OpenRouter/OpenAI fallback (original behavior)
14
  """
15
 
16
  import os
@@ -30,38 +26,20 @@ from gradio import Server
30
  from fastapi import HTTPException
31
  from starlette.responses import HTMLResponse
32
 
33
- # ────────────────────────────────────────────────
34
- # HuggingFace Inference Client (optional import)
35
- # ────────────────────────────────────────────────
36
- HF_AVAILABLE = False
37
- try:
38
- from huggingface_hub import InferenceClient
39
- HF_AVAILABLE = True
40
- except ImportError:
41
- InferenceClient = None
42
- print("[Warning] huggingface_hub not installed. HuggingFace mode disabled.")
43
-
44
  load_dotenv()
45
 
46
  OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
47
- OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')
48
- HF_TOKEN = os.getenv('HF_TOKEN', '')
49
-
50
- # ────────────────────────────────────────────────
51
- # TOGGLE: Set True to use HuggingFace Inference Client
52
- # ────────────────────────────────────────────────
53
- USE_HUGGINGFACE = False # <-- Set to False to use OpenRouter/OpenAI instead
54
 
 
 
 
55
  app = Server()
56
  PORT = 7860
57
- if USE_HUGGINGFACE:
58
- DEFAULT_MODEL = "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4"
59
- else:
60
- DEFAULT_MODEL = "nvidia/nemotron-3-nano-30b-a3b"
61
 
62
- # ────────────────────────────────────────────────
63
  # Node Manager
64
- # ────────────────────────────────────────────────
65
  class NodeManager:
66
  def __init__(self):
67
  self.trees: Dict[str, Dict[str, Any]] = {}
@@ -123,9 +101,9 @@ class NodeManager:
123
 
124
  node_manager = NodeManager()
125
 
126
- # ────────────────────────────────────────────────
127
  # History Manager
128
- # ────────────────────────────────────────────────
129
  class HistoryManager:
130
  def __init__(self):
131
  self.history: Dict[str, Dict] = {}
@@ -155,105 +133,49 @@ class HistoryManager:
155
 
156
  history_manager = HistoryManager()
157
 
158
- # ────────────────────────────────────────────────
159
- # LLM API Call β€” with HuggingFace Toggle
160
- # ────────────────────────────────────────────────
161
  def call_api(prompt: str, system_prompt: str = "You are a helpful assistant that generates decision trees.") -> Optional[str]:
162
-
163
- # ─── HuggingFace Inference Client Mode ───
164
- if USE_HUGGINGFACE:
165
- if not HF_TOKEN:
166
- print("[HF Error] No HF_TOKEN found in environment variables")
167
- return None
168
- if not HF_AVAILABLE:
169
- print("[HF Error] huggingface_hub not installed. Run: pip install huggingface-hub>=0.23.0")
170
- return None
171
-
172
- try:
173
- client = InferenceClient(token=HF_TOKEN)
174
-
175
- response = client.chat.completions.create(
176
- model=DEFAULT_MODEL,
177
- messages=[
178
- {"role": "system", "content": system_prompt},
179
- {"role": "user", "content": prompt}
180
- ],
181
- temperature=0.8,
182
- max_tokens=2048
183
- )
184
-
185
- return response.choices[0].message.content
186
-
187
- except Exception as e:
188
- print(f"[HF Exception] {e}")
189
- return None
190
 
191
- # ─── OpenRouter Mode (original fallback) ───
192
- if OPENROUTER_API_KEY:
193
- try:
194
- headers = {
195
- 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
196
- 'Content-Type': 'application/json',
197
- 'HTTP-Referer': 'http://localhost:7860',
198
- 'X-Title': 'Overthinker v20'
199
- }
200
- data = {
201
- 'model': DEFAULT_MODEL,
202
- 'messages': [
203
- {'role': 'system', 'content': system_prompt},
204
- {'role': 'user', 'content': prompt}
205
- ],
206
- 'temperature': 0.8,
207
- 'max_tokens': 2048
208
- }
209
- response = requests.post(
210
- 'https://openrouter.ai/api/v1/chat/completions',
211
- headers=headers,
212
- json=data,
213
- timeout=30
214
- )
215
- if response.status_code == 200:
216
- result = response.json()
217
- return result['choices'][0]['message']['content']
218
- else:
219
- print(f"[OpenRouter Error] {response.status_code}: {response.text}")
220
- except Exception as e:
221
- print(f"[OpenRouter Exception] {e}")
222
-
223
- if OPENAI_API_KEY:
224
- try:
225
- headers = {
226
- 'Authorization': f'Bearer {OPENAI_API_KEY}',
227
- 'Content-Type': 'application/json'
228
- }
229
- data = {
230
- 'model': 'gpt-3.5-turbo',
231
- 'messages': [
232
- {'role': 'system', 'content': system_prompt},
233
- {'role': 'user', 'content': prompt}
234
- ],
235
- 'temperature': 0.8,
236
- 'max_tokens': 2048
237
- }
238
- response = requests.post(
239
- 'https://api.openai.com/v1/chat/completions',
240
- headers=headers,
241
- json=data,
242
- timeout=30
243
- )
244
- if response.status_code == 200:
245
- result = response.json()
246
- return result['choices'][0]['message']['content']
247
- else:
248
- print(f"[OpenAI Error] {response.status_code}: {response.text}")
249
- except Exception as e:
250
- print(f"[OpenAI Exception] {e}")
251
 
252
  return None
253
 
254
- # ────────────────────────────────────────────────
255
- # Fallback generators (two distinct fallbacks)
256
- # ────────────────────────────────────────────────
257
  def _fallback_options(decision: str, context: str = "") -> dict:
258
  """Generate fallback children that read as options/choices."""
259
  import random
@@ -330,12 +252,12 @@ def _fallback_outcomes(decision: str, context: str = "") -> dict:
330
  labels = [pos, neu, neg]
331
  desc_map = {
332
  'positive': f'If this path unfolds favorably, {pos.lower()}. This represents a best-case scenario where your decision leads to growth and improvement.',
333
- 'neutral': f'On this path, {neu.lower()}. The outcome is neither clearly good nor bad \u2014 it requires careful monitoring.',
334
  'negative': f'In a challenging scenario, {neg.lower()}. This represents potential risks and difficulties that may arise.'
335
  }
336
  tip_map = {
337
  'positive': 'Nurture this positive outcome by staying engaged and proactive.',
338
- 'neutral': 'Monitor this neutral path closely \u2014 small changes can shift the outcome.',
339
  'negative': 'Prepare contingency plans to mitigate this risk if it materializes.'
340
  }
341
  children = []
@@ -353,9 +275,9 @@ def _fallback_outcomes(decision: str, context: str = "") -> dict:
353
  children.append(child)
354
  return {'children': children}
355
 
356
- # ────────────────────────────────────────────────
357
- # Prompt Builders \u2014 Dual prompts
358
- # ────────────────────────────────────────────────
359
  def build_root_prompt(decision: str) -> str:
360
  return f'''You are an AI that helps people explore decisions by generating decision trees.
361
 
@@ -380,14 +302,13 @@ def build_options_prompt(decision_label: str, decision_desc: str, count: int, co
380
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
381
 
382
  Parent node: "{decision_label}"
383
- Description: "{decision_desc}"
384
- {comment_section}
385
 
386
- Generate EXACTLY {count} child nodes that represent different OPTIONS or CHOICES the person could take. Each child should be a distinct, realistic possibility \u2014 an actionable decision they could make at this point.
387
 
388
  IMPORTANT: Frame each child as an OPTION or CHOICE, not as an outcome. For example:
389
  - GOOD: "Consult a financial advisor" (describes an action/choice)
390
- - BAD: "Financial situation improves" (describes an outcome \u2014 DO NOT use here)
391
 
392
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
393
  {{
@@ -409,14 +330,13 @@ Ensure children have unique IDs like child_1, child_2, etc.'''
409
  def build_outcomes_prompt(decision_label: str, decision_desc: str, count: int, comment: str = "") -> str:
410
  """
411
  Prompt for generating OUTCOMES/CONSEQUENCES (for outcome-type nodes).
412
- Now requests a diverse range: positive, neutral, and negative outcomes.
413
  """
414
  comment_section = f'\nUser context/comment: "{comment}"' if comment else ''
415
  return f'''You are an AI that helps explore decisions by generating decision tree branches.
416
 
417
  Parent node: "{decision_label}"
418
- Description: "{decision_desc}"
419
- {comment_section}
420
 
421
  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.
422
 
@@ -426,7 +346,7 @@ Generate EXACTLY {count} child nodes that represent a DIVERSE RANGE of possible
426
 
427
  IMPORTANT: Frame each child as an OUTCOME or CONSEQUENCE, not as a choice someone makes. For example:
428
  - GOOD: "Financial stability improves" (describes a result)
429
- - BAD: "Consider financial planning" (describes a choice \u2014 DO NOT do this)
430
 
431
  Return ONLY valid JSON with exactly this structure (no markdown, no backticks):
432
  {{
@@ -463,10 +383,9 @@ def parse_json_response(text: str) -> Optional[dict]:
463
  print(f"[Raw text] {text[:500]}")
464
  return None
465
 
466
-
467
- # ────────────────────────────────────────────────
468
  # Routes
469
- # ────────────────────────────────────────────────
470
 
471
  @app.get("/")
472
  async def index():
@@ -474,7 +393,7 @@ async def index():
474
  if os.path.exists(html_path):
475
  with open(html_path, "r", encoding="utf-8") as f:
476
  return HTMLResponse(content=f.read(), status_code=200)
477
- return HTMLResponse(content="<h1>Overthinker v24</h1><p>index.html not found</p>", status_code=404)
478
 
479
 
480
  @app.post("/create_tree")
@@ -522,27 +441,25 @@ async def get_children(node_id: str, count: int = 3, node_type: str = "outcome",
522
  parent = node_manager.get_node(tree_id, node_id)
523
  if not parent:
524
  raise HTTPException(status_code=404, detail="Parent node not found")
525
-
526
  # Determine what type of children to generate (input -> options; outcome -> outcomes)
527
  next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
528
  next_type = next_type_map.get(node_type, 'outcome')
529
-
530
  parent_label = parent.get('label', 'Unknown')
531
  parent_desc = parent.get('description', '')
532
-
533
  # Select the appropriate prompt and fallback based on next_type
534
  if next_type == 'input':
535
- # Generate options/choices
536
  prompt = build_options_prompt(parent_label, parent_desc, count, comment or "")
537
  fallback_func = _fallback_options
538
  else:
539
- # Generate outcomes
540
  prompt = build_outcomes_prompt(parent_label, parent_desc, count, comment or "")
541
  fallback_func = _fallback_outcomes
542
-
543
  ai_response = call_api(prompt)
544
  parsed = parse_json_response(ai_response) if ai_response else None
545
-
546
  if parsed and 'children' in parsed:
547
  children = parsed['children']
548
  for i, child in enumerate(children):
@@ -556,7 +473,7 @@ async def get_children(node_id: str, count: int = 3, node_type: str = "outcome",
556
  node_manager.add_children(tree_id, node_id, children)
557
  history_manager.push_state(tree_id, {'action': 'expand', 'node_id': node_id, 'children': children})
558
  return {'children': children}
559
-
560
  # Fallback
561
  fallback = fallback_func(parent_label, parent_desc)
562
  children = fallback.get('children', [])
@@ -583,23 +500,23 @@ async def add_options(request: dict):
583
  parent = node_manager.get_node(tree_id, node_id)
584
  if not parent:
585
  raise HTTPException(status_code=404, detail="Parent node not found")
586
-
587
  parent_label = parent.get('label', 'Unknown')
588
  parent_desc = parent.get('description', '')
589
  node_type = parent.get('type', 'root')
590
  next_type_map = {'root': 'input', 'input': 'outcome', 'outcome': 'input'}
591
  next_type = next_type_map.get(node_type, 'outcome')
592
-
593
  if next_type == 'input':
594
  prompt = build_options_prompt(parent_label, parent_desc, count)
595
  fallback_func = _fallback_options
596
  else:
597
  prompt = build_outcomes_prompt(parent_label, parent_desc, count)
598
  fallback_func = _fallback_outcomes
599
-
600
  ai_response = call_api(prompt)
601
  parsed = parse_json_response(ai_response) if ai_response else None
602
-
603
  if parsed and 'children' in parsed:
604
  children = parsed['children']
605
  for i, child in enumerate(children):
@@ -613,7 +530,7 @@ async def add_options(request: dict):
613
  node_manager.add_children(tree_id, node_id, children)
614
  history_manager.push_state(tree_id, {'action': 'add', 'node_id': node_id, 'children': children})
615
  return {'children': children}
616
-
617
  fallback = fallback_func(parent_label, parent_desc)
618
  children = fallback.get('children', [])
619
  for i, child in enumerate(children):
@@ -707,31 +624,18 @@ async def export_path_md(request: dict):
707
  md += '\n'
708
  return md
709
 
710
-
711
- # ────────────────────────────────────────────────
712
  # Launch
713
- # ────────────────────────────────────────────────
714
  if __name__ == "__main__":
715
- mode_str = "HuggingFace Inference" if USE_HUGGINGFACE else "OpenRouter/OpenAI"
716
- print(f"\U0001f9e0 Overthinker v24 \u2014 Starting on port {PORT}")
717
  print(f"\U0001f916 Model: {DEFAULT_MODEL}")
718
- print(f"\U0001f500 Inference Mode: {mode_str}")
719
  print(f"\U0001f310 Open http://localhost:{PORT} in your browser")
720
-
721
- if USE_HUGGINGFACE:
722
- if not HF_TOKEN:
723
- print("\u26a0\ufe0f No HF_TOKEN found. Set it in your .env file.")
724
- elif not HF_AVAILABLE:
725
- print("\u26a0\ufe0f huggingface_hub not installed. Run: pip install huggingface-hub>=0.23.0")
726
- else:
727
- print(f"\u2705 HuggingFace Inference Client ready (token: {HF_TOKEN[:6]}...{HF_TOKEN[-4:]})")
728
- else:
729
- if not OPENROUTER_API_KEY and not OPENAI_API_KEY:
730
- print("\u26a0\ufe0f No API key found. Using local fallback generation (limited).")
731
-
732
  print(f"\U0001f4da API docs available at http://localhost:{PORT}/docs")
733
  app.launch(
734
  server_port=PORT,
735
  show_error=True,
736
  share=False
737
- )
 
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
 
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]] = {}
 
101
 
102
  node_manager = NodeManager()
103
 
104
+ # ---------------------------------------------------------------------------
105
  # History Manager
106
+ # ---------------------------------------------------------------------------
107
  class HistoryManager:
108
  def __init__(self):
109
  self.history: Dict[str, Dict] = {}
 
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
 
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 = []
 
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
 
 
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
  {{
 
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
 
 
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
  {{
 
383
  print(f"[Raw text] {text[:500]}")
384
  return None
385
 
386
+ # ---------------------------------------------------------------------------
 
387
  # Routes
388
+ # ---------------------------------------------------------------------------
389
 
390
  @app.get("/")
391
  async def index():
 
393
  if os.path.exists(html_path):
394
  with open(html_path, "r", encoding="utf-8") as f:
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")
 
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):
 
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', [])
 
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):
 
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):
 
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
+ )