Spaces:
Paused
Paused
Update graph.py
Browse files
graph.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# graph.py
|
| 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 |
-
# ---
|
| 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
|
| 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 |
-
# ---
|
| 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
|
| 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
|
| 98 |
return None
|
| 99 |
|
| 100 |
-
# --- Artifact detection
|
| 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
|
| 109 |
-
if any(k in t for k in ["excel", ".xlsx", "spreadsheet", "csv"
|
| 110 |
-
return {"requires_artifact": True, "artifact_type": "excel", "artifact_hint": "Excel
|
| 111 |
-
if any(k in t for k in ["word document", ".docx", "docx"
|
| 112 |
-
return {"requires_artifact": True, "artifact_type": "word", "artifact_hint": "Word document
|
| 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 ["
|
| 116 |
-
return {"requires_artifact": True, "artifact_type": "
|
| 117 |
-
if any(k in t for k in ["
|
| 118 |
-
return {"requires_artifact": True, "artifact_type": "
|
| 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
|
| 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 "
|
| 139 |
return "repo"
|
| 140 |
-
if "script" in s or "python" in s
|
| 141 |
return "script"
|
| 142 |
detection = detect_requested_output_types(goal_text or "")
|
| 143 |
return detection.get("artifact_type") or "word"
|
| 144 |
|
| 145 |
-
# ---
|
| 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
|
| 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
|
| 176 |
ext = ".py"
|
| 177 |
-
elif
|
| 178 |
ext = ".R"
|
| 179 |
-
elif "java" in l
|
| 180 |
ext = ".java"
|
| 181 |
-
elif "javascript" 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
|
| 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"
|
| 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"
|
| 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 |
-
# ---
|
| 270 |
def run_triage_agent(state: AgentState):
|
| 271 |
log.info("--- TRIAGE ---")
|
| 272 |
-
prompt = f"
|
| 273 |
response = llm.invoke(prompt)
|
| 274 |
content = getattr(response, "content", "") or ""
|
| 275 |
if 'greeting' in content.lower():
|
| 276 |
-
|
| 277 |
-
|
| 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
|
| 284 |
-
path = ensure_list(state, 'execution_path') + ["Planner
|
| 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": "
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
plan_data['max_loops_initial'] = INITIAL_MAX_REWORK_CYCLES
|
| 297 |
-
plan_data['estimated_cost_usd'] = round(
|
| 298 |
plan_data['cost_per_loop_usd'] = max(0.01, round(cost_per_loop, 3))
|
| 299 |
-
|
|
|
|
| 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',
|
| 304 |
-
|
| 305 |
-
return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan
|
| 306 |
|
| 307 |
def run_memory_retrieval(state: AgentState):
|
| 308 |
-
log.info("--- MEMORY
|
| 309 |
-
path = ensure_list(state, 'execution_path') + ["Memory
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 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
|
| 321 |
-
path = ensure_list(state, 'execution_path') + ["Intent
|
| 322 |
-
prompt =
|
| 323 |
response = llm.invoke(prompt)
|
| 324 |
core_obj = getattr(response, "content", "") or ""
|
| 325 |
-
|
| 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
|
| 333 |
current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
|
| 334 |
max_loops_val = ensure_int(state, 'max_loops', 0)
|
| 335 |
-
|
| 336 |
-
path = ensure_list(state, 'execution_path') + ["PM Agent"]
|
| 337 |
|
| 338 |
-
# BUILD COMPREHENSIVE CONTEXT
|
| 339 |
context_parts = [
|
| 340 |
-
f"=== USER
|
| 341 |
-
f"{state.get('
|
| 342 |
-
f"\n===
|
| 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
|
| 350 |
-
context_parts.append(f"{state.get('
|
| 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 |
-
|
| 357 |
-
prompt = f"""You are a Project Manager creating a DETAILED, EXECUTABLE plan.
|
| 358 |
|
| 359 |
{full_context}
|
| 360 |
|
| 361 |
-
|
| 362 |
- State EXACTLY what will be created/analyzed
|
| 363 |
-
- Specify WHAT
|
| 364 |
-
- Define WHAT
|
| 365 |
|
| 366 |
-
|
| 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
|
| 376 |
-
"
|
| 377 |
-
"key_requirements": ["Critical requirements that MUST be met"]
|
| 378 |
}}
|
| 379 |
|
| 380 |
-
|
| 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 |
-
|
| 406 |
-
|
| 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'] =
|
| 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 |
-
|
| 428 |
-
return blocks
|
| 429 |
|
| 430 |
def run_experimenter_agent(state: AgentState):
|
| 431 |
-
log.info("--- EXPERIMENTER
|
| 432 |
-
path = ensure_list(state, 'execution_path') + ["Experimenter
|
| 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
|
| 445 |
|
| 446 |
-
# BUILD
|
| 447 |
context_parts = [
|
| 448 |
-
f"=== USER
|
| 449 |
-
f"{state.get('
|
| 450 |
-
f"\n===
|
| 451 |
-
f"{
|
| 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===
|
| 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 |
-
#
|
| 469 |
-
|
|
|
|
| 470 |
|
| 471 |
{full_context}
|
| 472 |
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
-
|
| 476 |
-
|
| 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 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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=
|
| 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=
|
| 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=
|
| 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
|
| 536 |
code_blocks = _extract_code_blocks(llm_text, lang_hint)
|
|
|
|
| 537 |
|
| 538 |
-
|
| 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 |
-
|
| 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=
|
| 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
|
| 604 |
_state = state or {}
|
| 605 |
-
path = ensure_list(_state, 'execution_path') + ["Synthesis
|
| 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"===
|
| 613 |
-
f"{_state.get('
|
| 614 |
-
f"\n===
|
| 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**
|
| 634 |
-
synthesis_context.append(f"\n=== ARTIFACTS
|
| 635 |
-
synthesis_context.append("\n".join(artifact_details))
|
| 636 |
|
| 637 |
if exp_results.get('stdout'):
|
| 638 |
-
synthesis_context.append(f"\n===
|
| 639 |
-
synthesis_context.append(exp_results.get('stdout', ''))
|
| 640 |
|
| 641 |
-
if exp_results.get('stderr')
|
| 642 |
-
synthesis_context.append(f"\n===
|
| 643 |
-
synthesis_context.append(exp_results.get('stderr', ''))
|
| 644 |
|
| 645 |
full_context = "\n".join(synthesis_context)
|
| 646 |
|
| 647 |
-
|
| 648 |
-
synthesis_prompt = f"""You are creating the FINAL RESPONSE after executing a user's request.
|
| 649 |
|
| 650 |
{full_context}
|
| 651 |
|
| 652 |
-
Create
|
| 653 |
-
1. Directly addresses
|
| 654 |
-
2. Explains what was accomplished and HOW
|
| 655 |
-
3. References specific artifacts
|
| 656 |
-
4. Provides context on how to USE
|
| 657 |
-
5. Highlights KEY INSIGHTS
|
| 658 |
6. Suggests NEXT STEPS if relevant
|
| 659 |
|
| 660 |
-
|
| 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
|
| 678 |
-
path = ensure_list(state, 'execution_path') + ["QA
|
| 679 |
|
| 680 |
-
# Enhanced QA with context awareness
|
| 681 |
qa_context = [
|
| 682 |
-
f"===
|
| 683 |
-
f"{state.get('
|
| 684 |
-
f"\n===
|
| 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 |
-
|
| 695 |
-
|
| 696 |
-
prompt = f"""Review the draft response against the core objective.
|
| 697 |
|
| 698 |
-
{
|
| 699 |
|
| 700 |
Respond ONLY with:
|
| 701 |
-
- 'APPROVED' if
|
| 702 |
-
- OR provide SPECIFIC, ACTIONABLE feedback
|
| 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
|
| 726 |
-
path = ensure_list(state, 'execution_path') + ["Archivist
|
| 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
|
| 748 |
-
path = ensure_list(state, 'execution_path') + ["Disclaimer
|
| 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 |
-
|
|
|
|
| 756 |
|
| 757 |
-
return {
|
| 758 |
-
"draftResponse": final_response,
|
| 759 |
-
"execution_path": path,
|
| 760 |
-
"status_update": "Budget limit reached. Preparing final draft..."
|
| 761 |
-
}
|
| 762 |
|
| 763 |
-
# ---
|
| 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 |
-
|
| 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 {}
|