JatinAutonomousLabs commited on
Commit
f6d1866
·
verified ·
1 Parent(s): 8c0735d

Update graph.py

Browse files
Files changed (1) hide show
  1. graph.py +314 -343
graph.py CHANGED
@@ -1,4 +1,4 @@
1
- # graph.py (Enhanced with better context passing)
2
  import json
3
  import re
4
  import math
@@ -8,6 +8,7 @@ import shutil
8
  import zipfile
9
  import operator
10
  from typing import TypedDict, List, Dict, Optional, Annotated
 
11
  from langchain_openai import ChatOpenAI
12
  from langgraph.graph import StateGraph, END
13
  from memory_manager import memory_manager
@@ -22,7 +23,7 @@ from docx import Document
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")
@@ -51,7 +52,7 @@ def ensure_int(state, key, default=0):
51
  def sanitize_path(path: str) -> str:
52
  return os.path.abspath(path)
53
 
54
- # --- Setup & constants ---
55
  setup_logging()
56
  log = get_logger(__name__)
57
  INITIAL_MAX_REWORK_CYCLES = 3
@@ -59,7 +60,7 @@ GPT4O_INPUT_COST_PER_1K_TOKENS = 0.005
59
  GPT4O_OUTPUT_COST_PER_1K_TOKENS = 0.015
60
  AVG_TOKENS_PER_CALL = 2.0
61
 
62
- # --- AgentState ---
63
  class AgentState(TypedDict):
64
  userInput: str
65
  chatHistory: List[str]
@@ -76,7 +77,7 @@ class AgentState(TypedDict):
76
  max_loops: int
77
  status_update: str
78
 
79
- # --- LLM & parsing ---
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]:
@@ -94,10 +95,10 @@ def parse_json_from_llm(llm_output: str) -> Optional[dict]:
94
  json_str = llm_output[start:end+1]
95
  return json.loads(json_str)
96
  except Exception as e:
97
- log.error(f"JSON parsing failed. Error: {e}. Raw head: {llm_output[:300]}")
98
  return None
99
 
100
- # --- Artifact detection & normalization ---
101
  KNOWN_ARTIFACT_TYPES = {"notebook","excel","word","pdf","image","repo","script"}
102
 
103
  def detect_requested_output_types(text: str) -> Dict:
@@ -105,19 +106,17 @@ def detect_requested_output_types(text: str) -> Dict:
105
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
106
  t = text.lower()
107
  if any(k in t for k in ["jupyter notebook", "jupyter", "notebook", "ipynb"]):
108
- return {"requires_artifact": True, "artifact_type": "notebook", "artifact_hint": "jupyter notebook (.ipynb)"}
109
- if any(k in t for k in ["excel", ".xlsx", "spreadsheet", "csv", "sheet"]):
110
- return {"requires_artifact": True, "artifact_type": "excel", "artifact_hint": "Excel/CSV file"}
111
- if any(k in t for k in ["word document", ".docx", "docx", "word file"]):
112
- return {"requires_artifact": True, "artifact_type": "word", "artifact_hint": "Word document (.docx)"}
113
  if any(k in t for k in ["pdf", "pdf file"]):
114
  return {"requires_artifact": True, "artifact_type": "pdf", "artifact_hint": "PDF document"}
115
- if any(k in t for k in ["image", "plot", "chart", "png", "jpg", "jpeg"]):
116
- return {"requires_artifact": True, "artifact_type": "image", "artifact_hint": "image/plot"}
117
- if any(k in t for k in ["repo", "repository", "app repo", "dockerfile", "requirements.txt", "package.json"]):
118
- return {"requires_artifact": True, "artifact_type": "repo", "artifact_hint": "application repository (zip)"}
119
- if any(k in t for k in [".py", "python script", "r script", ".R", ".r", "java", ".java", "javascript", ".js"]):
120
- return {"requires_artifact": True, "artifact_type": "script", "artifact_hint": "language script (py/r/java/js/etc.)"}
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:
@@ -129,20 +128,20 @@ def normalize_experiment_type(exp_type: Optional[str], goal_text: str) -> str:
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:
133
  return "excel"
134
  if "word" in s or "docx" in s:
135
  return "word"
136
  if "pdf" in s:
137
  return "pdf"
138
- if "repo" in s or "repository" in s or "app" 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)
@@ -159,7 +158,7 @@ def write_notebook_from_text(llm_text: str, out_dir: Optional[str]=None) -> str:
159
  if i < len(code_blocks) and code_blocks[i].strip():
160
  cells.append(new_code_cell(code_blocks[i].strip()))
161
  if not cells:
162
- cells = [new_markdown_cell("# Notebook\n\nNo content parsed from LLM output.")]
163
  nb['cells'] = cells
164
  uid = uuid.uuid4().hex[:10]
165
  filename = os.path.join(out_dir, f"generated_notebook_{uid}.ipynb")
@@ -172,16 +171,14 @@ def write_script(code_text: str, language_hint: Optional[str]=None, out_dir: Opt
172
  ext = ".txt"
173
  if language_hint:
174
  l = language_hint.lower()
175
- if "python" in l or ".py" in l:
176
  ext = ".py"
177
- elif l == "r" or l == ".r":
178
  ext = ".R"
179
- elif "java" in l or ".java" in l:
180
  ext = ".java"
181
- elif "javascript" in l or ".js" in l:
182
  ext = ".js"
183
- elif "bash" in l or "sh" in l:
184
- ext = ".sh"
185
  uid = uuid.uuid4().hex[:10]
186
  filename = os.path.join(out_dir, f"generated_script_{uid}{ext}")
187
  with open(filename, "w", encoding="utf-8") as f:
@@ -214,7 +211,7 @@ def write_excel_from_tables(maybe_table_text: str, out_dir: Optional[str]=None)
214
  else:
215
  df = pd.DataFrame({"content":[str(maybe_table_text)]})
216
  except Exception:
217
- if "," in maybe_table_text or "\t" in maybe_table_text:
218
  from io import StringIO
219
  df = pd.read_csv(StringIO(maybe_table_text))
220
  else:
@@ -223,7 +220,7 @@ def write_excel_from_tables(maybe_table_text: str, out_dir: Optional[str]=None)
223
  return filename
224
  except Exception as e:
225
  log.error(f"Excel creation failed: {e}")
226
- return write_docx_from_text(f"Failed to create excel. Error: {e}\n\nOriginal:\n{maybe_table_text}", out_dir=out_dir)
227
 
228
  def write_pdf_from_text(text: str, out_dir: Optional[str]=None) -> str:
229
  out_dir = out_dir or OUT_DIR
@@ -241,7 +238,7 @@ def write_pdf_from_text(text: str, out_dir: Optional[str]=None) -> str:
241
  return filename
242
  except Exception as e:
243
  log.error(f"PDF creation failed: {e}")
244
- return write_docx_from_text(f"Failed to create PDF. Error: {e}\n\nOriginal:\n{text}", out_dir=out_dir)
245
 
246
  def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out_dir: Optional[str]=None) -> str:
