JatinAutonomousLabs commited on
Commit
6892189
·
verified ·
1 Parent(s): f51b81e

Update graph.py

Browse files
Files changed (1) hide show
  1. graph.py +201 -172
graph.py CHANGED
@@ -1,4 +1,4 @@
1
- # graph.py (patched, OUT_DIR-enabled)
2
  import json
3
  import re
4
  import math
@@ -22,17 +22,14 @@ from docx import Document
22
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
23
  from reportlab.lib.styles import getSampleStyleSheet
24
 
25
- # --- Configurable output directory (change via ENV if needed) ---
26
  OUT_DIR = os.environ.get("OUT_DIR", "/tmp")
27
- # Ensure output directory exists
28
  os.makedirs(OUT_DIR, exist_ok=True)
29
- # Also ensure a subdir for exported outputs (keeps things organized)
30
  EXPORTS_DIR = os.path.join(OUT_DIR, "exports")
31
  os.makedirs(EXPORTS_DIR, exist_ok=True)
32
 
33
  # --- Helpers ---
34
  def ensure_list(state, key):
35
- """Return a list from state[key], default [] if missing/None/not-list."""
36
  v = state.get(key) if state else None
37
  if v is None:
38
  return []
@@ -43,7 +40,6 @@ def ensure_list(state, key):
43
  return [v]
44
 
45
  def ensure_int(state, key, default=0):
46
- """Return an int from state[key], default if missing/invalid."""
47
  try:
48
  v = state.get(key) if state else None
49
  if v is None:
@@ -53,7 +49,6 @@ def ensure_int(state, key, default=0):
53
  return default
54
 
55
  def sanitize_path(path: str) -> str:
56
- """Sanitize/normalize output path for return to UI."""
57
  return os.path.abspath(path)
58
 
59
  # --- Setup & constants ---
@@ -76,7 +71,6 @@ class AgentState(TypedDict):
76
  draftResponse: str
77
  qaFeedback: Optional[str]
78
  approved: bool
79
- # Annotate execution_path so Langgraph will treat it as an accumulating field
80
  execution_path: Annotated[List[str], operator.add]
81
  rework_cycles: int
82
  max_loops: int
@@ -86,7 +80,6 @@ class AgentState(TypedDict):
86
  llm = ChatOpenAI(model="gpt-4o", temperature=0.1, max_retries=3, request_timeout=60)
87
 
88
  def parse_json_from_llm(llm_output: str) -> Optional[dict]:
89
- """Robustly try to extract JSON object from LLM text."""
90
  try:
91
  if not llm_output:
92
  return None
@@ -108,7 +101,6 @@ def parse_json_from_llm(llm_output: str) -> Optional[dict]:
108
  KNOWN_ARTIFACT_TYPES = {"notebook","excel","word","pdf","image","repo","script"}
109
 
110
  def detect_requested_output_types(text: str) -> Dict:
111
- """Heuristic detect requested artifact type from text."""
112
  if not text:
113
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
114
  t = text.lower()
@@ -129,15 +121,12 @@ def detect_requested_output_types(text: str) -> Dict:
129
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
130
 
131
  def normalize_experiment_type(exp_type: Optional[str], goal_text: str) -> str:
132
- """Map arbitrary LLM returned experiment_type into known set or infer from goal_text."""
133
  if not exp_type:
134
  detection = detect_requested_output_types(goal_text or "")
135
- return detection.get("artifact_type") or "docx"
136
  s = exp_type.strip().lower()
137
- # direct mapping heuristics
138
  if s in KNOWN_ARTIFACT_TYPES:
139
  return s
140
- # common synonyms
141
  if "notebook" in s or "ipynb" in s:
142
  return "notebook"
143
  if "excel" in s or "xlsx" in s or "spreadsheet" in s:
@@ -150,11 +139,10 @@ def normalize_experiment_type(exp_type: Optional[str], goal_text: str) -> str:
150
  return "repo"
151
  if "script" in s or "python" in s or ".py" in s:
152
  return "script"
153
- # fallback to detection from goal
154
  detection = detect_requested_output_types(goal_text or "")
155
- return detection.get("artifact_type") or "docx"
156
 
157
- # --- Notebook & artifact builders (use OUT_DIR when out_dir is None) ---
158
  def write_notebook_from_text(llm_text: str, out_dir: Optional[str]=None) -> str:
159
  out_dir = out_dir or OUT_DIR
160
  os.makedirs(out_dir, exist_ok=True)
@@ -280,7 +268,7 @@ def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out
280
 
281
  # --- Node functions ---
