JatinAutonomousLabs commited on
Commit
c277b6d
·
verified ·
1 Parent(s): aa83392

Update graph.py

Browse files
Files changed (1) hide show
  1. graph.py +323 -228
graph.py CHANGED
@@ -1,4 +1,4 @@
1
- # graph.py - Enhanced with better loop control and cost tracking
2
 
3
  import json
4
  import re
@@ -8,7 +8,7 @@ import uuid
8
  import shutil
9
  import zipfile
10
  import operator
11
- from typing import TypedDict, List, Dict, Optional, Annotated
12
  from datetime import datetime
13
  from langchain_openai import ChatOpenAI
14
  from langgraph.graph import StateGraph, END
@@ -76,11 +76,36 @@ class AgentState(TypedDict):
76
  execution_path: Annotated[List[str], operator.add]
77
  rework_cycles: int
78
  max_loops: int
79
- status_update: str
80
- # NEW: For real-time cost tracking
81
  current_cost: float
82
  budget_exceeded: bool
83
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  # --- LLM ---
86
  llm = ChatOpenAI(model="gpt-4o", temperature=0.5, max_retries=3, request_timeout=60)
@@ -187,7 +212,7 @@ def parse_json_from_llm(llm_output: str) -> Optional[dict]:
187
  except Exception as e:
188
  logger.debug(f"json.loads still failed after cleanup: {e}")
189
 
190
- # nothing parsed log preview and return None
191
  logger.error("parse_json_from_llm failed to parse LLM output. LLM output preview (200 chars): %s", text[:200].replace("\n","\\n"))
192
  return None
193
 
@@ -368,8 +393,15 @@ def run_triage_agent(state: AgentState):
368
  response = llm.invoke(prompt)
369
  content = getattr(response, "content", "") or ""
370
  if 'greeting' in content.lower():
371
- return {"draftResponse": "Hello! How can I help?", "execution_path": ["Triage"], "status_update": "Greeting"}
372
- return {"execution_path": ["Triage"], "status_update": "Task detected"}
 
 
 
 
 
 
 
373
 
374
  def run_planner_agent(state: AgentState):
375
  log.info("--- PLANNER ---")
@@ -378,7 +410,11 @@ def run_planner_agent(state: AgentState):
378
  response = llm.invoke(prompt)
379
  plan_data = parse_json_from_llm(getattr(response, "content", "") or "")
380
  if not plan_data:
381
- return {"pmPlan": {"error": "Planning failed"}, "execution_path": path, "status_update": "Error"}
 
 
 
 
382
 
383
  calls = plan_data.get('estimated_llm_calls_per_loop', 3)
384
  cost_per_loop = (calls * AVG_TOKENS_PER_CALL) * ((GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2)
@@ -392,14 +428,22 @@ def run_planner_agent(state: AgentState):
392
  plan_data.setdefault('experiment_type', detection.get('artifact_type'))
393
  plan_data.setdefault('experiment_goal', state.get('userInput',''))
394
 
395
- return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan created"}
 
 
 
 
396
 
397
  def run_memory_retrieval(state: AgentState):
398
  log.info("--- MEMORY ---")
399
  path = ensure_list(state, 'execution_path') + ["Memory"]
400
  mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
401
  context = "\n".join([f"Memory: {m.page_content}" for m in mems]) if mems else "No memories"
402
- return {"retrievedMemory": context, "execution_path": path, "status_update": "Memory retrieved"}
 
 
 
 
403
 
404
  def run_intent_agent(state: AgentState):
405
  log.info("--- INTENT ---")
@@ -407,7 +451,11 @@ def run_intent_agent(state: AgentState):
407
  prompt = f"Refine into clear objective.\n\nMemory: {state.get('retrievedMemory')}\n\nRequest: {state.get('userInput','')}\n\nCore Objective:"
408
  response = llm.invoke(prompt)
409
  core_obj = getattr(response, "content", "") or ""
410
- return {"coreObjectivePrompt": core_obj, "execution_path": path, "status_update": "Objective clarified"}
 
 
 
 
411
 
412
  def run_pm_agent(state: AgentState):
413
  log.info("--- PM ---")
@@ -424,14 +472,17 @@ def run_pm_agent(state: AgentState):
424
  "experiment_type": "word",
425
  "experiment_goal": state.get('coreObjectivePrompt', state.get('userInput',''))
426
  }
427
- return {"pmPlan": fallback_plan, "execution_path": path, "rework_cycles": current_rework, "status_update": "Rework limit hit - manual review"}
 
 
 
 
 
428
 
429
  # Normal behavior: increment rework count for this pass
430
  current_cycles = current_rework + 1
431
  path = ensure_list(state, 'execution_path') + ["PM"]
432
 
