JatinAutonomousLabs commited on
Commit
a935b87
·
verified ·
1 Parent(s): 5651f4f

Update graph.py

Browse files
Files changed (1) hide show
  1. graph.py +320 -88
graph.py CHANGED
@@ -1,7 +1,11 @@
1
- # graph.py (patched)
2
  import json
3
  import re
4
  import math
 
 
 
 
5
  from typing import TypedDict, List, Dict, Optional
6
  from langchain_openai import ChatOpenAI
7
  from langgraph.graph import StateGraph, END
@@ -9,21 +13,26 @@ from memory_manager import memory_manager
9
  from code_executor import execute_python_code
10
  from logging_config import setup_logging, get_logger
11
 
 
 
 
 
 
 
 
 
 
12
  def ensure_list(state, key):
13
- """Return a list from state[key], default [] if missing/None/not-list."""
14
  v = state.get(key) if state else None
15
  if v is None:
16
  return []
17
  if isinstance(v, list):
18
  return v
19
- # if it's a tuple, convert to list
20
  if isinstance(v, tuple):
21
  return list(v)
22
- # fallback: wrap single scalar value
23
  return [v]
24
 
25
  def ensure_int(state, key, default=0):
26
- """Return an int-like value from state[key], default if missing/None/not-int."""
27
  try:
28
  v = state.get(key) if state else None
29
  if v is None:
@@ -32,7 +41,11 @@ def ensure_int(state, key, default=0):
32
  except Exception:
33
  return default
34
 
35
- # --- Setup Logging & Constants ---
 
 
 
 
36
  setup_logging()
37
  log = get_logger(__name__)
38
  INITIAL_MAX_REWORK_CYCLES = 3
@@ -40,7 +53,7 @@ GPT4O_INPUT_COST_PER_1K_TOKENS = 0.005
40
  GPT4O_OUTPUT_COST_PER_1K_TOKENS = 0.015
41
  AVG_TOKENS_PER_CALL = 2.0
42
 
43
- # --- Agent State Definition ---
44
  class AgentState(TypedDict):
45
  userInput: str
46
  chatHistory: List[str]
@@ -57,74 +70,225 @@ class AgentState(TypedDict):
57
  max_loops: int
58
  status_update: str
59
 
60
- # --- Helper for Robust JSON Parsing ---
 
 
61
  def parse_json_from_llm(llm_output: str) -> Optional[dict]:
62
  try:
63
  match = re.search(r"```json\n({.*?})\n```", llm_output, re.DOTALL)
64
  if match:
65
  json_str = match.group(1)
66
  else:
67
- json_str = llm_output[llm_output.find('{'):llm_output.rfind('}')+1]
 
 
 
 
68
  return json.loads(json_str)
69
- except (json.JSONDecodeError, AttributeError) as e:
70
- log.error(f"JSON parsing failed. Raw output: '{llm_output}'. Error: {e}")
71
  return None
72
 
