JatinAutonomousLabs commited on
Commit
936fcc3
·
verified ·
1 Parent(s): 16ed480

Update app_gradio.py

Browse files
Files changed (1) hide show
  1. app_gradio.py +99 -21
app_gradio.py CHANGED
@@ -1,4 +1,8 @@
1
- # app_gradio.py - Enhanced with streamlined UI
 
 
 
 
2
 
3
  import patch_gradio_jsonschema # MUST be first
4
 
@@ -10,6 +14,9 @@ import shutil
10
  import uuid
11
  from datetime import datetime
12
  from dotenv import load_dotenv
 
 
 
13
 
14
  from memory_manager import MemoryManager, memory_manager
15
  from graph import triage_app, planner_app, main_app
@@ -20,6 +27,7 @@ load_dotenv()
20
  log = get_logger(__name__)
21
 
22
  OUT_DIR = os.environ.get("OUT_DIR", "/tmp")
 
23
 
24
  # Create necessary directories
25
  os.makedirs(OUT_DIR, exist_ok=True)
@@ -27,6 +35,38 @@ os.makedirs("outputs", exist_ok=True)
27
  os.makedirs("uploads", exist_ok=True)
28
  os.makedirs("conversations", exist_ok=True)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # --- State Management ---
31
  def get_default_state():
32
  """Initialize clean session state."""
@@ -40,7 +80,9 @@ def get_default_state():
40
  "experimentResults": {},
41
  "approved": False,
42
  "execution_path": [],
43
- "chatHistory": []
 
 
44
  }
45
 
46
  def reset_all_artifacts_and_context():
@@ -82,10 +124,16 @@ def save_conversation(history, state):
82
  filename = f"conv_{timestamp}.json"
83
  filepath = os.path.join("conversations", filename)
84
 
 
 
 
 
 
 
85
  with open(filepath, 'w', encoding='utf-8') as f:
86
- json.dump(history, f, indent=2)
87
 
88
- state_val = state or {}
89
  state_val["current_conversation_file"] = filepath
90
  log.info(f"Conversation saved: {filepath}")
91
 
@@ -96,13 +144,15 @@ def load_conversation(filepath, state):
96
  return [], state
97
 
98
  with open(filepath, 'r', encoding='utf-8') as f:
99
- history = json.load(f)
 
 
 
100
 
101
- state_val = state or {}
102
- state_val["current_conversation_file"] = filepath
103
  log.info(f"Conversation loaded: {filepath}")
104
 
105
- return history, state_val
 
106
 
107
  def get_saved_conversations():
108
  if not os.path.exists("conversations"):
@@ -202,8 +252,10 @@ def collect_artifact_paths_from_result(exp_results):
202
  log.warning(f"Path normalization error: {e}")
203
  continue
204
 
 
205
  return list(dict.fromkeys(paths))
206
 
 
207
  # --- Core Logic Functions ---
208
  def start_estimation(message, history, state):
209
  log.info(f"Starting estimation: '{message}'")
@@ -313,38 +365,56 @@ def execute_main_task(history, state, budget):
313
  "chatHistory": state.get("chatHistory", []),
314
  "max_loops": max_loops_calibrated,
315
  "status_update": "Task approved. Starting...",
316
- "rework_cycles": state.get("rework_cycles", 0) or 0,
317
  "pmPlan": state.get("pmPlan", {}) or {},
318
  "experimentResults": state.get("experimentResults", {}) or {},
319
  "approved": False,
320
- "execution_path": state.get("execution_path", []) or [],
 
 
321
  }
322
 
323
  aggregated_state = initial_state.copy()
 
324
 
325
  try:
326
- for step in main_app.stream(initial_state):
 
 
327
  if not isinstance(step, dict) or not step:
328
  continue
329
 
330
  node_name = list(step.keys())[0]
331
  node_output = step.get(node_name)
332
 
 
333
  if not isinstance(node_output, dict):
334
  if node_output is not None:
335
  log.warning(f"Node '{node_name}' returned non-dict: {node_output}")
336
  aggregated_state["status_update"] = str(node_output)
337
  continue
338
 
339
- # Merge output
340
  for key, value in node_output.items():
341
  if key == 'execution_path':
342
- if isinstance(value, list):
343
- aggregated_state[key] = aggregated_state.get(key, []) + value
 
 
 
344
  else:
345
- aggregated_state[key] = aggregated_state.get(key, []) + [value]
 
346
  else:
347
  aggregated_state[key] = value
 
 
 
 
 
 
 
 
 
348
 
349
  status = aggregated_state.get("status_update", f"Running: {node_name}...")
350
 
@@ -356,12 +426,18 @@ def execute_main_task(history, state, budget):
356
  aggregated_state,
357
  gr.update(visible=False),
358
  gr.update(interactive=False),
359
- f"🔄 {status}",
360
  [],
361
  update_artifact_list()
362
  )
363
-
364
- # Final processing
 
 
 
 
 
 
365
  final_response = aggregated_state.get("draftResponse", "No response generated.")