282
  def run_triage_agent(state: AgentState):
283
- log.info("--- triage ---")
284
  prompt = f"Analyze the user input. Is it a simple conversational greeting or a task? Respond with 'greeting' or 'task'.\n\nUser Input: \"{state.get('userInput','')}\""
285
  response = llm.invoke(prompt)
286
  content = getattr(response, "content", "") or ""
@@ -292,7 +280,7 @@ def run_triage_agent(state: AgentState):
292
  return {"execution_path": ["Triage Agent"], "status_update": "Request requires a plan. Proceeding..."}
293
 
294
  def run_planner_agent(state: AgentState):
295
- log.info("--- ✈️ Running Planner Agent ---")
296
  path = ensure_list(state, 'execution_path') + ["Planner Agent"]
297
  prompt = (
298
  f"Analyze the user's request. Provide a high-level plan and estimate the number of LLM calls for one loop. "
@@ -317,7 +305,7 @@ def run_planner_agent(state: AgentState):
317
  return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan and cost estimate created. Awaiting approval."}
318
 
319
  def run_memory_retrieval(state: AgentState):
320
- log.info("--- 🧠 Accessing Long-Term Memory ---")
321
  path = ensure_list(state, 'execution_path') + ["Memory Retriever"]
322
  relevant_mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
323
  if relevant_mems:
@@ -329,7 +317,7 @@ def run_memory_retrieval(state: AgentState):
329
  return {"retrievedMemory": context, "execution_path": path, "status_update": "Searching for relevant past information..."}
330
 
331
  def run_intent_agent(state: AgentState):
332
- log.info("--- 🎯 Running Intent Agent ---")
333
  path = ensure_list(state, 'execution_path') + ["Intent Agent"]
334
  prompt = (f"Refine the user's request into a clear, actionable 'core objective prompt'.\n\nRelevant Memory:\n{state.get('retrievedMemory')}\n\nUser Request: \"{state.get('userInput','')}\"\n\nCore Objective:")
335
  response = llm.invoke(prompt)
@@ -341,34 +329,97 @@ def run_intent_agent(state: AgentState):
341
  return {"coreObjectivePrompt": core_obj, **extras, "execution_path": path, "status_update": "Clarifying the main objective..."}
342
 
343
  def run_pm_agent(state: AgentState):
344
- log.info("--- 👷 Running PM Agent ---")
345
  current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
346
  max_loops_val = ensure_int(state, 'max_loops', 0)
347
  log.info(f"Starting work cycle {current_cycles}/{max_loops_val + 1}")
348
  path = ensure_list(state, 'execution_path') + ["PM Agent"]
349
- feedback = f"QA Feedback (must be addressed): {state.get('qaFeedback')}" if state.get('qaFeedback') else ""
350
- prompt = (
351
- f"Decompose the core objective into a plan. Determine if code execution or artifact generation is needed and define the goal.\n\n"
352
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\n{feedback}\n\n"
353
- f"Respond in JSON with keys: 'plan_steps' (list), 'experiment_needed' (bool), 'experiment_type' (optional string), and 'experiment_goal' (str if needed)."
354
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  response = llm.invoke(prompt)
356
  plan = parse_json_from_llm(getattr(response, "content", "") or "")
 
357
  if not plan:
358
- log.warning("PM Agent did not produce JSON applying heuristic fallback.")
359
- plan = {"plan_steps": ["Analyze files", "Create notebook if requested", "Synthesize answers"], "experiment_needed": False}
360
- # normalize experiment type
 
 
 
 
 
 
 
 
 
 
 
361
  exp_type_raw = plan.get('experiment_type') or ""
362
  plan_goal = plan.get('experiment_goal') or state.get('userInput','') or state.get('coreObjectivePrompt','')
363
  normalized = normalize_experiment_type(exp_type_raw, plan_goal)
364
  plan['experiment_type'] = normalized
 
365
  if plan.get('experiment_needed') and not plan.get('experiment_goal'):
366
  plan['experiment_goal'] = plan_goal
367
- log.info(f"Generated Plan: Experiment Needed = {plan.get('experiment_needed', False)}, Type = {plan.get('experiment_type')}")
368
- return {"pmPlan": plan, "execution_path": path, "rework_cycles": current_cycles, "status_update": "Breaking down the objective into a detailed plan..."}
 
 
 
 
 
 
 
369
 
370
  def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
371
- # prefer specific language fences, fallback to generic fenced blocks
372
  if lang_hint and "python" in (lang_hint or "").lower():
373
  blocks = re.findall(r"```python\s*(.*?)\s*```", text, re.DOTALL)
374
  if blocks:
@@ -377,53 +428,140 @@ def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
377
  return blocks
378
 
379
  def run_experimenter_agent(state: AgentState):
380
- log.info("--- 🔬 Running Experimenter Agent ---")
381
  path = ensure_list(state, 'execution_path') + ["Experimenter Agent"]
382
  pm = state.get('pmPlan', {}) or {}
 
383
  if not pm.get('experiment_needed'):
384
- return {"experimentCode": None, "experimentResults": None, "execution_path": path, "status_update": "Proceeding without a code experiment."}
 
 
 
 
 
 
385
  exp_type = normalize_experiment_type(pm.get('experiment_type'), pm.get('experiment_goal',''))
386
  goal = pm.get('experiment_goal', 'No goal specified.')
387
- response = llm.invoke(
388
- f"Produce content for artifact type '{exp_type}' to achieve: {goal}\n"
389
- "Return runnable code in fenced code blocks where appropriate, and explanatory text otherwise."
390
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  llm_text = getattr(response, "content", "") or ""
392
  out_dir = OUT_DIR
393
- results = {"success": False, "paths": {}, "stderr": "", "stdout": ""}
 
394
  try:
395
  if exp_type == 'notebook':
396
  nb_path = write_notebook_from_text(llm_text, out_dir=out_dir)
397
  results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
398
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Notebook generated at {nb_path}"}
 
 
 
 
 
 
399
  elif exp_type == 'excel':
400
  excel_path = write_excel_from_tables(llm_text, out_dir=out_dir)
401
  results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
402
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Excel generated at {excel_path}"}
 
 
 
 
 
 
403
  elif exp_type == 'word':
404
  docx_path = write_docx_from_text(llm_text, out_dir=out_dir)
405
  results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
406
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"DOCX generated at {docx_path}"}
 
 
 
 
 
 
407
  elif exp_type == 'pdf':
408
  pdf_path = write_pdf_from_text(llm_text, out_dir=out_dir)
409
  results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
410
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"PDF generated at {pdf_path}"}
 
 
 
 
 
 
411
  elif exp_type == 'script':
412
  lang_hint = pm.get('experiment_language') or ("python" if ".py" in goal.lower() else None)
413
  code_blocks = _extract_code_blocks(llm_text, lang_hint)
 
414
  if not code_blocks:
415
  code_text = llm_text
416
  else:
417
  code_text = "\n\n# === BLOCK ===\n\n".join(code_blocks)
 
418
  script_path = write_script(code_text, language_hint=lang_hint, out_dir=out_dir)
419
  exec_results = {}
 
420
  if script_path.endswith(".py"):
421
  try:
422
- exec_results = execute_python_code(open(script_path,"r",encoding="utf-8").read())
423
  except Exception as e:
424
  exec_results = {"stdout":"","stderr":str(e),"success":False}
425
- results.update({"success": True, "paths": {"script": sanitize_path(script_path)}, "stdout": exec_results.get("stdout",""), "stderr": exec_results.get("stderr","")})
426
- return {"experimentCode": code_text, "experimentResults": results, "execution_path": path, "status_update": f"Script generated at {script_path}"}
 
 
 
 
 
 
 
 
 
 
 
 
427
  elif exp_type == 'repo':
428
  repo_files = {}
429
  readme = (llm_text[:1000] + "\n\n") if llm_text else "Generated repo"
@@ -434,131 +572,22 @@ def run_experimenter_agent(state: AgentState):
434
  repo_files["requirements.txt"] = reqs
435
  zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=out_dir)