247
  out_dir = out_dir or OUT_DIR
@@ -249,14 +246,17 @@ def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out
249
  uid = uuid.uuid4().hex[:8]
250
  repo_dir = os.path.join(out_dir, f"{repo_name}_{uid}")
251
  os.makedirs(repo_dir, exist_ok=True)
 
252
  for rel_path, content in files_map.items():
253
  dest = os.path.join(repo_dir, rel_path)
254
  os.makedirs(os.path.dirname(dest), exist_ok=True)
 
255
  if isinstance(content, str) and os.path.exists(content):
256
  shutil.copyfile(content, dest)
257
  else:
258
  with open(dest, "w", encoding="utf-8") as fh:
259
  fh.write(str(content))
 
260
  zip_path = os.path.join(out_dir, f"{repo_name}_{uid}.zip")
261
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
262
  for root, _, files in os.walk(repo_dir):
@@ -264,283 +264,347 @@ def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out
264
  full = os.path.join(root, f)
265
  arc = os.path.relpath(full, repo_dir)
266
  zf.write(full, arc)
 
267
  return zip_path
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 ""
275
  if 'greeting' in content.lower():
276
- log.info("Triage result: Simple Greeting.")
277
- return {"draftResponse": "Hello! How can I help you today?", "execution_path": ["Triage Agent"], "status_update": "Responding to greeting."}
278
- else:
279
- log.info("Triage result: Complex Task.")
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. "
287
- f"User Request: \"{state.get('userInput','')}\". Respond in JSON with keys: 'plan' (list of strings), 'estimated_llm_calls_per_loop' (integer)."
288
- )
289
  response = llm.invoke(prompt)
290
  plan_data = parse_json_from_llm(getattr(response, "content", "") or "")
291
  if not plan_data:
292
- return {"pmPlan": {"error": "Failed to create a valid plan."}, "execution_path": path, "status_update": "Error: Could not create a plan."}
293
- calls_per_loop = plan_data.get('estimated_llm_calls_per_loop', 3)
294
- cost_per_loop = (calls_per_loop * AVG_TOKENS_PER_CALL) * ((GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2)
295
- estimated_cost = cost_per_loop * (INITIAL_MAX_REWORK_CYCLES + 1)
296
  plan_data['max_loops_initial'] = INITIAL_MAX_REWORK_CYCLES
297
- plan_data['estimated_cost_usd'] = round(estimated_cost, 2)
298
  plan_data['cost_per_loop_usd'] = max(0.01, round(cost_per_loop, 3))
299
- detection = detect_requested_output_types(state.get('userInput','') or state.get('coreObjectivePrompt','') or '')
 
300
  if detection.get('requires_artifact'):
301
  plan_data.setdefault('experiment_needed', True)
302
  plan_data.setdefault('experiment_type', detection.get('artifact_type'))
303
- plan_data.setdefault('experiment_goal', f"Produce an artifact: {detection.get('artifact_hint')}. {state.get('userInput','')}")
304
- log.info(f"Pre-flight Estimate: {plan_data}")
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:
312
- context = "\n".join([f"Memory: {mem.page_content}" for mem in relevant_mems])
313
- log.info(f"Found {len(relevant_mems)} relevant memories.")
314
- else:
315
- context = "No relevant memories found."
316
- log.info(context)
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)
324
  core_obj = getattr(response, "content", "") or ""
325
- detection = detect_requested_output_types(core_obj or state.get('userInput',''))
326
- extras = {}
327
- if detection.get('requires_artifact'):
328
- extras['artifact_detection'] = detection
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:
426
  return blocks
427
- blocks = re.findall(r"```(?:\w+)?\s*(.*?)\s*```", text, re.DOTALL)
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"):
@@ -550,74 +614,37 @@ Use fenced code blocks with language identifiers where appropriate.
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"
568
- repo_files["README.md"] = readme
569
- nb_path = write_notebook_from_text(llm_text, out_dir=out_dir)
570
- repo_files["analysis.ipynb"] = nb_path
571
- reqs = "nbformat\npandas\nopenpyxl\npython-docx\nreportlab"
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}")
594
  results.update({"success": False, "stderr": str(e)})
595
- return {
596
- "experimentCode": None,
597
- "experimentResults": results,
598
- "execution_path": path,
599
- "status_update": "Error: Experimenter failed."
600
- }
601
 
602
  def run_synthesis_agent(state: AgentState):
603
- log.info("--- SYNTHESIS AGENT ---")
604
  _state = state or {}
605
- path = ensure_list(_state, 'execution_path') + ["Synthesis Agent"]
606
 
607
  exp_results = _state.get('experimentResults')
608
  pm_plan = _state.get('pmPlan', {}) or {}
609
 
610
- # BUILD COMPREHENSIVE SYNTHESIS CONTEXT
611
  synthesis_context = [
612
- f"=== ORIGINAL USER REQUEST ===",
613
- f"{_state.get('userInput', '')}",
614
- f"\n=== CORE OBJECTIVE ===",
615
- f"{_state.get('coreObjectivePrompt', '')}",
616
- f"\n=== EXECUTION PLAN ===",
617
- f"{json.dumps(pm_plan.get('plan_steps', []), indent=2)}",
618
  ]