73
- # --- LLM Initialization ---
74
- llm = ChatOpenAI(model="gpt-4o", temperature=0.1, max_retries=3, request_timeout=60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- # --- Agent Node Functions ---
77
  def run_triage_agent(state: AgentState):
78
  log.info("--- triage ---")
79
- prompt = f"Analyze the user input. Is it a simple conversational greeting or a task? Respond with 'greeting' or 'task'.\n\nUser Input: \"{state['userInput']}\""
80
  response = llm.invoke(prompt)
81
-
82
  if 'greeting' in response.content.lower():
83
  log.info("Triage result: Simple Greeting.")
84
- return {
85
- "draftResponse": "Hello! How can I help you today?",
86
- "execution_path": ["Triage Agent"],
87
- "status_update": "Responding to greeting."
88
- }
89
  else:
90
  log.info("Triage result: Complex Task.")
91
- return {
92
- "execution_path": ["Triage Agent"],
93
- "status_update": "Request requires a plan. Proceeding..."
94
- }
95
 
96
  def run_planner_agent(state: AgentState):
97
  log.info("--- ✈️ Running Planner Agent ---")
98
  path = ensure_list(state, 'execution_path') + ["Planner Agent"]
99
  prompt = (
100
  f"Analyze the user's request. Provide a high-level plan and estimate the number of LLM calls for one loop. "
101
- f"User Request: \"{state['userInput']}\". Respond in JSON with keys: 'plan' (list of strings), "
102
- f"'estimated_llm_calls_per_loop' (integer)."
103
  )
104
-
105
  response = llm.invoke(prompt)
106
  plan_data = parse_json_from_llm(response.content)
107
-
108
  if not plan_data:
109
  return {"pmPlan": {"error": "Failed to create a valid plan."}, "execution_path": path, "status_update": "Error: Could not create a plan."}
110
-
111
  calls_per_loop = plan_data.get('estimated_llm_calls_per_loop', 3)
112
- cost_per_loop = (calls_per_loop * AVG_TOKENS_PER_CALL) * (
113
- (GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2
114
- )
115
  estimated_cost = cost_per_loop * (INITIAL_MAX_REWORK_CYCLES + 1)
116
-
117
  plan_data['max_loops_initial'] = INITIAL_MAX_REWORK_CYCLES
118
  plan_data['estimated_cost_usd'] = round(estimated_cost, 2)
119
  plan_data['cost_per_loop_usd'] = max(0.01, round(cost_per_loop, 3))
120
-
 
 
 
 
121
  log.info(f"Pre-flight Estimate: {plan_data}")
122
  return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan and cost estimate created. Awaiting approval."}
123
 
124
  def run_memory_retrieval(state: AgentState):
125
  log.info("--- 🧠 Accessing Long-Term Memory ---")
126
  path = ensure_list(state, 'execution_path') + ["Memory Retriever"]
127
- relevant_mems = memory_manager.retrieve_relevant_memories(state['userInput'])
128
  if relevant_mems:
129
  context = "\n".join([f"Memory: {mem.page_content}" for mem in relevant_mems])
130
  log.info(f"Found {len(relevant_mems)} relevant memories.")
@@ -136,79 +300,154 @@ def run_memory_retrieval(state: AgentState):
136
  def run_intent_agent(state: AgentState):
137
  log.info("--- 🎯 Running Intent Agent ---")
138
  path = ensure_list(state, 'execution_path') + ["Intent Agent"]
139
- prompt = (
140
- f"Refine the user's request into a clear, actionable 'core objective prompt'.\n\n"
141
- f"Relevant Memory:\n{state.get('retrievedMemory')}\n\nUser Request: \"{state.get('userInput')}\"\n\nCore Objective:"
142
- )
143
  response = llm.invoke(prompt)
144
- return {"coreObjectivePrompt": response.content, "execution_path": path, "status_update": "Clarifying the main objective..."}
 
 
 
 
 
145
 
146
  def run_pm_agent(state: AgentState):
147
  log.info("--- 👷 Running PM Agent ---")
148
- # coerce rework_cycles/max_loops to integers if they are None or falsy
149
  current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
150
  max_loops_val = ensure_int(state, 'max_loops', 0)
151
  log.info(f"Starting work cycle {current_cycles}/{max_loops_val + 1}")
152
-
153
  path = ensure_list(state, 'execution_path') + ["PM Agent"]
154
  feedback = f"QA Feedback (must be addressed): {state.get('qaFeedback')}" if state.get('qaFeedback') else ""
155
  prompt = (
156
- f"Decompose the core objective into a plan. Determine if code execution is needed and define the goal.\n\n"
157
  f"Core Objective: {state.get('coreObjectivePrompt')}\n\n{feedback}\n\n"
158
- f"Respond in JSON with keys: 'plan_steps' (list), 'experiment_needed' (bool), and 'experiment_goal' (str if needed)."
159
  )
160
-
161
  response = llm.invoke(prompt)
162
  plan = parse_json_from_llm(response.content)
163
-
164
  if not plan:
165
- log.error("PM Agent failed to produce a valid JSON plan.")
166
- plan = {"plan_steps": ["Error: The Project Manager failed to create a valid plan."], "experiment_needed": False}
167
-
168
- log.info(f"Generated Plan: Experiment Needed = {plan.get('experiment_needed', False)}")
 
 
 
 
 
 
 
 
 
169
  return {"pmPlan": plan, "execution_path": path, "rework_cycles": current_cycles, "status_update": "Breaking down the objective into a detailed plan..."}
170
 
 
 
 
171
  def run_experimenter_agent(state: AgentState):
172
  log.info("--- 🔬 Running Experimenter Agent ---")
173
  path = ensure_list(state, 'execution_path') + ["Experimenter Agent"]
174
- if not state.get('pmPlan', {}).get('experiment_needed'):
 
175
  return {"experimentCode": None, "experimentResults": None, "execution_path": path, "status_update": "Proceeding without a code experiment."}
176
-
177
- goal = state.get('pmPlan', {}).get('experiment_goal', 'No goal specified.')
178
- prompt = f"Write a Python script to achieve this goal: {goal}. Print results to standard output."
179
- response = llm.invoke(prompt)
180
- code_match = re.search(r"```python\n(.*?)\n```", response.content, re.DOTALL)
181
- if not code_match:
182
- log.error("Experimenter failed to generate valid Python code.")
183
- results = {"success": False, "stderr": "No valid Python code block was generated."}
184
- return {"experimentCode": "# ERROR: No code generated", "experimentResults": results, "execution_path": path, "status_update": "Error: Failed to write necessary code."}
185
-
186
- code = code_match.group(1).strip()
187
- results = execute_python_code(code)
188
- return {"experimentCode": code, "experimentResults": results, "execution_path": path, "status_update": "Running code to find a solution..."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  def run_synthesis_agent(state: AgentState):
191
  log.info("--- ✍️ Running Synthesis Agent ---")
192
  path = ensure_list(state, 'execution_path') + ["Synthesis Agent"]
193
  exp_results = state.get('experimentResults')
194
  results_summary = "No experiment was conducted."
195
- if exp_results:
196
- results_summary = f"Experiment Output:\nSTDOUT:\n{exp_results.get('stdout', '')}\nSTDERR:\n{exp_results.get('stderr', '')}"
197
-
 
 
 
 
 
 
 
 
198
  prompt = (
199
  f"Synthesize all information into a final response.\n\nCore Objective: {state.get('coreObjectivePrompt')}\n\n"
200
  f"Plan: {state.get('pmPlan', {}).get('plan_steps')}\n\n{results_summary}\n\nFinal Response:"
201
  )
202
  response = llm.invoke(prompt)
203
- return {"draftResponse": response.content, "execution_path": path, "status_update": "Putting together the final response..."}
 
 
 
204
 
205
  def run_qa_agent(state: AgentState):
206
  log.info("--- ✅ Running QA Agent ---")
207
  path = ensure_list(state, 'execution_path') + ["QA Agent"]
208
- prompt = (
209
- f"Review the draft response based on the core objective. Respond ONLY with 'APPROVED' or provide concise feedback for rework.\n\n"
210
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\nDraft: {state.get('draftResponse')}"
211
- )
212
  response = llm.invoke(prompt)
213
  if "APPROVED" in response.content.upper():
214
  return {"approved": True, "qaFeedback": None, "execution_path": path, "status_update": "Response approved!"}
@@ -218,10 +457,8 @@ def run_qa_agent(state: AgentState):
218
  def run_archivist_agent(state: AgentState):
219
  log.info("--- 💾 Running Archivist Agent ---")
220
  path = ensure_list(state, 'execution_path') + ["Archivist Agent"]
221
- summary_prompt = (
222
- f"Create a concise summary of this successful task for long-term memory.\n\n"
223
- f"Core Objective: {state.get('coreObjectivePrompt')}\n\nFinal Response: {state.get('draftResponse')}\n\nMemory Summary:"
224
- )
225
  response = llm.invoke(summary_prompt)
226
  memory_manager.add_to_memory(response.content, {"objective": state.get('coreObjectivePrompt')})
227
  return {"execution_path": path, "status_update": "Saving key learnings for future reference..."}
@@ -229,13 +466,11 @@ def run_archivist_agent(state: AgentState):
229
  def run_disclaimer_agent(state: AgentState):
230
  log.warning("--- ⚠️ Running Disclaimer Agent ---")
231
  path = ensure_list(state, 'execution_path') + ["Disclaimer Agent"]
232
- disclaimer = (
233
- "**DISCLAIMER: The process was stopped after exhausting the budget. The following response is the best available draft and may be incomplete.**\n\n---\n\n"
234
- )
235
  final_response = disclaimer + state.get('draftResponse', "No response was generated.")
236
  return {"draftResponse": final_response, "execution_path": path, "status_update": "Budget limit reached. Preparing final draft..."}
237
 
238
- # --- Conditional Edges & Graph Definition ---
239
  def should_continue(state: AgentState):
240
  log.info("--- 🤔 Decision: Is the response QA approved? ---")
241
  if state.get("approved"):
@@ -249,25 +484,22 @@ def should_continue(state: AgentState):
249
  return "pm_agent"
250
 
251
  def should_run_experiment(state: AgentState):
252
- decision = "experimenter_agent" if state.get('pmPlan', {}).get('experiment_needed') else "synthesis_agent"
253
- return decision
254
 
255
- # --- Build the Graphs ---
256
- # 1. Triage Graph
257
  triage_workflow = StateGraph(AgentState)
258
  triage_workflow.add_node("triage", run_triage_agent)
259
  triage_workflow.set_entry_point("triage")
260
  triage_workflow.add_edge("triage", END)
261
  triage_app = triage_workflow.compile()
262
 
263
- # 2. Planner-only Graph
264
  planner_workflow = StateGraph(AgentState)
265
  planner_workflow.add_node("planner", run_planner_agent)
266
  planner_workflow.set_entry_point("planner")
267
  planner_workflow.add_edge("planner", END)
268
  planner_app = planner_workflow.compile()
269
 
270
- # 3. Full Execution Graph
271
  main_workflow = StateGraph(AgentState)
272
  main_workflow.add_node("memory_retriever", run_memory_retrieval)
273
  main_workflow.add_node("intent_agent", run_intent_agent)
@@ -292,4 +524,4 @@ main_workflow.add_conditional_edges("qa_agent", should_continue, {
292
  "pm_agent": "pm_agent",
293
  "disclaimer_agent": "disclaimer_agent"
294
  })
295
- main_app = main_workflow.compile()
 
1
+ # graph.py (patched: artifact generation using nbformat, python-docx, pandas/openpyxl, reportlab)
2
  import json
3
  import re
4
  import math
5
+ import os
6
+ import uuid
7
+ import shutil
8
+ import zipfile
9
  from typing import TypedDict, List, Dict, Optional
10
  from langchain_openai import ChatOpenAI
11
  from langgraph.graph import StateGraph, END
 
13
  from code_executor import execute_python_code
14
  from logging_config import setup_logging, get_logger
15
 
16
+ # External artifact libs
17
+ import nbformat
18
+ from nbformat.v4 import new_notebook, new_markdown_cell, new_code_cell
19
+ import pandas as pd
20
+ from docx import Document
21
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
22
+ from reportlab.lib.styles import getSampleStyleSheet
23
+
24
+ # --- Helpers ---
25
  def ensure_list(state, key):
 
26
  v = state.get(key) if state else None
27
  if v is None:
28
  return []
29
  if isinstance(v, list):
30
  return v
 
31
  if isinstance(v, tuple):
32
  return list(v)
 
33
  return [v]
34
 
35
  def ensure_int(state, key, default=0):
 
36
  try:
37
  v = state.get(key) if state else None
38
  if v is None:
 
41
  except Exception:
42
  return default
43
 
44
+ def sanitize_path(path: str) -> str:
45
+ # On HF Spaces you may want to move to a served directory. Keep as-is here.
46
+ return path
47
+
48
+ # --- Logging & constants ---
49
  setup_logging()
50
  log = get_logger(__name__)
51
  INITIAL_MAX_REWORK_CYCLES = 3
 
53
  GPT4O_OUTPUT_COST_PER_1K_TOKENS = 0.015
54
  AVG_TOKENS_PER_CALL = 2.0
55
 
56
+ # --- AgentState ---
57
  class AgentState(TypedDict):
58
  userInput: str
59
  chatHistory: List[str]
 
70
  max_loops: int
71
  status_update: str
72
 
73
+ # --- LLM & parsing ---
74
+ llm = ChatOpenAI(model="gpt-4o", temperature=0.1, max_retries=3, request_timeout=60)
75
+
76
  def parse_json_from_llm(llm_output: str) -> Optional[dict]:
77
  try:
78
  match = re.search(r"```json\n({.*?})\n```", llm_output, re.DOTALL)
79
  if match:
80
  json_str = match.group(1)
81
  else:
82
+ start = llm_output.find('{')
83
+ end = llm_output.rfind('}')
84
+ if start == -1 or end == -1:
85
+ return None
86
+ json_str = llm_output[start:end+1]
87
  return json.loads(json_str)
88
+ except Exception as e:
89
+ log.error(f"JSON parsing failed. Error: {e}. Raw: {llm_output[:300]}")
90
  return None
91
 
92
+ # --- Artifact detection ---
93
+ def detect_requested_output_types(text: str) -> Dict:
94
+ if not text:
95
+ return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
96
+ t = text.lower()
97
+ if any(k in t for k in ["jupyter notebook", "jupyter", "notebook", "ipynb"]):
98
+ return {"requires_artifact": True, "artifact_type": "notebook", "artifact_hint": "jupyter notebook (.ipynb)"}
99
+ if any(k in t for k in ["excel", ".xlsx", "spreadsheet", "csv", "sheet"]):
100
+ return {"requires_artifact": True, "artifact_type": "excel", "artifact_hint": "Excel/CSV file"}
101
+ if any(k in t for k in ["word document", ".docx", "docx", "word file"]):
102
+ return {"requires_artifact": True, "artifact_type": "word", "artifact_hint": "Word document (.docx)"}
103
+ if any(k in t for k in ["pdf", "pdf file"]):
104
+ return {"requires_artifact": True, "artifact_type": "pdf", "artifact_hint": "PDF document"}
105
+ if any(k in t for k in ["image", "plot", "chart", "png", "jpg", "jpeg"]):
106
+ return {"requires_artifact": True, "artifact_type": "image", "artifact_hint": "image/plot"}
107
+ if any(k in t for k in ["repo", "repository", "app repo", "dockerfile", "requirements.txt", "package.json"]):
108
+ return {"requires_artifact": True, "artifact_type": "repo", "artifact_hint": "application repository (zip)"}
109
+ # scripts for languages
110
+ if any(k in t for k in [".py", "python script", "r script", ".R", ".r", "java", ".java", "javascript", ".js"]):
111
+ # heuristic: choose 'script' and later infer language
112
+ return {"requires_artifact": True, "artifact_type": "script", "artifact_hint": "language script (py/r/java/js/etc.)"}
113
+ return {"requires_artifact": False, "artifact_type": None, "artifact_hint": None}
114
+
115
+ # --- Notebook & artifact builders ---
116
+ def write_notebook_from_text(llm_text: str, out_dir: str="/tmp") -> str:
117
+ """
118
+ Build a notebook via nbformat from llm_text using fenced python code blocks as code cells and other text as markdown.
119
+ """
120
+ code_blocks = re.findall(r"```python\n(.*?)\n```", llm_text, re.DOTALL)
121
+ # fallback to any fenced blocks
122
+ if not code_blocks:
123
+ code_blocks = re.findall(r"```\n(.*?)\n```", llm_text, re.DOTALL)
124
+ # split markdown by removing code blocks
125
+ md_parts = re.split(r"```(?:python)?\n.*?\n```", llm_text, flags=re.DOTALL)
126
+ nb = new_notebook()
127
+ cells = []
128
+ max_len = max(len(md_parts), len(code_blocks))
129
+ for i in range(max_len):
130
+ if i < len(md_parts) and md_parts[i].strip():
131
+ cells.append(new_markdown_cell(md_parts[i].strip()))
132
+ if i < len(code_blocks) and code_blocks[i].strip():
133
+ cells.append(new_code_cell(code_blocks[i].strip()))
134
+ if not cells:
135
+ cells = [new_markdown_cell("# Notebook\n\nNo content parsed from LLM output.")]
136
+ nb['cells'] = cells
137
+ uid = uuid.uuid4().hex[:10]
138
+ filename = os.path.join(out_dir, f"generated_notebook_{uid}.ipynb")
139
+ nbformat.write(nb, filename)
140
+ return filename
141
+
142
+ def write_script(code_text: str, language_hint: Optional[str]=None, out_dir: str="/tmp") -> str:
143
+ ext = ".txt"
144
+ if language_hint:
145
+ l = language_hint.lower()
146
+ if "python" in l or ".py" in l:
147
+ ext = ".py"
148
+ elif l in ("r", ".r"):
149
+ ext = ".R"
150
+ elif "java" in l or ".java" in l:
151
+ ext = ".java"
152
+ elif "javascript" in l or "node" in l or ".js" in l:
153
+ ext = ".js"
154
+ elif "bash" in l or "sh" in l:
155
+ ext = ".sh"
156
+ uid = uuid.uuid4().hex[:10]
157
+ filename = os.path.join(out_dir, f"generated_script_{uid}{ext}")
158
+ with open(filename, "w", encoding="utf-8") as f:
159
+ f.write(code_text)
160
+ return filename
161
+
162
+ def write_docx_from_text(text: str, out_dir: str="/tmp") -> str:
163
+ doc = Document()
164
+ # naive: split into paragraphs on double-newline
165
+ for para in [p.strip() for p in text.split("\n\n") if p.strip()]:
166
+ doc.add_paragraph(para)
167
+ uid = uuid.uuid4().hex[:10]
168
+ filename = os.path.join(out_dir, f"generated_doc_{uid}.docx")
169
+ doc.save(filename)
170
+ return filename
171
+
172
+ def write_excel_from_tables(maybe_table_text: str, out_dir: str="/tmp") -> str:
173
+ """
174
+ Heuristic: If LLM returns a JSON-convertible table or CSV snippet, attempt to form a DataFrame.
175
+ Otherwise write a small DataFrame with the provided text.
176
+ """
177
+ uid = uuid.uuid4().hex[:10]
178
+ filename = os.path.join(out_dir, f"generated_excel_{uid}.xlsx")
179
+ try:
180
+ # try JSON parse
181
+ parsed = None
182
+ try:
183
+ parsed = json.loads(maybe_table_text)
184
+ # if parsed is list of dicts
185
+ if isinstance(parsed, list):
186
+ df = pd.DataFrame(parsed)
187
+ elif isinstance(parsed, dict):
188
+ # dict of lists or single mapping
189
+ df = pd.DataFrame([parsed])
190
+ else:
191
+ df = pd.DataFrame({"content":[str(maybe_table_text)]})
192
+ except Exception:
193
+ # fallback: look for CSV text
194
+ if "," in maybe_table_text or "\t" in maybe_table_text:
195
+ from io import StringIO
196
+ df = pd.read_csv(StringIO(maybe_table_text))
197
+ else:
198
+ df = pd.DataFrame({"content":[maybe_table_text]})
199
+ df.to_excel(filename, index=False, engine="openpyxl")
200
+ return filename
201
+ except Exception as e:
202
+ log.error(f"Excel creation failed: {e}")
203
+ # write fallback docx with text
204
+ return write_docx_from_text(f"Failed to create excel. Error: {e}\n\nOriginal:\n{maybe_table_text}", out_dir=out_dir)
205
+
206
+ def write_pdf_from_text(text: str, out_dir: str="/tmp") -> str:
207
+ uid = uuid.uuid4().hex[:10]
208
+ filename = os.path.join(out_dir, f"generated_doc_{uid}.pdf")
209
+ try:
210
+ doc = SimpleDocTemplate(filename)
211
+ styles = getSampleStyleSheet()
212
+ flowables = []
213
+ for para in [p.strip() for p in text.split("\n\n") if p.strip()]:
214
+ flowables.append(Paragraph(para.replace("\n","<br/>"), styles["Normal"]))
215
+ flowables.append(Spacer(1, 8))
216
+ doc.build(flowables)
217
+ return filename
218
+ except Exception as e:
219
+ log.error(f"PDF creation failed: {e}")
220
+ # fallback to docx
221
+ return write_docx_from_text(f"Failed to create PDF. Error: {e}\n\nOriginal:\n{text}", out_dir=out_dir)
222
+
223
+ def build_repo_zip(files_map: Dict[str,str], repo_name: str="generated_app", out_dir: str="/tmp") -> str:
224
+ """
225
+ files_map: dict of relative path -> absolute local file path/content.
226
+ If the value is a string and exists as a path, include file. If not a path, create a file with that content.
227
+ """
228
+ uid = uuid.uuid4().hex[:8]
229
+ repo_dir = os.path.join(out_dir, f"{repo_name}_{uid}")
230
+ os.makedirs(repo_dir, exist_ok=True)
231
+ for rel_path, content in files_map.items():
232
+ dest = os.path.join(repo_dir, rel_path)
233
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
234
+ if isinstance(content, str) and os.path.exists(content):
235
+ shutil.copyfile(content, dest)
236
+ else:
237
+ # treat content as file content
238
+ with open(dest, "w", encoding="utf-8") as fh:
239
+ fh.write(str(content))
240
+ zip_path = os.path.join(out_dir, f"{repo_name}_{uid}.zip")
241
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
242
+ for root, _, files in os.walk(repo_dir):
243
+ for f in files:
244
+ full = os.path.join(root, f)
245
+ arc = os.path.relpath(full, repo_dir)
246
+ zf.write(full, arc)
247
+ return zip_path
248
+
249
+ # --- Node functions (triage/planner/memory/intent/pm/experimenter/synthesis/qa/archivist/disclaimer) ---
250
+ # For brevity reuse earlier implementations but with artifact creation in experimenter
251
 
 
252
  def run_triage_agent(state: AgentState):
253
  log.info("--- triage ---")
254
+ 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','')}\""
255
  response = llm.invoke(prompt)
 
256
  if 'greeting' in response.content.lower():
257
  log.info("Triage result: Simple Greeting.")
258
+ return {"draftResponse": "Hello! How can I help you today?", "execution_path": ["Triage Agent"], "status_update": "Responding to greeting."}
 
 
 
 
259
  else:
260
  log.info("Triage result: Complex Task.")
261
+ return {"execution_path": ["Triage Agent"], "status_update": "Request requires a plan. Proceeding..."}
 
 
 
262
 
263
  def run_planner_agent(state: AgentState):
264
  log.info("--- ✈️ Running Planner Agent ---")
265
  path = ensure_list(state, 'execution_path') + ["Planner Agent"]
266
  prompt = (
267
  f"Analyze the user's request. Provide a high-level plan and estimate the number of LLM calls for one loop. "
268
+ f"User Request: \"{state.get('userInput','')}\". Respond in JSON with keys: 'plan' (list of strings), 'estimated_llm_calls_per_loop' (integer)."
 
269
  )
 
270
  response = llm.invoke(prompt)
271
  plan_data = parse_json_from_llm(response.content)
 
272
  if not plan_data:
273
  return {"pmPlan": {"error": "Failed to create a valid plan."}, "execution_path": path, "status_update": "Error: Could not create a plan."}
 
274
  calls_per_loop = plan_data.get('estimated_llm_calls_per_loop', 3)
275
+ cost_per_loop = (calls_per_loop * AVG_TOKENS_PER_CALL) * ((GPT4O_INPUT_COST_PER_1K_TOKENS + GPT4O_OUTPUT_COST_PER_1K_TOKENS) / 2)
 
 
276
  estimated_cost = cost_per_loop * (INITIAL_MAX_REWORK_CYCLES + 1)
 
277
  plan_data['max_loops_initial'] = INITIAL_MAX_REWORK_CYCLES
278
  plan_data['estimated_cost_usd'] = round(estimated_cost, 2)
279
  plan_data['cost_per_loop_usd'] = max(0.01, round(cost_per_loop, 3))
280
+ detection = detect_requested_output_types(state.get('userInput','') or state.get('coreObjectivePrompt','') or '')
281
+ if detection.get('requires_artifact'):
282
+ plan_data.setdefault('experiment_needed', True)
283
+ plan_data.setdefault('experiment_type', detection.get('artifact_type'))
284
+ plan_data.setdefault('experiment_goal', f"Produce an artifact: {detection.get('artifact_hint')}. {state.get('userInput','')}")
285
  log.info(f"Pre-flight Estimate: {plan_data}")
286
  return {"pmPlan": plan_data, "execution_path": path, "status_update": "Plan and cost estimate created. Awaiting approval."}
287
 
288
  def run_memory_retrieval(state: AgentState):
289
  log.info("--- 🧠 Accessing Long-Term Memory ---")
290
  path = ensure_list(state, 'execution_path') + ["Memory Retriever"]
291
+ relevant_mems = memory_manager.retrieve_relevant_memories(state.get('userInput',''))
292
  if relevant_mems:
293
  context = "\n".join([f"Memory: {mem.page_content}" for mem in relevant_mems])
294
  log.info(f"Found {len(relevant_mems)} relevant memories.")
 
300
  def run_intent_agent(state: AgentState):
301
  log.info("--- 🎯 Running Intent Agent ---")
302
  path = ensure_list(state, 'execution_path') + ["Intent Agent"]
303
+ 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:")
 
 
 
304
  response = llm.invoke(prompt)
305
+ core_obj = response.content
306
+ detection = detect_requested_output_types(core_obj or state.get('userInput',''))
307
+ extras = {}
308
+ if detection.get('requires_artifact'):
309
+ extras['artifact_detection'] = detection
310
+ return {"coreObjectivePrompt": core_obj, **extras, "execution_path": path, "status_update": "Clarifying the main objective..."}
311
 
312
  def run_pm_agent(state: AgentState):
313
  log.info("--- 👷 Running PM Agent ---")
 
314
  current_cycles = ensure_int(state, 'rework_cycles', 0) + 1
315
  max_loops_val = ensure_int(state, 'max_loops', 0)
316
  log.info(f"Starting work cycle {current_cycles}/{max_loops_val + 1}")
 
317
  path = ensure_list(state, 'execution_path') + ["PM Agent"]
318
  feedback = f"QA Feedback (must be addressed): {state.get('qaFeedback')}" if state.get('qaFeedback') else ""
319
  prompt = (
320
+ f"Decompose the core objective into a plan. Determine if code execution or artifact generation is needed and define the goal.\n\n"
321
  f"Core Objective: {state.get('coreObjectivePrompt')}\n\n{feedback}\n\n"
322
+ f"Respond in JSON with keys: 'plan_steps' (list), 'experiment_needed' (bool), 'experiment_type' (optional string), and 'experiment_goal' (str if needed)."
323
  )
 
324
  response = llm.invoke(prompt)
325
  plan = parse_json_from_llm(response.content)
 
326
  if not plan:
327
+ log.warning("PM Agent did not produce JSON applying heuristic fallback.")
328
+ plan = {"plan_steps": ["Analyze files", "Create notebook if requested", "Synthesize answers"], "experiment_needed": False}
329
+ intent_detector = state.get('artifact_detection') or {}
330
+ if intent_detector.get('requires_artifact'):
331
+ plan['experiment_needed'] = True
332
+ plan['experiment_type'] = intent_detector.get('artifact_type')
333
+ plan['experiment_goal'] = f"Produce an artifact: {intent_detector.get('artifact_hint')}. Use document reading and test edge cases for messy files in the folder. {state.get('userInput','')}"
334
+ if plan.get('experiment_needed') and not plan.get('experiment_type'):
335
+ detection = detect_requested_output_types(state.get('coreObjectivePrompt','') or state.get('userInput',''))
336
+ if detection.get('requires_artifact'):
337
+ plan['experiment_type'] = detection.get('artifact_type')
338
+ plan['experiment_goal'] = plan.get('experiment_goal') or f"Produce an artifact: {detection.get('artifact_hint')}."
339
+ log.info(f"Generated Plan: Experiment Needed = {plan.get('experiment_needed', False)}, Type = {plan.get('experiment_type')}")
340
  return {"pmPlan": plan, "execution_path": path, "rework_cycles": current_cycles, "status_update": "Breaking down the objective into a detailed plan..."}
341
 
342
+ def _extract_python_blocks(text: str) -> List[str]:
343
+ return re.findall(r"```python\n(.*?)\n```", text, re.DOTALL) or re.findall(r"```\n(.*?)\n```", text, re.DOTALL)
344
+
345
  def run_experimenter_agent(state: AgentState):
346
  log.info("--- 🔬 Running Experimenter Agent ---")
347
  path = ensure_list(state, 'execution_path') + ["Experimenter Agent"]
348
+ pm = state.get('pmPlan', {}) or {}
349
+ if not pm.get('experiment_needed'):
350
  return {"experimentCode": None, "experimentResults": None, "execution_path": path, "status_update": "Proceeding without a code experiment."}
351
+ exp_type = pm.get('experiment_type') or 'notebook'
352
+ goal = pm.get('experiment_goal', 'No goal specified.')
353
+ response = llm.invoke(
354
+ f"Produce content for artifact type '{exp_type}' to achieve: {goal}\n"
355
+ "Return runnable code in fenced code blocks where appropriate, and explanatory text in plaintext."
356
+ )
357
+ llm_text = response.content or ""
358
+ out_dir = "/tmp"
359
+ results = {"success": False, "paths": {}, "stderr": "", "stdout": ""}
360
+ try:
361
+ if exp_type == 'notebook':
362
+ nb_path = write_notebook_from_text(llm_text, out_dir=out_dir)
363
+ results.update({"success": True, "paths": {"notebook": sanitize_path(nb_path)}})
364
+ return {"experimentCode": None, "experimentResults": results, "experiment_llm_text": llm_text, "execution_path": path, "status_update": f"Notebook generated at {nb_path}"}
365
+ elif exp_type == 'excel':
366
+ excel_path = write_excel_from_tables(llm_text, out_dir=out_dir)
367
+ results.update({"success": True, "paths": {"excel": sanitize_path(excel_path)}})
368
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Excel generated at {excel_path}"}
369
+ elif exp_type == 'word':
370
+ docx_path = write_docx_from_text(llm_text, out_dir=out_dir)
371
+ results.update({"success": True, "paths": {"docx": sanitize_path(docx_path)}})
372
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"DOCX generated at {docx_path}"}
373
+ elif exp_type == 'pdf':
374
+ pdf_path = write_pdf_from_text(llm_text, out_dir=out_dir)
375
+ results.update({"success": True, "paths": {"pdf": sanitize_path(pdf_path)}})
376
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"PDF generated at {pdf_path}"}
377
+ elif exp_type == 'script':
378
+ # pick a language hint from plan or goal
379
+ lang_hint = pm.get('experiment_language') or ("python" if ".py" in goal.lower() else None)
380
+ # extract code blocks
381
+ code_blocks = _extract_python_blocks(llm_text)
382
+ if not code_blocks:
383
+ # fallback: entire content
384
+ code_text = llm_text
385
+ else:
386
+ code_text = "\n\n# === BLOCK ===\n\n".join(code_blocks)
387
+ script_path = write_script(code_text, language_hint=lang_hint, out_dir=out_dir)
388
+ # optionally execute python scripts
389
+ exec_results = {}
390
+ if script_path.endswith(".py"):
391
+ exec_results = execute_python_code(open(script_path,"r",encoding="utf-8").read())
392
+ results.update({"success": True, "paths": {"script": sanitize_path(script_path)}, "stdout": exec_results.get("stdout",""), "stderr": exec_results.get("stderr","")})
393
+ return {"experimentCode": code_text, "experimentResults": results, "execution_path": path, "status_update": f"Script generated at {script_path}"}
394
+ elif exp_type == 'repo':
395
+ # build a minimal repo by calling LLM for file suggestions or using code blocks
396
+ # Heuristic: create a simple app repo containing a notebook and README and requirements.txt
397
+ repo_files = {}
398
+ # README from first 400 chars as text
399
+ readme = (llm_text[:1000] + "\n\n") if llm_text else "Generated repo"
400
+ repo_files["README.md"] = readme
401
+ # include generated notebook
402
+ nb_path = write_notebook_from_text(llm_text, out_dir=out_dir)
403
+ repo_files["analysis.ipynb"] = nb_path
404
+ # requirements: keep minimal
405
+ reqs = "nbformat\npandas\nopenpyxl\npython-docx\nreportlab"
406
+ repo_files["requirements.txt"] = reqs
407
+ zip_path = build_repo_zip(repo_files, repo_name="generated_app", out_dir=out_dir)
408
+ results.update({"success": True, "paths": {"repo_zip": sanitize_path(zip_path)}})
409
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Repository ZIP created at {zip_path}"}
410
+ else:
411
+ # fallback: create docx with llm_text
412
+ fallback = write_docx_from_text(llm_text, out_dir=out_dir)
413
+ results.update({"success": True, "paths": {"docx": sanitize_path(fallback)}})
414
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": f"Fallback DOCX generated at {fallback}"}
415
+ except Exception as e:
416
+ log.error(f"Experimenter failed: {e}")
417
+ results.update({"success": False, "stderr": str(e)})
418
+ return {"experimentCode": None, "experimentResults": results, "execution_path": path, "status_update": "Error: Experimenter failed."}
419
 
420
  def run_synthesis_agent(state: AgentState):
421
  log.info("--- ✍️ Running Synthesis Agent ---")
422
  path = ensure_list(state, 'execution_path') + ["Synthesis Agent"]
423
  exp_results = state.get('experimentResults')
424
  results_summary = "No experiment was conducted."
425
+ artifact_message = ""
426
+ if exp_results and isinstance(exp_results, dict):
427
+ paths = exp_results.get("paths") or {}
428
+ if paths:
429
+ artifact_lines = []
430
+ for k,v in paths.items():
431
+ artifact_lines.append(f"- {k}: `{v}`")
432
+ artifact_message = "\n\n**Artifacts produced:**\n" + "\n".join(artifact_lines)
433
+ results_summary = f"Artifacts produced: {list(paths.keys())}"
434
+ else:
435
+ results_summary = f"Experiment Output Stdout: {exp_results.get('stdout','')}\nStderr: {exp_results.get('stderr','')}"
436
  prompt = (
437
  f"Synthesize all information into a final response.\n\nCore Objective: {state.get('coreObjectivePrompt')}\n\n"
438
  f"Plan: {state.get('pmPlan', {}).get('plan_steps')}\n\n{results_summary}\n\nFinal Response:"
439
  )
440
  response = llm.invoke(prompt)
441
+ final_text = response.content or ""
442
+ if artifact_message:
443
+ final_text = final_text + "\n\n" + artifact_message
444
+ return {"draftResponse": final_text, "execution_path": path, "status_update": "Putting together the final response..."}
445
 
446
  def run_qa_agent(state: AgentState):
447
  log.info("--- ✅ Running QA Agent ---")
448
  path = ensure_list(state, 'execution_path') + ["QA Agent"]
449
+ prompt = (f"Review the draft response based on the core objective. Respond ONLY with 'APPROVED' or provide concise feedback for rework.\n\n"
450
+ f"Core Objective: {state.get('coreObjectivePrompt')}\n\nDraft: {state.get('draftResponse')}")
 
 
451
  response = llm.invoke(prompt)
452
  if "APPROVED" in response.content.upper():
453
  return {"approved": True, "qaFeedback": None, "execution_path": path, "status_update": "Response approved!"}
 
457
  def run_archivist_agent(state: AgentState):
458
  log.info("--- 💾 Running Archivist Agent ---")
459
  path = ensure_list(state, 'execution_path') + ["Archivist Agent"]
460
+ summary_prompt = (f"Create a concise summary of this successful task for long-term memory.\n\n"
461
+ f"Core Objective: {state.get('coreObjectivePrompt')}\n\nFinal Response: {state.get('draftResponse')}\n\nMemory Summary:")
 
 
462
  response = llm.invoke(summary_prompt)
463
  memory_manager.add_to_memory(response.content, {"objective": state.get('coreObjectivePrompt')})
464
  return {"execution_path": path, "status_update": "Saving key learnings for future reference..."}
 
466
  def run_disclaimer_agent(state: AgentState):
467
  log.warning("--- ⚠️ Running Disclaimer Agent ---")
468
  path = ensure_list(state, 'execution_path') + ["Disclaimer Agent"]
469
+ disclaimer = ("**DISCLAIMER: The process was stopped after exhausting the budget. The following response is the best available draft and may be incomplete.**\n\n---\n\n")
 
 
470
  final_response = disclaimer + state.get('draftResponse', "No response was generated.")
471
  return {"draftResponse": final_response, "execution_path": path, "status_update": "Budget limit reached. Preparing final draft..."}
472
 
473
+ # --- Decision & Graph ---
474
  def should_continue(state: AgentState):
475
  log.info("--- 🤔 Decision: Is the response QA approved? ---")
476
  if state.get("approved"):
 
484
  return "pm_agent"
485
 
486
  def should_run_experiment(state: AgentState):
487
+ pm = state.get('pmPlan', {}) or {}
488
+ return "experimenter_agent" if pm.get('experiment_needed') else "synthesis_agent"
489
 
490
+ # --- Build graphs (same as before) ---
 
491
  triage_workflow = StateGraph(AgentState)
492
  triage_workflow.add_node("triage", run_triage_agent)
493
  triage_workflow.set_entry_point("triage")
494
  triage_workflow.add_edge("triage", END)
495
  triage_app = triage_workflow.compile()
496
 
 
497
  planner_workflow = StateGraph(AgentState)
498
  planner_workflow.add_node("planner", run_planner_agent)
499
  planner_workflow.set_entry_point("planner")
500
  planner_workflow.add_edge("planner", END)
501
  planner_app = planner_workflow.compile()
502
 
 
503
  main_workflow = StateGraph(AgentState)
504
  main_workflow.add_node("memory_retriever", run_memory_retrieval)
505
  main_workflow.add_node("intent_agent", run_intent_agent)
 
524
  "pm_agent": "pm_agent",
525
  "disclaimer_agent": "disclaimer_agent"
526
  })
527
+ main_app = main_workflow.compile()