436
  results.update({"success": True, "paths": {"repo_zip": sanitize_path(zip_path)}})
437
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Repository ZIP created at {zip_path}"}
 
 
 
 
 
 
438
  else:
439
- # safe fallback: write docx
440
  fallback = write_docx_from_text(llm_text, out_dir=out_dir)
441
  results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
442
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Fallback DOCX generated at {fallback}"}
443
- except Exception as e:
444
- log.error(f"Experimenter failed: {e}")
445
- results.update({"success": False, "stderr": str(e)})
446
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Error: Experimenter failed."}
447
-
448
- def run_synthesis_agent(state: AgentState):
449
- log.info("--- ✍️ Running Synthesis Agent ---")
450
-
451
- # FIX: Defensively ensure state is a dictionary-like object to prevent AttributeError if state is None
452
- _state = state or {}
453
-
454
- path = ensure_list(_state, 'execution_path') + ["Synthesis Agent"]
455
- exp_results = _state.get('experimentResults')
456
- results_summary = "No experiment was conducted."
457
- artifact_message = ""
458
-
459
- if exp_results and isinstance(exp_results, dict):
460
- paths = exp_results.get("paths") or {}
461
- if paths:
462
- artifact_lines = []
463
- for k,v in paths.items():
464
- artifact_lines.append(f"- {k}: `{v}`")
465
- artifact_message = "\n\n**Artifacts produced:**\n" + "\n".join(artifact_lines)
466
- results_summary = f"Artifacts produced: {list(paths.keys())}"
467
- else:
468
- results_summary = f"Experiment Output Stdout: {exp_results.get('stdout','')}\nStderr: {exp_results.get('stderr','')}"
469
 