366
  exp_results = aggregated_state.get("experimentResults", {})
367
 
@@ -394,21 +470,22 @@ def execute_main_task(history, state, budget):
394
  artifact_md = "### Generated Artifacts:\n" + "\n".join(artifact_lines)
395
 
396
  history[-1] = ["", final_response]
397
- final_state_json = json.dumps(aggregated_state, indent=2)
 
398
 
399
  yield (
400
  history,
401
  final_state_json,
402
  gr.update(visible=False),
403
  gr.update(interactive=True),
404
- "✅ Task complete",
405
  exported_paths,
406
  artifact_md
407
  )
408
 
409
  except Exception as e:
410
  log.exception("Execution error")
411
- final_state_json = json.dumps(aggregated_state, indent=2)
412
  history[-1] = ["", f"❌ Execution failed: {e}"]
413
 
414
  yield (
@@ -421,6 +498,7 @@ def execute_main_task(history, state, budget):
421
  "No artifacts generated."
422
  )
423
 
 
424
  def cancel_task(history, state):
425
  history.append(["", "❌ Task cancelled."])
426
  return (
 
1
+ #### `app_gradio.py`
2
+ This file is significantly updated to manage real-time cost, prevent runaway loops, and handle state more robustly. A new helper class `TokenCostCalculator` is introduced for this purpose.
3
+
4
+ ```python
5
+ # app_gradio.py - Enhanced with real-time cost tracking and safety features
6
 
7
  import patch_gradio_jsonschema # MUST be first
8
 
 
14
  import uuid
15
  from datetime import datetime
16
  from dotenv import load_dotenv
17
+ from typing import Any, Dict, List
18
+ from langchain_core.callbacks import BaseCallbackHandler
19
+ from langchain_core.outputs import LLMResult
20
 
21
  from memory_manager import MemoryManager, memory_manager
22
  from graph import triage_app, planner_app, main_app
 
27
  log = get_logger(__name__)
28
 
29
  OUT_DIR = os.environ.get("OUT_DIR", "/tmp")
30
+ MAX_EXECUTION_PATH_LENGTH = 50 # Safety break for runaway loops
31
 
32
  # Create necessary directories
33
  os.makedirs(OUT_DIR, exist_ok=True)
 
35
  os.makedirs("uploads", exist_ok=True)
36
  os.makedirs("conversations", exist_ok=True)
37
 
38
+ # --- Real-time Cost Tracking ---
39
+ class TokenCostCalculator(BaseCallbackHandler):
40
+ """Callback handler to calculate the cost of LLM calls."""
41
+ def __init__(self):
42
+ super().__init__()
43
+ self.total_cost = 0.0
44
+ self.model_costs = {
45
+ "gpt-4o": {"input": 0.005 / 1000, "output": 0.015 / 1000},
46
+ "default": {"input": 0.001 / 1000, "output": 0.002 / 1000} # Fallback
47
+ }
48
+
49
+ def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
50
+ try:
51
+ model_name = response.llm_output.get("model_name", "default")
52
+ token_usage = response.llm_output.get("token_usage", {})
53
+ prompt_tokens = token_usage.get("prompt_tokens", 0)
54
+ completion_tokens = token_usage.get("completion_tokens", 0)
55
+
56
+ costs = self.model_costs.get(model_name, self.model_costs["default"])
57
+
58
+ cost = (prompt_tokens * costs["input"]) + (completion_tokens * costs["output"])
59
+ self.total_cost += cost
60
+ log.info(f"LLM call cost: ${cost:.6f} | Total cost: ${self.total_cost:.4f}")
61
+ except Exception as e:
62
+ log.warning(f"Could not calculate token cost: {e}")
63
+
64
+ def get_total_cost(self) -> float:
65
+ return self.total_cost
66
+
67
+ def reset(self) -> None:
68
+ self.total_cost = 0.0
69
+
70
  # --- State Management ---
71
  def get_default_state():
72
  """Initialize clean session state."""
 
80
  "experimentResults": {},
81
  "approved": False,
82
  "execution_path": [],
83
+ "chatHistory": [],
84
+ "current_cost": 0.0,
85
+ "budget_exceeded": False
86
  }
87
 
88
  def reset_all_artifacts_and_context():
 
124
  filename = f"conv_{timestamp}.json"
125
  filepath = os.path.join("conversations", filename)
126
 
127
+ # Ensure state is a dict before saving
128
+ conversation_data = {
129
+ "history": history,
130
+ "state": state if isinstance(state, dict) else {}
131
+ }
132
+
133
  with open(filepath, 'w', encoding='utf-8') as f:
134
+ json.dump(conversation_data, f, indent=2)
135
 
136
+ state_val = state if isinstance(state, dict) else {}
137
  state_val["current_conversation_file"] = filepath
138
  log.info(f"Conversation saved: {filepath}")
139
 
 
144
  return [], state
145
 
146
  with open(filepath, 'r', encoding='utf-8') as f:
147
+ conversation_data = json.load(f)
148
+
149
+ history = conversation_data.get("history", [])
150
+ loaded_state = conversation_data.get("state", get_default_state())
151
 
 
 
152
  log.info(f"Conversation loaded: {filepath}")
153
 
154
+ return history, loaded_state
155
+
156
 
157
  def get_saved_conversations():
158
  if not os.path.exists("conversations"):
 
252
  log.warning(f"Path normalization error: {e}")
253
  continue
254
 
255
+ # Return a unique list of paths
256
  return list(dict.fromkeys(paths))
257
 
258
+
259
  # --- Core Logic Functions ---
260
  def start_estimation(message, history, state):
261
  log.info(f"Starting estimation: '{message}'")
 
365
  "chatHistory": state.get("chatHistory", []),
366
  "max_loops": max_loops_calibrated,
367
  "status_update": "Task approved. Starting...",
368
+ "rework_cycles": 0,
369
  "pmPlan": state.get("pmPlan", {}) or {},
370
  "experimentResults": state.get("experimentResults", {}) or {},
371
  "approved": False,
372
+ "execution_path": [],
373
+ "current_cost": 0.0,
374
+ "budget_exceeded": False
375
  }
376
 
377
  aggregated_state = initial_state.copy()
378
+ cost_tracker = TokenCostCalculator()
379
 
380
  try:
381
+ config = {"callbacks": [cost_tracker]}
382
+
383
+ for step in main_app.stream(initial_state, config=config):
384
  if not isinstance(step, dict) or not step:
385
  continue
386
 
387
  node_name = list(step.keys())[0]
388
  node_output = step.get(node_name)
389
 
390
+ # --- Robust State Aggregation ---
391
  if not isinstance(node_output, dict):
392
  if node_output is not None:
393
  log.warning(f"Node '{node_name}' returned non-dict: {node_output}")
394
  aggregated_state["status_update"] = str(node_output)
395
  continue
396
 
 
397
  for key, value in node_output.items():
398
  if key == 'execution_path':
399
+ current_path = aggregated_state.get(key, [])
400
+ if isinstance(current_path, list):
401
+ # Ensure we are always extending a list
402
+ new_items = value if isinstance(value, list) else [value]
403
+ aggregated_state[key] = current_path + new_items
404
  else:
405
+ # Recover from corrupted state
406
+ aggregated_state[key] = list(current_path) + (value if isinstance(value, list) else [value])
407
  else:
408
  aggregated_state[key] = value
409
+
410
+ # --- Real-time Budget Check ---
411
+ current_cost = cost_tracker.get_total_cost()
412
+ aggregated_state["current_cost"] = current_cost
413
+ if current_cost > float(budget):
414
+ log.warning(f"Budget exceeded! Current cost ${current_cost:.4f} > Budget ${float(budget):.4f}")
415
+ aggregated_state["budget_exceeded"] = True
416
+ aggregated_state["status_update"] = "Budget exceeded, finishing up..."
417
+ # No need to break here, the graph's `should_continue` will handle it
418
 
419
  status = aggregated_state.get("status_update", f"Running: {node_name}...")
420
 
 
426
  aggregated_state,
427
  gr.update(visible=False),
428
  gr.update(interactive=False),
429
+ f"🔄 {status} | Cost: ${current_cost:.3f}",
430
  [],
431
  update_artifact_list()
432
  )
433
+
434
+ # --- Safety Break for Runaway Loop ---
435
+ if len(aggregated_state.get("execution_path", [])) > MAX_EXECUTION_PATH_LENGTH:
436
+ log.error("Execution path limit reached. Terminating due to suspected runaway loop.")
437
+ aggregated_state["status_update"] = "ERROR: Runaway loop detected."
438
+ break
439
+
440
+ # Final processing after stream finishes or breaks
441
  final_response = aggregated_state.get("draftResponse", "No response generated.")
442
  exp_results = aggregated_state.get("experimentResults", {})
443
 
 
470
  artifact_md = "### Generated Artifacts:\n" + "\n".join(artifact_lines)
471
 
472
  history[-1] = ["", final_response]
473
+ final_state_json = json.dumps(aggregated_state, indent=2, default=str) # Use default=str for safety
474
+ final_cost = cost_tracker.get_total_cost()
475
 
476
  yield (
477
  history,
478
  final_state_json,
479
  gr.update(visible=False),
480
  gr.update(interactive=True),
481
+ f"✅ Task complete | Final Cost: ${final_cost:.3f}",
482
  exported_paths,
483
  artifact_md
484
  )
485
 
486
  except Exception as e:
487
  log.exception("Execution error")
488
+ final_state_json = json.dumps(aggregated_state, indent=2, default=str)
489
  history[-1] = ["", f"❌ Execution failed: {e}"]
490
 
491
  yield (
 
498
  "No artifacts generated."
499
  )
500
 
501
+
502
  def cancel_task(history, state):
503
  history.append(["", "❌ Task cancelled."])
504
  return (