619
 
620
- # Extract artifact details
621
  artifact_details = []
622
  artifact_message = ""
623
 
@@ -630,150 +657,94 @@ def run_synthesis_agent(state: AgentState):
630
  artifact_lines.append(f"- **{artifact_type.title()}**: `{os.path.basename(artifact_path)}`")
631
  artifact_details.append(f"{artifact_type}: {artifact_path}")
632
 
633
- artifact_message = "\n\n**📦 Artifacts Generated:**\n" + "\n".join(artifact_lines)
634
- synthesis_context.append(f"\n=== ARTIFACTS CREATED ===")
635
- synthesis_context.append("\n".join(artifact_details))
636
 
637
  if exp_results.get('stdout'):
638
- synthesis_context.append(f"\n=== EXECUTION OUTPUT ===")
639
- synthesis_context.append(exp_results.get('stdout', ''))
640
 
641
- if exp_results.get('stderr') and not exp_results.get('success'):
642
- synthesis_context.append(f"\n=== EXECUTION ERRORS ===")
643
- synthesis_context.append(exp_results.get('stderr', ''))
644
 
645
  full_context = "\n".join(synthesis_context)
646
 
647
- # ENHANCED SYNTHESIS PROMPT
648
- synthesis_prompt = f"""You are creating the FINAL RESPONSE after executing a user's request.
649
 
650
  {full_context}
651
 
652
- Create a comprehensive, professional response that:
653
- 1. Directly addresses the user's original request
654
- 2. Explains what was accomplished and HOW it was done
655
- 3. References specific artifacts created and explains their PURPOSE
656
- 4. Provides context on how to USE/ACCESS the deliverables
657
- 5. Highlights KEY INSIGHTS or findings
658
  6. Suggests NEXT STEPS if relevant
659
 
660
- TONE: Professional, clear, helpful. Be SPECIFIC about what was created.
661
 
662
- Write the final response:"""
663
-
664
  response = llm.invoke(synthesis_prompt)
665
  final_text = getattr(response, "content", "") or ""
666
 
667
  if artifact_message:
668
  final_text = final_text + "\n\n---\n" + artifact_message
669
 
670
- return {
671
- "draftResponse": final_text,
672
- "execution_path": path,
673
- "status_update": "Final response synthesized."
674
- }
675
 
676
  def run_qa_agent(state: AgentState):
677
- log.info("--- QA AGENT ---")
678
- path = ensure_list(state, 'execution_path') + ["QA Agent"]
679
 
680
- # Enhanced QA with context awareness
681
  qa_context = [
682
- f"=== ORIGINAL REQUEST ===",
683
- f"{state.get('userInput', '')}",
684
- f"\n=== CORE OBJECTIVE ===",
685
- f"{state.get('coreObjectivePrompt', '')}",
686
- f"\n=== DRAFT RESPONSE ===",
687
- f"{state.get('draftResponse', '')}",
688
  ]
689
 
690
  if state.get('experimentResults'):
691
- qa_context.append(f"\n=== ARTIFACTS ===")
692
- qa_context.append(json.dumps(state.get('experimentResults', {}).get('paths', {}), indent=2))
693
 
694
- full_qa_context = "\n".join(qa_context)
695
-
696
- prompt = f"""Review the draft response against the core objective.
697
 
698
- {full_qa_context}
699
 
700
  Respond ONLY with:
701
- - 'APPROVED' if the response fully addresses the request with quality and specificity
702
- - OR provide SPECIFIC, ACTIONABLE feedback for improvement
703
 
704
- Your response:"""
705
-
706
  response = llm.invoke(prompt)
707
  content = getattr(response, "content", "") or ""
708
 
709
  if "APPROVED" in content.upper():
710
- return {
711
- "approved": True,
712
- "qaFeedback": None,
713
- "execution_path": path,
714
- "status_update": "Response approved!"
715
- }
716
  else:
717
- return {
718
- "approved": False,
719
- "qaFeedback": content or "No specific feedback.",
720
- "execution_path": path,
721
- "status_update": "Response needs improvement. Reworking..."
722
- }
723
 
724
  def run_archivist_agent(state: AgentState):
725
- log.info("--- ARCHIVIST AGENT ---")
726
- path = ensure_list(state, 'execution_path') + ["Archivist Agent"]
727
-
728
- summary_prompt = (
729
- f"Create a concise summary of this successful task for long-term memory.\n\n"
730
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\n"
731
- f"Final Response: {state.get('draftResponse')}\n\n"
732
- f"Memory Summary:"
733
- )
734
 
 
735
  response = llm.invoke(summary_prompt)
736
- memory_manager.add_to_memory(
737
- getattr(response,"content",""),
738
- {"objective": state.get('coreObjectivePrompt')}
739
- )
740
 
741
- return {
742
- "execution_path": path,
743
- "status_update": "Saving key learnings..."
744
- }
745
 
746
  def run_disclaimer_agent(state: AgentState):
747
- log.warning("--- DISCLAIMER AGENT ---")
748
- path = ensure_list(state, 'execution_path') + ["Disclaimer Agent"]
749
-
750
- disclaimer = (
751
- "**DISCLAIMER: Budget limit reached. The following is the best available draft and may be incomplete.**\n\n"
752
- "---\n\n"
753
- )
754
 
755
- final_response = disclaimer + state.get('draftResponse', "No response was generated.")
 
756
 
757
- return {
758
- "draftResponse": final_response,
759
- "execution_path": path,
760
- "status_update": "Budget limit reached. Preparing final draft..."
761
- }
762
 
763
- # --- Decision functions ---
764
  def should_continue(state: AgentState):
765
- log.info("--- DECISION: QA approved? ---")
766
-
767
  if state.get("approved"):
768
- log.info("Routing to: Archivist (Success Path)")
769
  return "archivist_agent"
770
-
771
  if ensure_int(state, "rework_cycles", 0) > ensure_int(state, "max_loops", 0):
772
- log.error(f"BUDGET LIMIT REACHED after {ensure_int(state, 'rework_cycles', 0)-1} cycles.")
773
  return "disclaimer_agent"