470
- prompt = (
471
- f"Synthesize all information into a final response.\n\nCore Objective: {_state.get('coreObjectivePrompt', '')}\n\n"
472
- f"Plan: {_state.get('pmPlan', {}).get('plan_steps')}\n\n{results_summary}\n\nFinal Response:"
473
- )
474
-
475
- response = llm.invoke(prompt)
476
- final_text = getattr(response, "content", "") or ""
477
- if artifact_message:
478
- final_text = final_text + "\n\n" + artifact_message
479
-
480
- return {"draftResponse": final_text, "execution_path": path, "status_update": "Putting together the final response..."}
481
-
482
- def run_qa_agent(state: AgentState):
483
- log.info("--- ✅ Running QA Agent ---")
484
- path = ensure_list(state, 'execution_path') + ["QA Agent"]
485
- prompt = (f"Review the draft response based on the core objective. Respond ONLY with 'APPROVED' or provide concise feedback for rework.\n\n"
486
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\nDraft: {state.get('draftResponse')}")
487
- response = llm.invoke(prompt)
488
- content = getattr(response, "content", "") or ""
489
- if "APPROVED" in content.upper():
490
- return {"approved": True, "qaFeedback": None, "execution_path": path, "status_update": "Response approved!"}
491
- else:
492
- return {"approved": False, "qaFeedback": content or "No specific feedback.", "execution_path": path, "status_update": "Response needs improvement. Reworking..."}
493
-
494
- def run_archivist_agent(state: AgentState):
495
- log.info("--- 💾 Running Archivist Agent ---")
496
- path = ensure_list(state, 'execution_path') + ["Archivist Agent"]
497
- summary_prompt = (f"Create a concise summary of this successful task for long-term memory.\n\n"
498
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\nFinal Response: {state.get('draftResponse')}\n\nMemory Summary:")
499
- response = llm.invoke(summary_prompt)
500
- memory_manager.add_to_memory(getattr(response,"content",""), {"objective": state.get('coreObjectivePrompt')})
501
- return {"execution_path": path, "status_update": "Saving key learnings for future reference..."}
502
-
503
- def run_disclaimer_agent(state: AgentState):
504
- log.warning("--- ⚠️ Running Disclaimer Agent ---")
505
- path = ensure_list(state, 'execution_path') + ["Disclaimer Agent"]
506
- disclaimer = ("**DISCLAIMER: The process was stopped after exhausting the budget. The following response is the best available draft and may be incomplete.**\n\n---\n\n")
507
- final_response = disclaimer + state.get('draftResponse', "No response was generated.")
508
- return {"draftResponse": final_response, "execution_path": path, "status_update": "Budget limit reached. Preparing final draft..."}
509
-
510
- # --- Decision & Graph ---
511
- def should_continue(state: AgentState):
512
- log.info("--- 🤔 Decision: Is the response QA approved? ---")
513
- if state.get("approved"):
514
- log.info("Routing to: Archivist (Success Path)")
515
- return "archivist_agent"
516
- if ensure_int(state, "rework_cycles", 0) > ensure_int(state, "max_loops", 0):
517
- log.error(f"BUDGET LIMIT REACHED after {ensure_int(state, 'rework_cycles', 0)-1} cycles.")
518
- return "disclaimer_agent"
519
- else:
520
- log.info("Routing to: PM Agent for rework")
521
- return "pm_agent"
522
-
523
- def should_run_experiment(state: AgentState):
524
- pm = state.get('pmPlan', {}) or {}
525
- return "experimenter_agent" if pm.get('experiment_needed') else "synthesis_agent"
526
-
527
- # --- Build graphs ---
528
- triage_workflow = StateGraph(AgentState)
529
- triage_workflow.add_node("triage", run_triage_agent)
530
- triage_workflow.set_entry_point("triage")
531
- triage_workflow.add_edge("triage", END)
532
- triage_app = triage_workflow.compile()
533
-
534
- planner_workflow = StateGraph(AgentState)
535
- planner_workflow.add_node("planner", run_planner_agent)
536
- planner_workflow.set_entry_point("planner")
537
- planner_workflow.add_edge("planner", END)
538
- planner_app = planner_workflow.compile()
539
-
540
- main_workflow = StateGraph(AgentState)
541
- main_workflow.add_node("memory_retriever", run_memory_retrieval)
542
- main_workflow.add_node("intent_agent", run_intent_agent)
543
- main_workflow.add_node("pm_agent", run_pm_agent)
544
- main_workflow.add_node("experimenter_agent", run_experimenter_agent)
545
- main_workflow.add_node("synthesis_agent", run_synthesis_agent)
546
- main_workflow.add_node("qa_agent", run_qa_agent)
547
- main_workflow.add_node("archivist_agent", run_archivist_agent)
548
- main_workflow.add_node("disclaimer_agent", run_disclaimer_agent)
549
-
550
- main_workflow.set_entry_point("memory_retriever")
551
- main_workflow.add_edge("memory_retriever", "intent_agent")
552
- main_workflow.add_edge("intent_agent", "pm_agent")
553
- main_workflow.add_edge("experimenter_agent", "synthesis_agent")
554
- main_workflow.add_edge("synthesis_agent", "qa_agent")
555
- main_workflow.add_edge("archivist_agent", END)
556
- main_workflow.add_edge("disclaimer_agent", END)
557
-
558
- main_workflow.add_conditional_edges("pm_agent", should_run_experiment)
559
- main_workflow.add_conditional_edges("qa_agent", should_continue, {
560
- "archivist_agent": "archivist_agent",
561
- "pm_agent": "pm_agent",
562
- "disclaimer_agent": "disclaimer_agent"
563
- })
564
- main_app = main_workflow.compile()
 
1
+ # graph.py (Enhanced with better context passing)
2
  import json
3
  import re
4
  import math
 
22
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
23
  from reportlab.lib.styles import getSampleStyleSheet
24
 
25
+ # --- Configurable output directory ---
26
  OUT_DIR = os.environ.get("OUT_DIR", "/tmp")
 
27
  os.makedirs(OUT_DIR, exist_ok=True)
 
28
  EXPORTS_DIR = os.path.join(OUT_DIR, "exports")
29
  os.makedirs(EXPORTS_DIR, exist_ok=True)
30
 
31
  # --- Helpers ---
32
  def ensure_list(state, key):
 
33
  v = state.get(key) if state else None
34
  if v is None:
35
  return []
 
40
  return [v]
41
 
42
  def ensure_int(state, key, default=0):
 
43
  try:
44
  v = state.get(key) if state else None
45
  if v is None:
 
49
  return default
50
 
51
  def sanitize_path(path: str) -> str:
 
52
  return os.path.abspath(path)
53
 
54
  # --- Setup & constants ---
 
71
  draftResponse: str
72
  qaFeedback: Optional[str]
73
  approved: bool
 
74
  execution_path: Annotated[List[str], operator.add]
75
  rework_cycles: int
76
  max_loops: int
 
80
  llm = ChatOpenAI(model="gpt-4o", temperature=0.1, max_retries=3, request_timeout=60)
81
 
82
  def parse_json_from_llm(llm_output: str) -> Optional[dict]:
 
83
  try:
84
  if not llm_output:
85
  return None
 
101
  KNOWN_ARTIFACT_TYPES = {"notebook","excel","word","pdf","image","repo","script"}
102
 
103
  def detect_requested_output_types(text: str) -> Dict:
 
104
  if not text:
105
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
106
  t = text.lower()
 
121
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
122
 
123
  def normalize_experiment_type(exp_type: Optional[str], goal_text: str) -> str:
 
124
  if not exp_type:
125
  detection = detect_requested_output_types(goal_text or "")
126
+ return detection.get("artifact_type") or "word"
127
  s = exp_type.strip().lower()
 
128
  if s in KNOWN_ARTIFACT_TYPES:
129
  return s
 
130
  if "notebook" in s or "ipynb" in s:
131
  return "notebook"
132
  if "excel" in s or "xlsx" in s or "spreadsheet" in s:
 
139
  return "repo"
140
  if "script" in s or "python" in s or ".py" in s:
141
  return "script"
 
142
  detection = detect_requested_output_types(goal_text or "")
143
+ return detection.get("artifact_type") or "word"
144
 
145
+ # --- Notebook & artifact builders ---
146
  def write_notebook_from_text(llm_text: str, out_dir: Optional[str]=None) -> str:
147
  out_dir = out_dir or OUT_DIR
148
  os.makedirs(out_dir, exist_ok=True)
 
268
 
269
  # --- Node functions ---
270
  def run_triage_agent(state: AgentState):
271
+ log.info("--- TRIAGE ---")
272
  prompt = f"Analyze the user input. Is it a simple conversational greeting or a task? Respond with 'greeting' or 'task'.\n\nUser Input: \"{state.get('userInput','')}\""
273
  response = llm.invoke(prompt)
274
  content = getattr(response, "content", "") or ""
 
280
  return {"execution_path": ["Triage Agent"], "status_update": "Request requires a plan. Proceeding..."}
281
 
282
  def run_planner_agent(state: AgentState):
283
+ log.info("--- PLANNER AGENT ---")
284
  path = ensure_list(state, 'execution_path') + ["Planner Agent"]
285
  prompt = (
286
  f"Analyze the user's request. Provide a high-level plan and estimate the number of LLM calls for one loop. "
 
305
  return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan and cost estimate created. Awaiting approval."}
306
 
307
  def run_memory_retrieval(state: AgentState):
308
+ log.info("--- MEMORY RETRIEVAL ---")
309
  path = ensure_list(state, 'execution_path') + ["Memory Retriever"]
310
  relevant_mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
311
  if relevant_mems:
 
317
  return {"retrievedMemory": context, "execution_path": path, "status_update": "Searching for relevant past information..."}
318
 
319
  def run_intent_agent(state: AgentState):
320
+ log.info("--- INTENT AGENT ---")
321
  path = ensure_list(state, 'execution_path') + ["Intent Agent"]
322
  prompt = (f"Refine the user's request into a clear, actionable 'core objective prompt'.\n\nRelevant Memory:\n{state.get('retrievedMemory')}\n\nUser Request: \"{state.get('userInput','')}\"\n\nCore Objective:")
323
  response = llm.invoke(prompt)
 
329
  return {"coreObjectivePrompt": core_obj, **extras, "execution_path": path, "status_update": "Clarifying the main objective..."}
330
 
331
  def run_pm_agent(state: AgentState):
332
+ log.info("--- PM AGENT ---")
333
  current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
334
  max_loops_val = ensure_int(state, 'max_loops', 0)
335
  log.info(f"Starting work cycle {current_cycles}/{max_loops_val + 1}")
336
  path = ensure_list(state, 'execution_path') + ["PM Agent"]
337
+
338
+ # BUILD COMPREHENSIVE CONTEXT
339
+ context_parts = [
340
+ f"=== USER'S ORIGINAL REQUEST ===",
341
+ f"{state.get('userInput', '')}",
342
+ f"\n=== CORE OBJECTIVE ===",
343
+ f"{state.get('coreObjectivePrompt', '')}",
344
+ f"\n=== RELEVANT MEMORY ===",
345
+ f"{state.get('retrievedMemory', 'None')}",
346
+ ]
347
+
348
+ if state.get('qaFeedback'):
349
+ context_parts.append(f"\n=== QA FEEDBACK (MUST ADDRESS) ===")
350
+ context_parts.append(f"{state.get('qaFeedback')}")
351
+ context_parts.append(f"\n=== PREVIOUS PLAN ===")
352
+ context_parts.append(f"{json.dumps(state.get('pmPlan', {}).get('plan_steps', []), indent=2)}")
353
+
354
+ full_context = "\n".join(context_parts)
355
+
356
+ # ENHANCED PM PROMPT
357
+ prompt = f"""You are a Project Manager creating a DETAILED, EXECUTABLE plan.
358
+
359
+ {full_context}
360
+
361
+ Your task is to create a plan where each step is SPECIFIC and ACTIONABLE:
362
+ - State EXACTLY what will be created/analyzed
363
+ - Specify WHAT information/data will be used
364
+ - Define WHAT approach/method will be applied
365
+
366
+ Respond in JSON format:
367
+ {{
368
+ "plan_steps": [
369
+ "Specific executable step 1 with clear deliverable...",
370
+ "Specific executable step 2 with clear action...",
371
+ "..."
372
+ ],
373
+ "experiment_needed": true/false,
374
+ "experiment_type": "notebook|script|excel|word|pdf|repo",
375
+ "experiment_goal": "Detailed description of artifact content and purpose",
376
+ "experiment_language": "python|r|java|javascript" (if script),
377
+ "key_requirements": ["Critical requirements that MUST be met"]
378
+ }}
379
+
380
+ CRITICAL: Be specific about:
381
+ - Analysis tasks: WHAT to analyze and HOW
382
+ - Code tasks: WHAT functionality to implement
383
+ - Document tasks: WHAT sections/content to include
384
+ - Using any uploaded files or user-provided data
385
+ """
386
+
387
  response = llm.invoke(prompt)
388
  plan = parse_json_from_llm(getattr(response, "content", "") or "")
389
+
390
  if not plan:
391
+ log.warning("PM Agent did not produce JSON applying fallback.")
392
+ detection = detect_requested_output_types(state.get('userInput', ''))
393
+ plan = {
394
+ "plan_steps": [
395
+ f"Analyze request: {state.get('userInput', '')[:100]}...",
396
+ "Process relevant information",
397
+ "Create deliverable with specific details",
398
+ "Review output quality"
399
+ ],
400
+ "experiment_needed": detection.get('requires_artifact', False),
401
+ "experiment_type": detection.get('artifact_type', 'word'),
402
+ "experiment_goal": state.get('coreObjectivePrompt', state.get('userInput', ''))
403
+ }
404
+
405
  exp_type_raw = plan.get('experiment_type') or ""
406
  plan_goal = plan.get('experiment_goal') or state.get('userInput','') or state.get('coreObjectivePrompt','')
407
  normalized = normalize_experiment_type(exp_type_raw, plan_goal)
408
  plan['experiment_type'] = normalized
409
+
410
  if plan.get('experiment_needed') and not plan.get('experiment_goal'):
411
  plan['experiment_goal'] = plan_goal
412
+
413
+ log.info(f"Plan: Steps={len(plan.get('plan_steps', []))}, Experiment={plan.get('experiment_needed')}, Type={plan.get('experiment_type')}")
414
+
415
+ return {
416
+ "pmPlan": plan,
417
+ "execution_path": path,
418
+ "rework_cycles": current_cycles,
419
+ "status_update": f"Detailed plan created ({len(plan.get('plan_steps', []))} steps)"
420
+ }
421
 
422
  def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
 
423
  if lang_hint and "python" in (lang_hint or "").lower():
424
  blocks = re.findall(r"```python\s*(.*?)\s*```", text, re.DOTALL)
425
  if blocks:
 
428
  return blocks
429
 
430
  def run_experimenter_agent(state: AgentState):
431
+ log.info("--- EXPERIMENTER AGENT ---")
432
  path = ensure_list(state, 'execution_path') + ["Experimenter Agent"]
433
  pm = state.get('pmPlan', {}) or {}
434
+
435
  if not pm.get('experiment_needed'):
436
+ return {
437
+ "experimentCode": None,
438
+ "experimentResults": None,
439
+ "execution_path": path,
440
+ "status_update": "No experiment needed."
441
+ }
442
+
443
  exp_type = normalize_experiment_type(pm.get('experiment_type'), pm.get('experiment_goal',''))
444
  goal = pm.get('experiment_goal', 'No goal specified.')
445
+
446
+ # BUILD COMPREHENSIVE CONTEXT FOR EXPERIMENTER
447
+ context_parts = [
448
+ f"=== USER'S ORIGINAL REQUEST ===",
449
+ f"{state.get('userInput', '')}",
450
+ f"\n=== CORE OBJECTIVE ===",
451
+ f"{state.get('coreObjectivePrompt', '')}",
452
+ f"\n=== EXECUTION PLAN ===",
453
+ f"{json.dumps(pm.get('plan_steps', []), indent=2)}",
454
+ f"\n=== KEY REQUIREMENTS ===",
455
+ f"{json.dumps(pm.get('key_requirements', []), indent=2)}",
456
+ ]
457
+
458
+ if state.get('retrievedMemory'):
459
+ context_parts.append(f"\n=== RELEVANT PAST CONTEXT ===")
460
+ context_parts.append(f"{state.get('retrievedMemory', '')}")
461
+
462
+ if state.get('qaFeedback'):
463
+ context_parts.append(f"\n=== FEEDBACK TO ADDRESS ===")
464
+ context_parts.append(f"{state.get('qaFeedback', '')}")
465
+
466
+ full_context = "\n".join(context_parts)
467
+
468
+ # ENHANCED EXPERIMENTER PROMPT
469
+ enhanced_prompt = f"""You are creating a HIGH-QUALITY {exp_type} artifact.
470
+
471
+ {full_context}
472
+
473
+ ARTIFACT GOAL: {goal}
474
+
475
+ QUALITY REQUIREMENTS:
476
+ 1. Use ALL specific details from the user's request
477
+ 2. Create PRODUCTION-READY, COMPLETE content (NO templates or placeholders)
478
+ 3. Include ACTUAL data, REALISTIC examples, and WORKING implementations
479
+ 4. For notebooks: Include markdown explanations, executable code, and visualizations
480
+ 5. For scripts: Include error handling, documentation, and real logic
481
+ 6. For documents: Provide substantive, detailed content based on context
482
+ 7. For analysis: Use specific methodologies and provide concrete insights
483
+
484
+ Generate complete, high-quality content for '{exp_type}'.
485
+ Use fenced code blocks with language identifiers where appropriate.
486
+ """
487
+
488
+ response = llm.invoke(enhanced_prompt)
489
  llm_text = getattr(response, "content", "") or ""
490
  out_dir = OUT_DIR
491
+ results = {"success": False, "paths": {}, "stderr": "", "stdout": "", "context_used": len(full_context)}
492
+
493
  try:
494
  if exp_type == 'notebook':
495
  nb_path = write_notebook_from_text(llm_text, out_dir=out_dir)
496
  results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
497
+ return {
498
+ "experimentCode": None,
499
+ "experimentResults": results,
500
+ "execution_path": path,
501
+ "status_update": f"Notebook generated ({len(full_context)} chars context)"
502
+ }
503
+
504
  elif exp_type == 'excel':
505
  excel_path = write_excel_from_tables(llm_text, out_dir=out_dir)
506
  results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
507
+ return {
508
+ "experimentCode": None,
509
+ "experimentResults": results,
510
+ "execution_path": path,
511
+ "status_update": f"Excel generated"
512
+ }
513
+
514
  elif exp_type == 'word':
515
  docx_path = write_docx_from_text(llm_text, out_dir=out_dir)
516
  results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
517
+ return {
518
+ "experimentCode": None,
519
+ "experimentResults": results,
520
+ "execution_path": path,
521
+ "status_update": f"DOCX generated"
522
+ }
523
+
524
  elif exp_type == 'pdf':
525
  pdf_path = write_pdf_from_text(llm_text, out_dir=out_dir)
526
  results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
527
+ return {
528
+ "experimentCode": None,
529
+ "experimentResults": results,
530
+ "execution_path": path,
531
+ "status_update": f"PDF generated"
532
+ }
533
+
534
  elif exp_type == 'script':
535
  lang_hint = pm.get('experiment_language') or ("python" if ".py" in goal.lower() else None)
536
  code_blocks = _extract_code_blocks(llm_text, lang_hint)
537
+
538
  if not code_blocks:
539
  code_text = llm_text
540
  else:
541
  code_text = "\n\n# === BLOCK ===\n\n".join(code_blocks)
542
+
543
  script_path = write_script(code_text, language_hint=lang_hint, out_dir=out_dir)
544
  exec_results = {}
545
+
546
  if script_path.endswith(".py"):
547
  try:
548
+ exec_results = execute_python_code(code_text)
549
  except Exception as e:
550
  exec_results = {"stdout":"","stderr":str(e),"success":False}
551
+
552
+ results.update({
553
+ "success": True,
554
+ "paths": {"script": sanitize_path(script_path)},
555
+ "stdout": exec_results.get("stdout",""),
556
+ "stderr": exec_results.get("stderr","")
557
+ })
558
+ return {
559
+ "experimentCode": code_text,
560
+ "experimentResults": results,
561
+ "execution_path": path,
562
+ "status_update": f"Script generated"
563
+ }
564
+
565
  elif exp_type == 'repo':
566
  repo_files = {}
567
  readme = (llm_text[:1000] + "\n\n") if llm_text else "Generated repo"
 
572
  repo_files["requirements.txt"] = reqs
573
  zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=out_dir)
574
  results.update({"success": True, "paths": {"repo_zip": sanitize_path(zip_path)}})
575
+ return {
576
+ "experimentCode": None,
577
+ "experimentResults": results,
578
+ "execution_path": path,
579
+ "status_update": f"Repository ZIP created"
580
+ }
581
+
582
  else:
 
583
  fallback = write_docx_from_text(llm_text, out_dir=out_dir)
584
  results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
585
+ return {
586
+ "experimentCode": None,
587
+ "experimentResults": results,
588
+ "execution_path": path,
589
+ "status_update": f"Fallback DOCX generated"
590
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
+ except Exception as e:
593
+ log.error(f"Experimenter failed: {e}")