433
- # (rest of your original PM prompt & parse flow, but ensure the output sets rework_cycles and max_loops)
434
- # --- build full_context like before ---
435
  context_parts = [
436
  f"=== USER REQUEST ===\n{state.get('userInput', '')}",
437
  f"\n=== OBJECTIVE ===\n{state.get('coreObjectivePrompt', '')}",
@@ -481,8 +532,14 @@ Be concrete.
481
 
482
  # Attach loop control info
483
  plan['max_loops_initial'] = max_loops_val
484
- plan['estimated_cost_usd'] = plan.get('estimated_cost_usd', plan.get('estimated_cost_usd', 0.0))
485
- return {"pmPlan": plan, "execution_path": path, "rework_cycles": current_cycles, "max_loops": max_loops_val, "status_update": f"Plan created ({len(plan.get('plan_steps', []))} steps)"}
 
 
 
 
 
 
486
 
487
  def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
488
  if lang_hint and "python" in (lang_hint or "").lower():
@@ -497,7 +554,12 @@ def run_experimenter_agent(state: AgentState):
497
  pm = state.get('pmPlan', {}) or {}
498
 
499
  if not pm.get('experiment_needed'):
500
- return {"experimentCode": None, "experimentResults": None, "execution_path": path, "status_update": "No experiment needed"}
 
 
 
 
 
501
 
502
  exp_type = normalize_experiment_type(pm.get('experiment_type'), pm.get('experiment_goal',''))
503
  goal = pm.get('experiment_goal', 'No goal')
@@ -527,184 +589,161 @@ def run_experimenter_agent(state: AgentState):
527
  GOAL: {goal}
528
 
529
  CRITICAL REQUIREMENTS:
530
-
531
  1. ACTUAL WORKING CODE - Not templates, not documentation, not examples. REAL production code.
532
-
533
  2. FILE STRUCTURE - Indicate each file clearly:
534
  ### path/to/file.py
535
  ```python
536
  [Complete working code]
537
- MUST INCLUDE:
538
-
539
- Complete API clients with error handling, retries, rate limiting
540
-
541
- Database schema with CREATE TABLE statements
542
-
543
- Data processing with real transformation logic
544
-
545
- Config management (.env handling)
546
-
547
- requirements.txt with ALL dependencies
548
-
549
- main.py entry point
550
-
551
- Comprehensive README
552
-
553
- CODE QUALITY:
554
-
555
- Environment variables for secrets
556
-
557
- Error handling and logging
558
-
559
- Docstrings and comments
560
-
561
- Real business logic based on request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
 
563
- RUNNABLE out of the box
 
564
 
565
- SPECIFIC TO REQUEST:
 
566
 
567
- Use EXACT APIs mentioned (e.g., CricAPI, SportsRadar)
 
 
 
 
568
 
569
- Implement SPECIFIC algorithms (e.g., batting avg, strike rate)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
- Create EXACT database tables needed
 
 
 
 
572
 
573
- Process SPECIFIC data formats
574
 
575
- NO placeholders like "# TODO"
576
- NO dummy data - implement REAL logic
577
- NO documentation-style code - PRODUCTION code only
 
578
 
579
- Format each file:
 
 
580
 
581
- path/to/file.py
582
- # Complete code here
583
- Generate complete repository:"""
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
  # OTHER ARTIFACT TYPES
586
  enhanced_prompt = f"""Create HIGH-QUALITY {exp_type} artifact.
587
- {full_context}
588
-
589
- GOAL: {goal}
590
-
591
- REQUIREMENTS:
592
-
593
- Use ALL specific details from request
594
-
595
- PRODUCTION-READY, COMPLETE content (NO placeholders)
596
-
597
- ACTUAL data, REALISTIC examples, WORKING code
598
-
599
- For notebooks: markdown + executable code + visualizations
600
-
601
- For scripts: error handling + docs + real logic
602
-
603
- For documents: substantive detailed content
604
-
605
- Generate complete content for '{exp_type}' with proper code fences."""
606
 
 
607
 
608
- response = llm.invoke(enhanced_prompt)
609
- llm_text = getattr(response, "content", "") or ""
610
-
611
- # Parse files from response
612
- repo_files = {}
613
-
614
- # Extract with ### headers
615
- file_pattern = r"###\s+([\w\/_\-\.]+)\s*\n```(?:\w+)?\s*\n(.*?)\n```"
616
- matches = re.finditer(file_pattern, llm_text, re.DOTALL)
617
-
618
- for match in matches:
619
- filepath = match.group(1).strip()
620
- content = match.group(2).strip()
621
- repo_files[filepath] = content
622
-
623
- # Fallback: extract code blocks
624
- if not repo_files:
625
- code_blocks = re.findall(r"```(?:python|sql)?\s*\n(.*?)\n```", llm_text, re.DOTALL)
626
- if code_blocks:
627
- for i, block in enumerate(code_blocks):
628
- if len(block) > 50: # Skip tiny blocks
629
- repo_files[f"module_{i}.py"] = block
630
-
631
- # Add README if missing
632
- if not any('README' in f.upper() for f in repo_files):
633
- repo_files["README.md"] = f"""# Generated Application
634
- Overview
635
- {goal}
636
-
637
- Files
638
- {chr(10).join(f'- {f}' for f in sorted(repo_files.keys()))}
639
-
640
- Setup
641
- pip install -r requirements.txt
642
-
643
- Copy .env.example to .env and configure
644
-
645
- Run: python main.py
646
- """
647
-
648
- # Add requirements.txt
649
- if "requirements.txt" not in repo_files:
650
- all_code = " ".join(repo_files.values()).lower()
651
- deps = []
652
- if 'requests' in all_code: deps.append('requests')
653
- if 'pandas' in all_code: deps.append('pandas')
654
- if 'numpy' in all_code: deps.append('numpy')
655
- if 'sqlalchemy' in all_code: deps.append('sqlalchemy')
656
- if 'postgresql' in all_code or 'psycopg2' in all_code: deps.append('psycopg2-binary')
657
- if 'flask' in all_code: deps.append('flask')
658
- if 'fastapi' in all_code:
659
- deps.append('fastapi')
660
- deps.append('uvicorn')
661
- if 'dotenv' in all_code: deps.append('python-dotenv')
662
-
663
- repo_files["requirements.txt"] = "\n".join(deps) if deps else "# Dependencies"
664
-
665
- # Add .env.example
666
- if ".env.example" not in repo_files:
667
- repo_files[".env.example"] = """# Configuration
668
- API_KEY=your_key_here
669
- DATABASE_URL=postgresql://user:pass@localhost/db
670
- DEBUG=False
671
- """
672
-
673
- # Add main.py if missing
674
- if not any('main.py' in f for f in repo_files):
675
- repo_files["main.py"] = """#!/usr/bin/env python3
676
- import os
677
- from dotenv import load_dotenv
678
-
679
- load_dotenv()
680
-
681
- def main():
682
- print("Application starting...")
683
- # Add your logic here
684
- pass
685
 
686
- if name == "main":
687
- main()
688
- """
 
 
 
 
689
 
690
- # Build zip
691
- zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=OUT_DIR)
692
-
693
- results = {
694
- "success": True,
695
- "paths": {"repo_zip": sanitize_path(zip_path)},
696
- "files_created": len(repo_files),
697
- "context_used": len(full_context)
698
- }
699
-
700
- return {
701
- "experimentCode": None,
702
- "experimentResults": results,
703
- "execution_path": path,
704
- "status_update": f"Repository created ({len(repo_files)} files)"
705
- }
706
 
707
-
708
  response = llm.invoke(enhanced_prompt)
709
  llm_text = getattr(response, "content", "") or ""
710
  results = {"success": False, "paths": {}, "stderr": "", "stdout": "", "context_used": len(full_context)}
@@ -713,22 +752,42 @@ CRITICAL REQUIREMENTS:
713
  if exp_type == 'notebook':
714
  nb_path = write_notebook_from_text(llm_text, out_dir=OUT_DIR)
715
  results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
716
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Notebook created"}
 
 
 
 
 
717
 
718
  elif exp_type == 'excel':
719
  excel_path = write_excel_from_tables(llm_text, out_dir=OUT_DIR)
720
  results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
721
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Excel created"}
 
 
 
 
 
722
 
723
  elif exp_type == 'word':
724
  docx_path = write_docx_from_text(llm_text, out_dir=OUT_DIR)
725
  results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
726
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "DOCX created"}
 
 
 
 
 
727
 
728
  elif exp_type == 'pdf':
729
  pdf_path = write_pdf_from_text(llm_text, out_dir=OUT_DIR)
730
  results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
731
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "PDF created"}
 
 
 
 
 
732
 
733
  elif exp_type == 'script':
734
  lang_hint = pm.get('experiment_language') or "python"
@@ -750,17 +809,33 @@ CRITICAL REQUIREMENTS:
750
  "stdout": exec_results.get("stdout",""),
751
  "stderr": exec_results.get("stderr","")
752
  })
753
- return {"experimentCode": code_text, "experimentResults": results, "execution_path": path, "status_update": "Script created"}
 
 
 
 
 
754
 
755
  else:
756
  fallback = write_docx_from_text(llm_text, out_dir=OUT_DIR)
757
  results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
758
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Document created"}
 
 
 
 
 
759
 
760
  except Exception as e:
761
  log.error(f"Experimenter failed: {e}")
762
  results.update({"success": False, "stderr": str(e)})
763
- return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Error"}
 
 
 
 
 
 
764
  def run_synthesis_agent(state: AgentState):
765
  log.info("--- SYNTHESIS ---")
766
  _state = state or {}
@@ -799,23 +874,17 @@ def run_synthesis_agent(state: AgentState):
799
  full_context = "\n".join(synthesis_context)
800
 
801
  synthesis_prompt = f"""Create FINAL RESPONSE after executing user's request.
802
- {full_context}
803
-
804
- Create comprehensive response that:
805
-
806
- Directly addresses original request
807
 
808
- Explains what was accomplished and HOW
809
-
810
- References specific artifacts and explains PURPOSE
811
-
812
- Provides context on how to USE deliverables
813
-
814
- Highlights KEY INSIGHTS
815
-
816
- Suggests NEXT STEPS if relevant
817
 
818
- Be SPECIFIC about what was created."""
 
 
 
 
 
 
 
819
 
820
  response = llm.invoke(synthesis_prompt)
821
  final_text = getattr(response, "content", "") or ""
@@ -823,7 +892,11 @@ def run_synthesis_agent(state: AgentState):
823
  if artifact_message:
824
  final_text = final_text + "\n\n---\n" + artifact_message
825
 
826
- return {"draftResponse": final_text, "execution_path": path, "status_update": "Response synthesized"}
 
 
 
 
827
 
828
  def run_qa_agent(state: AgentState):
829
  log.info("--- QA ---")
@@ -839,40 +912,47 @@ def run_qa_agent(state: AgentState):
839
  qa_context.append(f"\n=== ARTIFACTS ===\n{json.dumps(state.get('experimentResults', {}).get('paths', {}), indent=2)}")
840
 
841
  prompt = f"""You are a QA reviewer. Review the draft response against the user's objective.
842
- {chr(10).join(qa_context)}
843
 
844
- Review Instructions:
845
 
846
- Does the draft and its artifacts COMPLETELY satisfy ALL parts of the user's request?
 
 
 
847
 
848
- Is the quality of the work high?
849
 
850
- If this is a re-submission (rework cycle > 1), has the previous feedback been successfully addressed?
 
851
 
852
- Response Format (required JSON or a single word 'APPROVED'):
853
-
854
- Either return EXACTLY the single word:
855
- APPROVED
856
-
857
- Or return JSON like:
858
- {{
859
- "approved": false,
860
- "feedback": "Specific, actionable items to fix (bullet list or numbered).",
861
- "required_changes": ["..."]
862
- }}
863
- """
864
 
865
  try:
866
  response = llm.invoke(prompt)
867
  content = getattr(response, "content", "") or ""
868
  except Exception as e:
869
  log.exception("QA LLM call failed: %s", e)
870
- # Fail-safe: mark as not approved with conservative feedback
871
- return {"approved": False, "qaFeedback": "QA LLM failed; manual review required.", "execution_path": path, "status_update": "QA failed"}
 
 
 
 
872
 
873
  # If LLM returned APPROVED word, treat as approved
874
  if "APPROVED" in content.strip().upper() and len(content.strip()) <= 20:
875
- return {"approved": True, "qaFeedback": None, "execution_path": path, "status_update": "Approved"}
 
 
 
 
 
876
 
877
  # Else try JSON parse
878
  parsed = parse_json_from_llm(content)
@@ -884,10 +964,21 @@ def run_qa_agent(state: AgentState):
884
  feedback = "\n".join([str(x) for x in feedback])
885
  elif not isinstance(feedback, str):
886
  feedback = str(feedback)
887
- return {"approved": approved, "qaFeedback": feedback if not approved else None, "execution_path": path, "status_update": "QA completed"}
 
 
 
 
 
 
888
  # Fallback: return raw text as feedback (not approved)
889
  safe_feedback = content.strip()[:2000] or "QA produced no actionable output."
890
- return {"approved": False, "qaFeedback": safe_feedback, "execution_path": path, "status_update": "QA needs rework"}
 
 
 
 
 
891
 
892
  def run_archivist_agent(state: AgentState):
893
  log.info("--- ARCHIVIST ---")
@@ -897,7 +988,10 @@ def run_archivist_agent(state: AgentState):
897
  response = llm.invoke(summary_prompt)
898
  memory_manager.add_to_memory(getattr(response,"content",""), {"objective": state.get('coreObjectivePrompt')})
899
 
900
- return {"execution_path": path, "status_update": "Saved to memory"}
 
 
 
901
 
902
  def run_disclaimer_agent(state: AgentState):
903
  log.warning("--- DISCLAIMER ---")
@@ -907,7 +1001,11 @@ def run_disclaimer_agent(state: AgentState):
907
  disclaimer = f"**DISCLAIMER: {reason} Draft may be incomplete.**\n\n---\n\n"
908
  final_response = disclaimer + state.get('draftResponse', "No response")
909
 
910
- return {"draftResponse": final_response, "execution_path": path, "status_update": reason}
 
 
 
 
911
 
912
  def should_continue(state: AgentState):
913
  # Budget check first
@@ -929,7 +1027,6 @@ def should_continue(state: AgentState):
929
  # Default: return pm_agent so planner will create next plan
930
  return "pm_agent"
931
 
932
-
933
  def should_run_experiment(state: AgentState):
934
  pm = state.get('pmPlan', {}) or {}
935
  return "experimenter_agent" if pm.get('experiment_needed') else "synthesis_agent"
@@ -967,11 +1064,9 @@ main_workflow.add_edge("disclaimer_agent", END)
967
 
968
  main_workflow.add_conditional_edges("pm_agent", should_run_experiment)
969
  main_workflow.add_conditional_edges("qa_agent", should_continue, {
970
- "archivist_agent": "archivist_agent",
971
- "pm_agent": "pm_agent",
972
- "disclaimer_agent": "disclaimer_agent"
973
  })
974
 
975
- main_app = main_workflow.compile()
976
-
977
-
 
1
+ # graph.py - Fixed version with proper state handling for concurrent updates
2
 
3
  import json
4
  import re
 
8
  import shutil
9
  import zipfile
10
  import operator
11
+ from typing import TypedDict, List, Dict, Optional, Annotated, Any
12
  from datetime import datetime
13
  from langchain_openai import ChatOpenAI
14
  from langgraph.graph import StateGraph, END
 
76
  execution_path: Annotated[List[str], operator.add]
77
  rework_cycles: int
78
  max_loops: int
79
+ # Use Annotated with operator.add for fields that multiple agents might update
80
+ status_updates: Annotated[List[Dict[str, str]], operator.add] # Changed from status_update
81
  current_cost: float
82
  budget_exceeded: bool
83
+ # Add other fields that might have concurrent updates
84
+ pragmatistReport: Optional[Dict]
85
+ governanceReport: Optional[Dict]
86
+ complianceReport: Optional[Dict]
87
+ observerReport: Optional[Dict]
88
+ knowledgeInsights: Optional[Dict]
89
+
90
+ # Helper to get latest status
91
+ def get_latest_status(state: AgentState) -> str:
92
+ """Get the most recent status update from the list"""
93
+ updates = state.get('status_updates', [])
94
+ if updates and isinstance(updates, list):
95
+ # Get the last update's status value
96
+ for update in reversed(updates):
97
+ if isinstance(update, dict) and 'status' in update:
98
+ return update['status']
99
+ elif isinstance(update, str):
100
+ return update
101
+ return "Processing..."
102
+
103
+ # Helper to add status update
104
+ def add_status_update(node_name: str, status: str) -> Dict[str, Any]:
105
+ """Create a status update entry"""
106
+ return {
107
+ "status_updates": [{"node": node_name, "status": status, "timestamp": datetime.utcnow().isoformat()}]
108
+ }
109
 
110
  # --- LLM ---
111
  llm = ChatOpenAI(model="gpt-4o", temperature=0.5, max_retries=3, request_timeout=60)
 
212
  except Exception as e:
213
  logger.debug(f"json.loads still failed after cleanup: {e}")
214
 
215
+ # nothing parsed log preview and return None
216
  logger.error("parse_json_from_llm failed to parse LLM output. LLM output preview (200 chars): %s", text[:200].replace("\n","\\n"))
217
  return None
218
 
 
393
  response = llm.invoke(prompt)
394
  content = getattr(response, "content", "") or ""
395
  if 'greeting' in content.lower():
396
+ return {
397
+ "draftResponse": "Hello! How can I help?",
398
+ "execution_path": ["Triage"],
399
+ **add_status_update("Triage", "Greeting")
400
+ }
401
+ return {
402
+ "execution_path": ["Triage"],
403
+ **add_status_update("Triage", "Task detected")
404
+ }
405
 
406
  def run_planner_agent(state: AgentState):
407
  log.info("--- PLANNER ---")
 
410
  response = llm.invoke(prompt)
411
  plan_data = parse_json_from_llm(getattr(response, "content", "") or "")
412
  if not plan_data:
413
+ return {
414
+ "pmPlan": {"error": "Planning failed"},
415
+ "execution_path": path,
416
+ **add_status_update("Planner", "Error")
417
+ }
418
 
419
  calls = plan_data.get('estimated_llm_calls_per_loop', 3)
420
  cost_per_loop = (calls * AVG_TOKENS_PER_CALL) * ((GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2)
 
428
  plan_data.setdefault('experiment_type', detection.get('artifact_type'))
429
  plan_data.setdefault('experiment_goal', state.get('userInput',''))
430
 
431
+ return {
432
+ "pmPlan": plan_data,
433
+ "execution_path": path,
434
+ **add_status_update("Planner", "Plan created")
435
+ }
436
 
437
  def run_memory_retrieval(state: AgentState):
438
  log.info("--- MEMORY ---")
439
  path = ensure_list(state, 'execution_path') + ["Memory"]
440
  mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
441
  context = "\n".join([f"Memory: {m.page_content}" for m in mems]) if mems else "No memories"
442
+ return {
443
+ "retrievedMemory": context,
444
+ "execution_path": path,
445
+ **add_status_update("Memory", "Memory retrieved")
446
+ }
447
 
448
  def run_intent_agent(state: AgentState):
449
  log.info("--- INTENT ---")
 
451
  prompt = f"Refine into clear objective.\n\nMemory: {state.get('retrievedMemory')}\n\nRequest: {state.get('userInput','')}\n\nCore Objective:"
452
  response = llm.invoke(prompt)
453
  core_obj = getattr(response, "content", "") or ""
454
+ return {
455
+ "coreObjectivePrompt": core_obj,
456
+ "execution_path": path,
457
+ **add_status_update("Intent", "Objective clarified")
458
+ }
459
 
460
  def run_pm_agent(state: AgentState):
461
  log.info("--- PM ---")
 
472
  "experiment_type": "word",
473
  "experiment_goal": state.get('coreObjectivePrompt', state.get('userInput',''))
474
  }
475
+ return {
476
+ "pmPlan": fallback_plan,
477
+ "execution_path": path,
478
+ "rework_cycles": current_rework,
479
+ **add_status_update("PM", "Rework limit hit - manual review")
480
+ }
481
 
482
  # Normal behavior: increment rework count for this pass
483
  current_cycles = current_rework + 1
484
  path = ensure_list(state, 'execution_path') + ["PM"]
485
 
 
 
486
  context_parts = [
487
  f"=== USER REQUEST ===\n{state.get('userInput', '')}",
488
  f"\n=== OBJECTIVE ===\n{state.get('coreObjectivePrompt', '')}",
 
532
 
533
  # Attach loop control info
534
  plan['max_loops_initial'] = max_loops_val
535
+ plan['estimated_cost_usd'] = plan.get('estimated_cost_usd', 0.0)
536
+ return {
537
+ "pmPlan": plan,
538
+ "execution_path": path,
539
+ "rework_cycles": current_cycles,
540
+ "max_loops": max_loops_val,
541
+ **add_status_update("PM", f"Plan created ({len(plan.get('plan_steps', []))} steps)")
542
+ }
543
 
544
  def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
545
  if lang_hint and "python" in (lang_hint or "").lower():
 
554
  pm = state.get('pmPlan', {}) or {}
555
 
556
  if not pm.get('experiment_needed'):
557
+ return {
558
+ "experimentCode": None,
559
+ "experimentResults": None,
560
+ "execution_path": path,
561
+ **add_status_update("Experimenter", "No experiment needed")
562
+ }
563
 
564
  exp_type = normalize_experiment_type(pm.get('experiment_type'), pm.get('experiment_goal',''))
565
  goal = pm.get('experiment_goal', 'No goal')
 
589
  GOAL: {goal}
590
 
591
  CRITICAL REQUIREMENTS:
 
592
  1. ACTUAL WORKING CODE - Not templates, not documentation, not examples. REAL production code.
 
593
  2. FILE STRUCTURE - Indicate each file clearly:
594
  ### path/to/file.py
595
  ```python
596
  [Complete working code]
597
+ ```
598
+
599
+ MUST INCLUDE:
600
+ - Complete API clients with error handling, retries, rate limiting
601
+ - Database schema with CREATE TABLE statements
602
+ - Data processing with real transformation logic
603
+ - Config management (.env handling)
604
+ - requirements.txt with ALL dependencies
605
+ - main.py entry point
606
+ - Comprehensive README
607
+
608
+ CODE QUALITY:
609
+ - Environment variables for secrets
610
+ - Error handling and logging
611
+ - Docstrings and comments
612
+ - Real business logic based on request
613
+ - RUNNABLE out of the box
614
+
615
+ SPECIFIC TO REQUEST:
616
+ - Use EXACT APIs mentioned (e.g., CricAPI, SportsRadar)
617
+ - Implement SPECIFIC algorithms (e.g., batting avg, strike rate)
618
+ - Create EXACT database tables needed
619
+ - Process SPECIFIC data formats
620
+ - NO placeholders like "# TODO"
621
+ - NO dummy data - implement REAL logic
622
+ - NO documentation-style code - PRODUCTION code only
623
+
624
+ Format each file:
625
+ ### path/to/file.py
626
+ ```
627
+ # Complete code here
628
+ ```
629
+
630
+ Generate complete repository:"""
631
+
632
+ response = llm.invoke(repo_prompt)
633
+ llm_text = getattr(response, "content", "") or ""
634
+
635
+ # Parse files from response
636
+ repo_files = {}
637
+
638
+ # Extract with ### headers
639
+ file_pattern = r"###\s+([\w\/_\-\.]+)\s*\n```(?:\w+)?\s*\n(.*?)\n```"
640
+ matches = re.finditer(file_pattern, llm_text, re.DOTALL)
641
+
642
+ for match in matches:
643
+ filepath = match.group(1).strip()
644
+ content = match.group(2).strip()
645
+ repo_files[filepath] = content
646
+
647
+ # Fallback: extract code blocks
648
+ if not repo_files:
649
+ code_blocks = re.findall(r"```(?:python|sql)?\s*\n(.*?)\n```", llm_text, re.DOTALL)
650
+ if code_blocks:
651
+ for i, block in enumerate(code_blocks):
652
+ if len(block) > 50: # Skip tiny blocks
653
+ repo_files[f"module_{i}.py"] = block
654
+
655
+ # Add README if missing
656
+ if not any('README' in f.upper() for f in repo_files):
657
+ repo_files["README.md"] = f"""# Generated Application
658
 
659
+ ## Overview
660
+ {goal}
661
 
662
+ ## Files
663
+ {chr(10).join(f'- {f}' for f in sorted(repo_files.keys()))}
664
 
665
+ ## Setup
666
+ 1. `pip install -r requirements.txt`
667
+ 2. Copy `.env.example` to `.env` and configure
668
+ 3. Run: `python main.py`
669
+ """
670
 
671
+ # Add requirements.txt if missing
672
+ if "requirements.txt" not in repo_files:
673
+ all_code = " ".join(repo_files.values()).lower()
674
+ deps = []
675
+ if 'requests' in all_code: deps.append('requests')
676
+ if 'pandas' in all_code: deps.append('pandas')
677
+ if 'numpy' in all_code: deps.append('numpy')
678
+ if 'sqlalchemy' in all_code: deps.append('sqlalchemy')
679
+ if 'postgresql' in all_code or 'psycopg2' in all_code: deps.append('psycopg2-binary')
680
+ if 'flask' in all_code: deps.append('flask')
681
+ if 'fastapi' in all_code:
682
+ deps.append('fastapi')
683
+ deps.append('uvicorn')
684
+ if 'dotenv' in all_code: deps.append('python-dotenv')
685
+
686
+ repo_files["requirements.txt"] = "\n".join(deps) if deps else "# Dependencies"
687
+
688
+ # Add .env.example if missing
689
+ if ".env.example" not in repo_files:
690
+ repo_files[".env.example"] = """# Configuration
691
+ API_KEY=your_key_here
692
+ DATABASE_URL=postgresql://user:pass@localhost/db
693
+ DEBUG=False
694
+ """
695
 
696
+ # Add main.py if missing
697
+ if not any('main.py' in f for f in repo_files):
698
+ repo_files["main.py"] = """#!/usr/bin/env python3
699
+ import os
700
+ from dotenv import load_dotenv
701
 
702
+ load_dotenv()
703
 
704
+ def main():
705
+ print("Application starting...")
706
+ # Add your logic here
707
+ pass
708
 
709
+ if __name__ == "__main__":
710
+ main()
711
+ """
712
 
713
+ # Build zip
714
+ zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=OUT_DIR)
715
+
716
+ results = {
717
+ "success": True,
718
+ "paths": {"repo_zip": sanitize_path(zip_path)},
719
+ "files_created": len(repo_files),
720
+ "context_used": len(full_context)
721
+ }
722
+
723
+ return {
724
+ "experimentCode": None,
725
+ "experimentResults": results,
726
+ "execution_path": path,
727
+ **add_status_update("Experimenter", f"Repository created ({len(repo_files)} files)")
728
+ }
729
 
730
  # OTHER ARTIFACT TYPES
731
  enhanced_prompt = f"""Create HIGH-QUALITY {exp_type} artifact.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
+ {full_context}
734
 
735
+ GOAL: {goal}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
+ REQUIREMENTS:
738
+ - Use ALL specific details from request
739
+ - PRODUCTION-READY, COMPLETE content (NO placeholders)
740
+ - ACTUAL data, REALISTIC examples, WORKING code
741
+ - For notebooks: markdown + executable code + visualizations
742
+ - For scripts: error handling + docs + real logic
743
+ - For documents: substantive detailed content
744
 
745
+ Generate complete content for '{exp_type}' with proper code fences."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
 
 
747
  response = llm.invoke(enhanced_prompt)
748
  llm_text = getattr(response, "content", "") or ""
749
  results = {"success": False, "paths": {}, "stderr": "", "stdout": "", "context_used": len(full_context)}
 
752
  if exp_type == 'notebook':
753
  nb_path = write_notebook_from_text(llm_text, out_dir=OUT_DIR)
754
  results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
755
+ return {
756
+ "experimentCode": None,
757
+ "experimentResults": results,
758
+ "execution_path": path,
759
+ **add_status_update("Experimenter", "Notebook created")
760
+ }
761
 
762
  elif exp_type == 'excel':
763
  excel_path = write_excel_from_tables(llm_text, out_dir=OUT_DIR)
764
  results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
765
+ return {
766
+ "experimentCode": None,
767
+ "experimentResults": results,
768
+ "execution_path": path,
769
+ **add_status_update("Experimenter", "Excel created")
770
+ }
771
 
772
  elif exp_type == 'word':
773
  docx_path = write_docx_from_text(llm_text, out_dir=OUT_DIR)
774
  results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
775
+ return {
776
+ "experimentCode": None,
777
+ "experimentResults": results,
778
+ "execution_path": path,
779
+ **add_status_update("Experimenter", "Word document created")
780
+ }
781
 
782
  elif exp_type == 'pdf':
783
  pdf_path = write_pdf_from_text(llm_text, out_dir=OUT_DIR)
784
  results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
785
+ return {
786
+ "experimentCode": None,
787
+ "experimentResults": results,
788
+ "execution_path": path,
789
+ **add_status_update("Experimenter", "PDF created")
790
+ }
791
 
792
  elif exp_type == 'script':
793
  lang_hint = pm.get('experiment_language') or "python"
 
809
  "stdout": exec_results.get("stdout",""),
810
  "stderr": exec_results.get("stderr","")
811
  })
812
+ return {
813
+ "experimentCode": code_text,
814
+ "experimentResults": results,
815
+ "execution_path": path,
816
+ **add_status_update("Experimenter", "Script created")
817
+ }
818
 
819
  else:
820
  fallback = write_docx_from_text(llm_text, out_dir=OUT_DIR)
821
  results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
822
+ return {
823
+ "experimentCode": None,
824
+ "experimentResults": results,
825
+ "execution_path": path,
826
+ **add_status_update("Experimenter", "Document created")
827
+ }
828
 
829
  except Exception as e:
830
  log.error(f"Experimenter failed: {e}")
831
  results.update({"success": False, "stderr": str(e)})
832
+ return {
833
+ "experimentCode": None,
834
+ "experimentResults": results,
835
+ "execution_path": path,
836
+ **add_status_update("Experimenter", f"Error: {str(e)}")
837
+ }
838
+
839
  def run_synthesis_agent(state: AgentState):
840
  log.info("--- SYNTHESIS ---")
841
  _state = state or {}
 
874
  full_context = "\n".join(synthesis_context)
875
 
876
  synthesis_prompt = f"""Create FINAL RESPONSE after executing user's request.
 
 
 
 
 
877
 
878
+ {full_context}
 
 
 
 
 
 
 
 
879
 
880
+ Create comprehensive response that:
881
+ - Directly addresses original request
882
+ - Explains what was accomplished and HOW
883
+ - References specific artifacts and explains PURPOSE
884
+ - Provides context on how to USE deliverables
885
+ - Highlights KEY INSIGHTS
886
+ - Suggests NEXT STEPS if relevant
887
+ - Be SPECIFIC about what was created."""
888
 
889
  response = llm.invoke(synthesis_prompt)
890
  final_text = getattr(response, "content", "") or ""
 
892
  if artifact_message:
893
  final_text = final_text + "\n\n---\n" + artifact_message
894
 
895
+ return {
896
+ "draftResponse": final_text,
897
+ "execution_path": path,
898
+ **add_status_update("Synthesis", "Response synthesized")
899
+ }
900
 
901
  def run_qa_agent(state: AgentState):
902
  log.info("--- QA ---")
 
912
  qa_context.append(f"\n=== ARTIFACTS ===\n{json.dumps(state.get('experimentResults', {}).get('paths', {}), indent=2)}")
913
 
914
  prompt = f"""You are a QA reviewer. Review the draft response against the user's objective.
 
915
 
916
+ {chr(10).join(qa_context)}
917
 
918
+ Review Instructions:
919
+ - Does the draft and its artifacts COMPLETELY satisfy ALL parts of the user's request?
920
+ - Is the quality of the work high?
921
+ - If this is a re-submission (rework cycle > 1), has the previous feedback been successfully addressed?
922
 
923
+ Response Format (required JSON or a single word 'APPROVED'):
924
 
925
+ Either return EXACTLY the single word:
926
+ APPROVED
927
 
928
+ Or return JSON like:
929
+ {{
930
+ "approved": false,
931
+ "feedback": "Specific, actionable items to fix (bullet list or numbered).",
932
+ "required_changes": ["..."]
933
+ }}
934
+ """
 
 
 
 
 
935
 
936
  try:
937
  response = llm.invoke(prompt)
938
  content = getattr(response, "content", "") or ""
939
  except Exception as e:
940
  log.exception("QA LLM call failed: %s", e)
941
+ return {
942
+ "approved": False,
943
+ "qaFeedback": "QA LLM failed; manual review required.",
944
+ "execution_path": path,
945
+ **add_status_update("QA", "QA failed")
946
+ }
947
 
948
  # If LLM returned APPROVED word, treat as approved
949
  if "APPROVED" in content.strip().upper() and len(content.strip()) <= 20:
950
+ return {
951
+ "approved": True,
952
+ "qaFeedback": None,
953
+ "execution_path": path,
954
+ **add_status_update("QA", "Approved")
955
+ }
956
 
957
  # Else try JSON parse
958
  parsed = parse_json_from_llm(content)
 
964
  feedback = "\n".join([str(x) for x in feedback])
965
  elif not isinstance(feedback, str):
966
  feedback = str(feedback)
967
+ return {
968
+ "approved": approved,
969
+ "qaFeedback": feedback if not approved else None,
970
+ "execution_path": path,
971
+ **add_status_update("QA", "QA completed")
972
+ }
973
+
974
  # Fallback: return raw text as feedback (not approved)
975
  safe_feedback = content.strip()[:2000] or "QA produced no actionable output."
976
+ return {
977
+ "approved": False,
978
+ "qaFeedback": safe_feedback,
979
+ "execution_path": path,
980
+ **add_status_update("QA", "QA needs rework")
981
+ }
982
 
983
  def run_archivist_agent(state: AgentState):
984
  log.info("--- ARCHIVIST ---")
 
988
  response = llm.invoke(summary_prompt)
989
  memory_manager.add_to_memory(getattr(response,"content",""), {"objective": state.get('coreObjectivePrompt')})
990
 
991
+ return {
992
+ "execution_path": path,
993
+ **add_status_update("Archivist", "Saved to memory")
994
+ }
995
 
996
  def run_disclaimer_agent(state: AgentState):
997
  log.warning("--- DISCLAIMER ---")
 
1001
  disclaimer = f"**DISCLAIMER: {reason} Draft may be incomplete.**\n\n---\n\n"
1002
  final_response = disclaimer + state.get('draftResponse', "No response")
1003
 
1004
+ return {
1005
+ "draftResponse": final_response,
1006
+ "execution_path": path,
1007
+ **add_status_update("Disclaimer", reason)
1008
+ }
1009
 
1010
  def should_continue(state: AgentState):
1011
  # Budget check first
 
1027
  # Default: return pm_agent so planner will create next plan
1028
  return "pm_agent"
1029
 
 
1030
  def should_run_experiment(state: AgentState):
1031
  pm = state.get('pmPlan', {}) or {}
1032
  return "experimenter_agent" if pm.get('experiment_needed') else "synthesis_agent"
 
1064
 
1065
  main_workflow.add_conditional_edges("pm_agent", should_run_experiment)
1066
  main_workflow.add_conditional_edges("qa_agent", should_continue, {
1067
+ "archivist_agent": "archivist_agent",
1068
+ "pm_agent": "pm_agent",
1069
+ "disclaimer_agent": "disclaimer_agent"
1070
  })
1071
 
1072
+ main_app = main_workflow.compile()