774
- else:
775
- log.info("Routing to: PM Agent for rework")
776
- return "pm_agent"
777
 
778
  def should_run_experiment(state: AgentState):
779
  pm = state.get('pmPlan', {}) or {}
 
1
+ # graph.py - Complete enhanced version with proper repo generation
2
  import json
3
  import re
4
  import math
 
8
  import zipfile
9
  import operator
10
  from typing import TypedDict, List, Dict, Optional, Annotated
11
+ from datetime import datetime
12
  from langchain_openai import ChatOpenAI
13
  from langgraph.graph import StateGraph, END
14
  from memory_manager import memory_manager
 
23
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
24
  from reportlab.lib.styles import getSampleStyleSheet
25
 
26
+ # --- Configuration ---
27
  OUT_DIR = os.environ.get("OUT_DIR", "/tmp")
28
  os.makedirs(OUT_DIR, exist_ok=True)
29
  EXPORTS_DIR = os.path.join(OUT_DIR, "exports")
 
52
  def sanitize_path(path: str) -> str:
53
  return os.path.abspath(path)
54
 
55
+ # --- Setup ---
56
  setup_logging()
57
  log = get_logger(__name__)
58
  INITIAL_MAX_REWORK_CYCLES = 3
 
60
  GPT4O_OUTPUT_COST_PER_1K_TOKENS = 0.015
61
  AVG_TOKENS_PER_CALL = 2.0
62
 
63
+ # --- State ---
64
  class AgentState(TypedDict):
65
  userInput: str
66
  chatHistory: List[str]
 
77
  max_loops: int
78
  status_update: str
79
 
80
+ # --- LLM ---
81
  llm = ChatOpenAI(model="gpt-4o", temperature=0.1, max_retries=3, request_timeout=60)
82
 
83
  def parse_json_from_llm(llm_output: str) -> Optional[dict]:
 
95
  json_str = llm_output[start:end+1]
96
  return json.loads(json_str)
97
  except Exception as e:
98
+ log.error(f"JSON parsing failed: {e}")
99
  return None
100
 
101
+ # --- Artifact detection ---
102
  KNOWN_ARTIFACT_TYPES = {"notebook","excel","word","pdf","image","repo","script"}
103
 
104
  def detect_requested_output_types(text: str) -> Dict:
 
106
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
107
  t = text.lower()
108
  if any(k in t for k in ["jupyter notebook", "jupyter", "notebook", "ipynb"]):
109
+ return {"requires_artifact": True, "artifact_type": "notebook", "artifact_hint": "jupyter notebook"}
110
+ if any(k in t for k in ["excel", ".xlsx", "spreadsheet", "csv"]):
111
+ return {"requires_artifact": True, "artifact_type": "excel", "artifact_hint": "Excel file"}
112
+ if any(k in t for k in ["word document", ".docx", "docx"]):
113
+ return {"requires_artifact": True, "artifact_type": "word", "artifact_hint": "Word document"}
114
  if any(k in t for k in ["pdf", "pdf file"]):
115
  return {"requires_artifact": True, "artifact_type": "pdf", "artifact_hint": "PDF document"}
116
+ if any(k in t for k in ["repo", "repository", "app repo", "backend", "codebase"]):
117
+ return {"requires_artifact": True, "artifact_type": "repo", "artifact_hint": "application repository"}
118
+ if any(k in t for k in [".py", "python script", "script"]):
119
+ return {"requires_artifact": True, "artifact_type": "script", "artifact_hint": "Python script"}
 
 
120
  return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
121
 
122
  def normalize_experiment_type(exp_type: Optional[str], goal_text: str) -> str:
 
128
  return s
129
  if "notebook" in s or "ipynb" in s:
130
  return "notebook"
131
+ if "excel" in s or "xlsx" in s:
132
  return "excel"
133
  if "word" in s or "docx" in s:
134
  return "word"
135
  if "pdf" in s:
136
  return "pdf"
137
+ if "repo" in s or "repository" in s or "backend" in s:
138
  return "repo"
139
+ if "script" in s or "python" in s:
140
  return "script"
141
  detection = detect_requested_output_types(goal_text or "")
142
  return detection.get("artifact_type") or "word"
143
 
144
+ # --- Artifact builders ---
145
  def write_notebook_from_text(llm_text: str, out_dir: Optional[str]=None) -> str:
146
  out_dir = out_dir or OUT_DIR
147
  os.makedirs(out_dir, exist_ok=True)
 
158
  if i < len(code_blocks) and code_blocks[i].strip():
159
  cells.append(new_code_cell(code_blocks[i].strip()))
160
  if not cells:
161
+ cells = [new_markdown_cell("# Notebook\n\nNo content generated.")]
162
  nb['cells'] = cells
163
  uid = uuid.uuid4().hex[:10]
164
  filename = os.path.join(out_dir, f"generated_notebook_{uid}.ipynb")
 
171
  ext = ".txt"
172
  if language_hint:
173
  l = language_hint.lower()
174
+ if "python" in l:
175
  ext = ".py"
176
+ elif "r" in l:
177
  ext = ".R"
178
+ elif "java" in l:
179
  ext = ".java"
180
+ elif "javascript" in l:
181
  ext = ".js"
 
 
182
  uid = uuid.uuid4().hex[:10]
183
  filename = os.path.join(out_dir, f"generated_script_{uid}{ext}")
184
  with open(filename, "w", encoding="utf-8") as f:
 
211
  else:
212
  df = pd.DataFrame({"content":[str(maybe_table_text)]})
213
  except Exception:
214
+ if "," in maybe_table_text:
215
  from io import StringIO
216
  df = pd.read_csv(StringIO(maybe_table_text))
217
  else:
 
220
  return filename
221
  except Exception as e:
222
  log.error(f"Excel creation failed: {e}")
223
+ return write_docx_from_text(f"Excel error: {e}\n\n{maybe_table_text}", out_dir=out_dir)
224
 
225
  def write_pdf_from_text(text: str, out_dir: Optional[str]=None) -> str:
226
  out_dir = out_dir or OUT_DIR
 
238
  return filename
239
  except Exception as e:
240
  log.error(f"PDF creation failed: {e}")
241
+ return write_docx_from_text(f"PDF error: {e}\n\n{text}", out_dir=out_dir)
242
 
243
  def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out_dir: Optional[str]=None) -> str:
244
  out_dir = out_dir or OUT_DIR
 
246
  uid = uuid.uuid4().hex[:8]
247
  repo_dir = os.path.join(out_dir, f"{repo_name}_{uid}")
248
  os.makedirs(repo_dir, exist_ok=True)
249
+
250
  for rel_path, content in files_map.items():
251
  dest = os.path.join(repo_dir, rel_path)
252
  os.makedirs(os.path.dirname(dest), exist_ok=True)
253
+
254
  if isinstance(content, str) and os.path.exists(content):
255
  shutil.copyfile(content, dest)
256
  else:
257
  with open(dest, "w", encoding="utf-8") as fh:
258
  fh.write(str(content))
259
+
260
  zip_path = os.path.join(out_dir, f"{repo_name}_{uid}.zip")
261
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
262
  for root, _, files in os.walk(repo_dir):
 
264
  full = os.path.join(root, f)
265
  arc = os.path.relpath(full, repo_dir)
266
  zf.write(full, arc)
267
+
268
  return zip_path
269
 
270
+ # --- Nodes ---
271
  def run_triage_agent(state: AgentState):
272
  log.info("--- TRIAGE ---")
273
+ prompt = f"Is this a greeting or a task? '{state.get('userInput','')}' Reply: 'greeting' or 'task'"
274
  response = llm.invoke(prompt)
275
  content = getattr(response, "content", "") or ""
276
  if 'greeting' in content.lower():
277
+ return {"draftResponse": "Hello! How can I help?", "execution_path": ["Triage"], "status_update": "Greeting"}
278
+ return {"execution_path": ["Triage"], "status_update": "Task detected"}
 
 
 
279
 
280
  def run_planner_agent(state: AgentState):
281
+ log.info("--- PLANNER ---")
282
+ path = ensure_list(state, 'execution_path') + ["Planner"]
283
+ prompt = f"Create a plan for: '{state.get('userInput','')}'. JSON with 'plan' (list), 'estimated_llm_calls_per_loop' (int)"
 
 
 
284
  response = llm.invoke(prompt)
285
  plan_data = parse_json_from_llm(getattr(response, "content", "") or "")
286
  if not plan_data:
287
+ return {"pmPlan": {"error": "Planning failed"}, "execution_path": path, "status_update": "Error"}
288
+
289
+ calls = plan_data.get('estimated_llm_calls_per_loop', 3)
290
+ cost_per_loop = (calls * AVG_TOKENS_PER_CALL) * ((GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2)
291
  plan_data['max_loops_initial'] = INITIAL_MAX_REWORK_CYCLES
292
+ plan_data['estimated_cost_usd'] = round(cost_per_loop * (INITIAL_MAX_REWORK_CYCLES + 1), 2)
293
  plan_data['cost_per_loop_usd'] = max(0.01, round(cost_per_loop, 3))
294
+
295
+ detection = detect_requested_output_types(state.get('userInput',''))
296
  if detection.get('requires_artifact'):
297
  plan_data.setdefault('experiment_needed', True)
298
  plan_data.setdefault('experiment_type', detection.get('artifact_type'))
299
+ plan_data.setdefault('experiment_goal', state.get('userInput',''))
300
+
301
+ return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan created"}
302
 
303
  def run_memory_retrieval(state: AgentState):
304
+ log.info("--- MEMORY ---")
305
+ path = ensure_list(state, 'execution_path') + ["Memory"]
306
+ mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
307
+ context = "\n".join([f"Memory: {m.page_content}" for m in mems]) if mems else "No memories"
308
+ return {"retrievedMemory": context, "execution_path": path, "status_update": "Memory retrieved"}
 
 
 
 
 
309
 
310
  def run_intent_agent(state: AgentState):
311
+ log.info("--- INTENT ---")
312
+ path = ensure_list(state, 'execution_path') + ["Intent"]
313
+ prompt = f"Refine into clear objective.\n\nMemory: {state.get('retrievedMemory')}\n\nRequest: {state.get('userInput','')}\n\nCore Objective:"
314
  response = llm.invoke(prompt)
315
  core_obj = getattr(response, "content", "") or ""
316
+ return {"coreObjectivePrompt": core_obj, "execution_path": path, "status_update": "Objective clarified"}
 
 
 
 
317
 
318
  def run_pm_agent(state: AgentState):
319
+ log.info("--- PM ---")
320
  current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
321
  max_loops_val = ensure_int(state, 'max_loops', 0)
322
+ path = ensure_list(state, 'execution_path') + ["PM"]
 
323
 
 
324
  context_parts = [
325
+ f"=== USER REQUEST ===\n{state.get('userInput', '')}",
326
+ f"\n=== OBJECTIVE ===\n{state.get('coreObjectivePrompt', '')}",
327
+ f"\n=== MEMORY ===\n{state.get('retrievedMemory', 'None')}",
 
 
 
328
  ]
329
 
330
  if state.get('qaFeedback'):
331
+ context_parts.append(f"\n=== QA FEEDBACK (MUST FIX) ===\n{state.get('qaFeedback')}")
332
+ context_parts.append(f"\n=== PREVIOUS PLAN ===\n{json.dumps(state.get('pmPlan', {}).get('plan_steps', []), indent=2)}")
 
 
333
 
334
  full_context = "\n".join(context_parts)
335
 
336
+ prompt = f"""Create DETAILED, EXECUTABLE plan.
 
337
 
338
  {full_context}
339
 
340
+ Each step must be SPECIFIC and ACTIONABLE:
341
  - State EXACTLY what will be created/analyzed
342
+ - Specify WHAT data/information will be used
343
+ - Define WHAT methods will be applied
344
 
345
+ JSON format:
346
  {{
347
+ "plan_steps": ["Specific step 1...", "Specific step 2..."],
 
 
 
 
348
  "experiment_needed": true/false,
349
  "experiment_type": "notebook|script|excel|word|pdf|repo",
350
+ "experiment_goal": "Detailed artifact description",
351
+ "key_requirements": ["Critical requirements"]
 
352
  }}
353
 
354
+ Be specific about using uploaded files, implementing algorithms, creating schemas."""
 
 
 
 
 
355
 
356
  response = llm.invoke(prompt)
357
  plan = parse_json_from_llm(getattr(response, "content", "") or "")
358
 
359
  if not plan:
 
360
  detection = detect_requested_output_types(state.get('userInput', ''))
361
  plan = {
362
+ "plan_steps": ["Analyze request", "Process information", "Create deliverable", "Review"],
 
 
 
 
 
363
  "experiment_needed": detection.get('requires_artifact', False),
364
  "experiment_type": detection.get('artifact_type', 'word'),
365
  "experiment_goal": state.get('coreObjectivePrompt', state.get('userInput', ''))
366
  }
367
 
368
+ exp_type = normalize_experiment_type(plan.get('experiment_type'), plan.get('experiment_goal',''))
369
+ plan['experiment_type'] = exp_type
 
 
370
 
371
  if plan.get('experiment_needed') and not plan.get('experiment_goal'):
372
+ plan['experiment_goal'] = state.get('userInput','')
 
 
373
 
374
+ return {"pmPlan": plan, "execution_path": path, "rework_cycles": current_cycles, "status_update": f"Plan created ({len(plan.get('plan_steps', []))} steps)"}
 
 
 
 
 
375
 
376
  def _extract_code_blocks(text: str, lang_hint: Optional[str]=None) -> List[str]:
377
  if lang_hint and "python" in (lang_hint or "").lower():
378
  blocks = re.findall(r"```python\s*(.*?)\s*```", text, re.DOTALL)
379
  if blocks:
380
  return blocks
381
+ return re.findall(r"```(?:\w+)?\s*(.*?)\s*```", text, re.DOTALL)
 
382
 
383
  def run_experimenter_agent(state: AgentState):
384
+ log.info("--- EXPERIMENTER ---")
385
+ path = ensure_list(state, 'execution_path') + ["Experimenter"]
386
  pm = state.get('pmPlan', {}) or {}
387
 
388
  if not pm.get('experiment_needed'):
389
+ return {"experimentCode": None, "experimentResults": None, "execution_path": path, "status_update": "No experiment needed"}
 
 
 
 
 
390
 
391
  exp_type = normalize_experiment_type(pm.get('experiment_type'), pm.get('experiment_goal',''))
392
+ goal = pm.get('experiment_goal', 'No goal')
393
 
394
+ # BUILD RICH CONTEXT
395
  context_parts = [
396
+ f"=== USER REQUEST ===\n{state.get('userInput', '')}",
397
+ f"\n=== OBJECTIVE ===\n{state.get('coreObjectivePrompt', '')}",
398
+ f"\n=== PLAN ===\n{json.dumps(pm.get('plan_steps', []), indent=2)}",
399
+ f"\n=== REQUIREMENTS ===\n{json.dumps(pm.get('key_requirements', []), indent=2)}",
 
 
 
 
400
  ]
401
 
402
  if state.get('retrievedMemory'):
403
+ context_parts.append(f"\n=== CONTEXT ===\n{state.get('retrievedMemory', '')}")
 
404
 
405
  if state.get('qaFeedback'):
406
+ context_parts.append(f"\n=== FEEDBACK TO ADDRESS ===\n{state.get('qaFeedback', '')}")
 
407
 
408
  full_context = "\n".join(context_parts)
409
 
410
+ # REPO REQUIRES SPECIAL HANDLING
411
+ if exp_type == 'repo':
412
+ repo_prompt = f"""Create COMPLETE, PRODUCTION-READY application repository.
413
 
414
  {full_context}
415
 
416
+ GOAL: {goal}
417
+
418
+ CRITICAL REQUIREMENTS:
419
+
420
+ 1. ACTUAL WORKING CODE - Not templates, not documentation, not examples. REAL production code.
421
+
422
+ 2. FILE STRUCTURE - Indicate each file clearly:
423
+ ### path/to/file.py
424
+ ```python
425
+ [Complete working code]
426
+ ```
427
+
428
+ 3. MUST INCLUDE:
429
+ - Complete API clients with error handling, retries, rate limiting
430
+ - Database schema with CREATE TABLE statements
431
+ - Data processing with real transformation logic
432
+ - Config management (.env handling)
433
+ - requirements.txt with ALL dependencies
434
+ - main.py entry point
435
+ - Comprehensive README
436
+
437
+ 4. CODE QUALITY:
438
+ - Environment variables for secrets
439
+ - Error handling and logging
440
+ - Docstrings and comments
441
+ - Real business logic based on request
442
+ - RUNNABLE out of the box
443
+
444
+ 5. SPECIFIC TO REQUEST:
445
+ - Use EXACT APIs mentioned (e.g., CricAPI, SportsRadar)
446
+ - Implement SPECIFIC algorithms (e.g., batting avg, strike rate)
447
+ - Create EXACT database tables needed
448
+ - Process SPECIFIC data formats
449
+
450
+ NO placeholders like "# TODO"
451
+ NO dummy data - implement REAL logic
452
+ NO documentation-style code - PRODUCTION code only
453
+
454
+ Format each file:
455
+ ### path/to/file.py
456
+ ```python
457
+ # Complete code here
458
+ ```
459
+
460
+ Generate complete repository:"""
461
+
462
+ response = llm.invoke(repo_prompt)
463
+ llm_text = getattr(response, "content", "") or ""
464
+
465
+ # Parse files from response
466
+ repo_files = {}
467
+
468
+ # Extract with ### headers
469
+ file_pattern = r"###\s+([\w\/_\-\.]+)\s*\n```(?:\w+)?\s*\n(.*?)\n```"
470
+ matches = re.finditer(file_pattern, llm_text, re.DOTALL)
471
+
472
+ for match in matches:
473
+ filepath = match.group(1).strip()
474
+ content = match.group(2).strip()
475
+ repo_files[filepath] = content
476
+
477
+ # Fallback: extract code blocks
478
+ if not repo_files:
479
+ code_blocks = re.findall(r"```(?:python|sql)?\s*\n(.*?)\n```", llm_text, re.DOTALL)
480
+ if code_blocks:
481
+ for i, block in enumerate(code_blocks):
482
+ if len(block) > 50: # Skip tiny blocks
483
+ repo_files[f"module_{i}.py"] = block
484
+
485
+ # Add README if missing
486
+ if not any('README' in f.upper() for f in repo_files):
487
+ repo_files["README.md"] = f"""# Generated Application
488
+
489
+ ## Overview
490
+ {goal}
491
 
492
+ ## Files
493
+ {chr(10).join(f'- {f}' for f in sorted(repo_files.keys()))}
 
 
 
 
 
 
494
 
495
+ ## Setup
496
+ 1. `pip install -r requirements.txt`
497
+ 2. Copy `.env.example` to `.env` and configure
498
+ 3. Run: `python main.py`
499
+ """
500
+
501
+ # Add requirements.txt
502
+ if "requirements.txt" not in repo_files:
503
+ all_code = " ".join(repo_files.values()).lower()
504
+ deps = []
505
+ if 'requests' in all_code: deps.append('requests')
506
+ if 'pandas' in all_code: deps.append('pandas')
507
+ if 'numpy' in all_code: deps.append('numpy')
508
+ if 'sqlalchemy' in all_code: deps.append('sqlalchemy')
509
+ if 'postgresql' in all_code or 'psycopg2' in all_code: deps.append('psycopg2-binary')
510
+ if 'flask' in all_code: deps.append('flask')
511
+ if 'fastapi' in all_code:
512
+ deps.append('fastapi')
513
+ deps.append('uvicorn')
514
+ if 'dotenv' in all_code: deps.append('python-dotenv')
515
+
516
+ repo_files["requirements.txt"] = "\n".join(deps) if deps else "# Dependencies"
517
+
518
+ # Add .env.example
519
+ if ".env.example" not in repo_files:
520
+ repo_files[".env.example"] = """# Configuration
521
+ API_KEY=your_key_here
522
+ DATABASE_URL=postgresql://user:pass@localhost/db
523
+ DEBUG=False
524
  """
525
+
526
+ # Add main.py if missing
527
+ if not any('main.py' in f for f in repo_files):
528
+ repo_files["main.py"] = """#!/usr/bin/env python3
529
+ import os
530
+ from dotenv import load_dotenv
531
+
532
+ load_dotenv()
533
+
534
+ def main():
535
+ print("Application starting...")
536
+ # Add your logic here
537
+ pass
538
+
539
+ if __name__ == "__main__":
540
+ main()
541
+ """
542
+
543
+ # Build zip
544
+ zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=OUT_DIR)
545
+
546
+ results = {
547
+ "success": True,
548
+ "paths": {"repo_zip": sanitize_path(zip_path)},
549
+ "files_created": len(repo_files),
550
+ "context_used": len(full_context)
551
+ }
552
+
553
+ return {
554
+ "experimentCode": None,
555
+ "experimentResults": results,
556
+ "execution_path": path,
557
+ "status_update": f"Repository created ({len(repo_files)} files)"
558
+ }
559
 
560
+ # OTHER ARTIFACT TYPES
561
+ enhanced_prompt = f"""Create HIGH-QUALITY {exp_type} artifact.
562
+
563
+ {full_context}
564
+
565
+ GOAL: {goal}
566
+
567
+ REQUIREMENTS:
568
+ 1. Use ALL specific details from request
569
+ 2. PRODUCTION-READY, COMPLETE content (NO placeholders)
570
+ 3. ACTUAL data, REALISTIC examples, WORKING code
571
+ 4. For notebooks: markdown + executable code + visualizations
572
+ 5. For scripts: error handling + docs + real logic
573
+ 6. For documents: substantive detailed content
574
+
575
+ Generate complete content for '{exp_type}' with proper code fences."""
576
+
577
  response = llm.invoke(enhanced_prompt)
578
  llm_text = getattr(response, "content", "") or ""
 
579
  results = {"success": False, "paths": {}, "stderr": "", "stdout": "", "context_used": len(full_context)}
580
 
581
  try:
582
  if exp_type == 'notebook':
583
+ nb_path = write_notebook_from_text(llm_text, out_dir=OUT_DIR)
584
  results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
585
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Notebook created"}
 
 
 
 
 
586
 
587
  elif exp_type == 'excel':
588
+ excel_path = write_excel_from_tables(llm_text, out_dir=OUT_DIR)
589
  results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
590
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Excel created"}
 
 
 
 
 
591
 
592
  elif exp_type == 'word':
593
+ docx_path = write_docx_from_text(llm_text, out_dir=OUT_DIR)
594
  results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
595
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "DOCX created"}
 
 
 
 
 
596
 
597
  elif exp_type == 'pdf':
598
+ pdf_path = write_pdf_from_text(llm_text, out_dir=OUT_DIR)
599
  results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
600
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "PDF created"}
 
 
 
 
 
601
 
602
  elif exp_type == 'script':
603
+ lang_hint = pm.get('experiment_language') or "python"
604
  code_blocks = _extract_code_blocks(llm_text, lang_hint)
605
+ code_text = "\n\n# === BLOCK ===\n\n".join(code_blocks) if code_blocks else llm_text
606
 
607
+ script_path = write_script(code_text, language_hint=lang_hint, out_dir=OUT_DIR)
 
 
 
 
 
608
  exec_results = {}
609
 
610
  if script_path.endswith(".py"):
 
614
  exec_results = {"stdout":"","stderr":str(e),"success":False}
615
 
616
  results.update({
617
+ "success": True,
618
+ "paths": {"script": sanitize_path(script_path)},
619
+ "stdout": exec_results.get("stdout",""),
620
  "stderr": exec_results.get("stderr","")
621
  })
622
+ return {"experimentCode": code_text, "experimentResults": results, "execution_path": path, "status_update": "Script created"}
623
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  else:
625
+ fallback = write_docx_from_text(llm_text, out_dir=OUT_DIR)
626
  results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
627
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Document created"}
 
 
 
 
 
628
 
629
  except Exception as e:
630
  log.error(f"Experimenter failed: {e}")
631
  results.update({"success": False, "stderr": str(e)})
632
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Error"}
 
 
 
 
 
633
 
634
  def run_synthesis_agent(state: AgentState):
635
+ log.info("--- SYNTHESIS ---")
636
  _state = state or {}
637
+ path = ensure_list(_state, 'execution_path') + ["Synthesis"]
638
 
639
  exp_results = _state.get('experimentResults')
640
  pm_plan = _state.get('pmPlan', {}) or {}
641
 
 
642
  synthesis_context = [
643
+ f"=== USER REQUEST ===\n{_state.get('userInput', '')}",
644
+ f"\n=== OBJECTIVE ===\n{_state.get('coreObjectivePrompt', '')}",
645
+ f"\n=== PLAN ===\n{json.dumps(pm_plan.get('plan_steps', []), indent=2)}",
 
 
 
646
  ]
647
 
 
648
  artifact_details = []
649
  artifact_message = ""
650
 
 
657
  artifact_lines.append(f"- **{artifact_type.title()}**: `{os.path.basename(artifact_path)}`")
658
  artifact_details.append(f"{artifact_type}: {artifact_path}")
659
 
660
+ artifact_message = "\n\n**Artifacts Generated:**\n" + "\n".join(artifact_lines)
661
+ synthesis_context.append(f"\n=== ARTIFACTS ===\n" + "\n".join(artifact_details))
 
662
 
663
  if exp_results.get('stdout'):
664
+ synthesis_context.append(f"\n=== OUTPUT ===\n{exp_results.get('stdout', '')}")
 
665
 
666
+ if exp_results.get('stderr'):
667
+ synthesis_context.append(f"\n=== ERRORS ===\n{exp_results.get('stderr', '')}")
 
668
 
669
  full_context = "\n".join(synthesis_context)
670
 
671
+ synthesis_prompt = f"""Create FINAL RESPONSE after executing user's request.
 
672
 
673
  {full_context}
674
 
675
+ Create comprehensive response that:
676
+ 1. Directly addresses original request
677
+ 2. Explains what was accomplished and HOW
678
+ 3. References specific artifacts and explains PURPOSE
679
+ 4. Provides context on how to USE deliverables
680
+ 5. Highlights KEY INSIGHTS
681
  6. Suggests NEXT STEPS if relevant
682
 
683
+ Be SPECIFIC about what was created."""
684
 
 
 
685
  response = llm.invoke(synthesis_prompt)
686
  final_text = getattr(response, "content", "") or ""
687
 
688
  if artifact_message:
689
  final_text = final_text + "\n\n---\n" + artifact_message
690
 
691
+ return {"draftResponse": final_text, "execution_path": path, "status_update": "Response synthesized"}
 
 
 
 
692
 
693
  def run_qa_agent(state: AgentState):
694
+ log.info("--- QA ---")
695
+ path = ensure_list(state, 'execution_path') + ["QA"]
696
 
 
697
  qa_context = [
698
+ f"=== REQUEST ===\n{state.get('userInput', '')}",
699
+ f"\n=== OBJECTIVE ===\n{state.get('coreObjectivePrompt', '')}",
700
+ f"\n=== DRAFT ===\n{state.get('draftResponse', '')}",
 
 
 
701
  ]
702
 
703
  if state.get('experimentResults'):
704
+ qa_context.append(f"\n=== ARTIFACTS ===\n{json.dumps(state.get('experimentResults', {}).get('paths', {}), indent=2)}")
 
705
 
706
+ prompt = f"""Review draft against objective.
 
 
707
 
708
+ {chr(10).join(qa_context)}
709
 
710
  Respond ONLY with:
711
+ - 'APPROVED' if fully addresses request with quality
712
+ - OR provide SPECIFIC, ACTIONABLE feedback"""
713
 
 
 
714
  response = llm.invoke(prompt)
715
  content = getattr(response, "content", "") or ""
716
 
717
  if "APPROVED" in content.upper():
718
+ return {"approved": True, "qaFeedback": None, "execution_path": path, "status_update": "Approved"}
 
 
 
 
 
719
  else:
720
+ return {"approved": False, "qaFeedback": content or "No feedback", "execution_path": path, "status_update": "Needs improvement"}
 
 
 
 
 
721
 
722
  def run_archivist_agent(state: AgentState):
723
+ log.info("--- ARCHIVIST ---")
724
+ path = ensure_list(state, 'execution_path') + ["Archivist"]
 
 
 
 
 
 
 
725
 
726
+ summary_prompt = f"Summarize for memory.\n\nObjective: {state.get('coreObjectivePrompt')}\n\nResponse: {state.get('draftResponse')}\n\nSummary:"
727
  response = llm.invoke(summary_prompt)
728
+ memory_manager.add_to_memory(getattr(response,"content",""), {"objective": state.get('coreObjectivePrompt')})
 
 
 
729
 
730
+ return {"execution_path": path, "status_update": "Saved to memory"}
 
 
 
731
 
732
  def run_disclaimer_agent(state: AgentState):
733
+ log.warning("--- DISCLAIMER ---")
734
+ path = ensure_list(state, 'execution_path') + ["Disclaimer"]
 
 
 
 
 
735
 
736
+ disclaimer = "**DISCLAIMER: Budget limit reached. Draft may be incomplete.**\n\n---\n\n"
737
+ final_response = disclaimer + state.get('draftResponse', "No response")
738
 
739
+ return {"draftResponse": final_response, "execution_path": path, "status_update": "Budget limit"}
 
 
 
 
740
 
741
+ # --- Decisions ---
742
  def should_continue(state: AgentState):
 
 
743
  if state.get("approved"):
 
744
  return "archivist_agent"
 
745
  if ensure_int(state, "rework_cycles", 0) > ensure_int(state, "max_loops", 0):
 
746
  return "disclaimer_agent"
747
+ return "pm_agent"
 
 
748
 
749
  def should_run_experiment(state: AgentState):
750
  pm = state.get('pmPlan', {}) or {}