JatinAutonomousLabs commited on
Commit
9049d91
·
verified ·
1 Parent(s): c33c1d5

Update graph_upgraded.py

Browse files
Files changed (1) hide show
  1. graph_upgraded.py +231 -238
graph_upgraded.py CHANGED
@@ -4,322 +4,321 @@ graph_upgraded.py
4
  Adds Pragmatist, Governance, Compliance, Observer, and Knowledge Curator agents
5
  to the existing LangGraph workflow implemented in graph.py
6
 
7
- Drop-in: put next to graph.py and call apply_upgrades() during startup.
 
 
 
8
  """
9
 
10
  import os
11
  import re
12
  import json
13
- import shutil
14
  import logging
15
  from datetime import datetime
16
  from typing import Optional, Dict, Any
17
 
18
  # Import existing graph machinery & helpers from your codebase
19
  import graph as base_graph
20
- from graph import AgentState, ensure_list, ensure_int, sanitize_path, build_repo_zip
21
  from memory_manager import memory_manager
22
  from logging_config import get_logger
23
 
24
  log = get_logger(__name__)
25
-
26
- # Reuse llm instance defined in graph.py
27
  llm = getattr(base_graph, "llm", None)
28
 
29
- # --- Augment AgentState: if used as a TypedDict elsewhere, introduce optional keys via runtime awareness.
30
- # (No static redefinition required; agents read/write these keys safely.)
31
- NEW_STATE_KEYS = [
32
- "pragmatistReport", "governanceReport", "complianceReport",
33
- "observerReport", "knowledgeInsights", "insightMetrics"
34
- ]
35
-
36
- # --- Utility helpers for the new agents ---
37
 
38
  def simple_cost_feasibility_check(pm_plan: Dict[str, Any]) -> Dict[str, Any]:
39
  """
40
- Lightweight feasibility check:
41
- - verifies estimated_cost_usd exists and isn't wildly above a threshold
42
- - checks experiment_type and flags heavy artifacts (repo, notebook)
43
  """
44
  report = {"ok": True, "notes": []}
45
- est_cost = pm_plan.get("estimated_cost_usd", None)
 
 
 
 
46
  exp_type = pm_plan.get("experiment_type", "word")
47
- if est_cost is None:
48
- report["notes"].append("No estimated_cost_usd provided — assume low confidence.")
49
  report["ok"] = False
50
  else:
51
- # Heuristic: if estimated cost > $200, or experiment_type repo, flag for pragmatist review
52
- if est_cost > 200:
53
- report["notes"].append(f"Estimated cost ${est_cost} > $200 — high-cost project.")
54
  report["ok"] = False
55
- if exp_type in ("repo", "notebook", "script") and exp_type != "word":
56
- report["notes"].append(f"Artifact type '{exp_type}' implies heavier engineering effort.")
 
 
 
57
  return report
58
 
59
  def scan_text_for_secrets(text: str) -> Dict[str, Any]:
60
  """
61
- Very small heuristic scanner to detect possible secrets or suspicious tokens.
62
- Not a replacement for dedicated secret scanners, but acts as a safety net.
63
  """
64
  findings = []
65
  if not text:
66
  return {"suspicious": False, "findings": findings}
67
- # look for API-like tokens, private keys, or 'password=' patterns
68
- token_patterns = [
69
- r"AKIA[0-9A-Z]{16}", # AWS access key style
 
70
  r"(?i)secret[_-]?(key|token)\b",
71
- r"(?i)password\s*[:=]\s*['\"][^'\"]{6,}['\"]",
72
- r"-----BEGIN PRIVATE KEY-----",
73
- r"AIza[0-9A-Za-z-_]{35}", # Google API key style
74
  ]
75
- for p in token_patterns:
76
  for m in re.finditer(p, text):
77
  findings.append({"pattern": p, "match": m.group(0)})
78
- suspicious = len(findings) > 0
79
- return {"suspicious": suspicious, "findings": findings}
80
 
81
  def summarize_logs_for_observer(log_paths: Optional[list] = None, sample_lines: int = 200) -> str:
82
  """
83
- Read configured log files (if present) and produce a short textual summary.
84
- Uses heuristics: count ERROR/WARNING, sample latest lines.
85
  """
86
  if not log_paths:
87
- # default to performance log and main log if available
88
- log_paths = []
89
- # common locations
90
- candidates = ["logs/performance.log", "logs/ai_lab.log", "performance.log", "logs/ai_lab.log"]
91
- for c in candidates:
92
- if os.path.exists(c):
93
- log_paths.append(c)
94
- summary_parts = []
95
- error_count = 0
96
- warn_count = 0
97
  for p in log_paths:
98
  try:
99
  with open(p, "r", encoding="utf-8", errors="ignore") as fh:
100
  lines = fh.readlines()[-sample_lines:]
101
- joined = "".join(lines)
102
- error_count += joined.upper().count("ERROR")
103
- warn_count += joined.upper().count("WARNING")
104
- summary_parts.append(f"--- {p} (last {len(lines)} lines) ---\n{joined[:2000]}")
105
  except Exception as e:
106
- summary_parts.append(f"Could not read {p}: {e}")
107
- header = f"Log summary: {error_count} ERROR(s), {warn_count} WARNING(s)"
108
- return header + "\n\n" + "\n\n".join(summary_parts)
109
 
110
  # --- New agent implementations ---
111
 
112
  def run_pragmatist_agent(state: AgentState) -> Dict[str, Any]:
113
  """
114
- Pragmatist: Check feasibility, suggest simplifications, and set a 'pragmatistReport'.
115
- Runs after PM and before Governance/Experimenter.
 
 
116
  """
117
  log.info(">>> PRAGMATIST AGENT")
118
  path = ensure_list(state, "execution_path") + ["Pragmatist"]
119
  pm = state.get("pmPlan", {}) or {}
120
  report = simple_cost_feasibility_check(pm)
121
- # Ask LLM to recommend simplifications if not OK
122
- ldr = ""
123
- if not report["ok"] and llm:
124
- prompt = (
125
- "You are a pragmatic engineering reviewer. The plan and context are:\n\n"
126
- f"Plan: {json.dumps(pm, indent=2)}\n\n"
127
- "Provide up to 5 concrete simplifications / scope-reductions that preserve core value but reduce cost or complexity.\n\n"
128
- "Return a JSON: { 'recommendations': [...], 'confidence': 'low|medium|high' }"
129
- )
130
  try:
 
 
 
 
 
131
  r = llm.invoke(prompt)
132
- ldr = getattr(r, "content", "") or ""
133
- parsed = base_graph.parse_json_from_llm(ldr) or {}
134
- report["llm_recs"] = parsed
 
 
 
135
  except Exception as e:
136
- report["llm_recs_error"] = str(e)
137
- state_update = {"pragmatistReport": report, "execution_path": path, "status_update": "Pragmatist review complete"}
138
- return state_update
139
 
140
  def run_governance_agent(state: AgentState) -> Dict[str, Any]:
141
  """
142
- Governance: Validate budget, SLA, and risk register. Produces 'governanceReport'
143
- and an 'approved_for_experiment' boolean.
 
 
144
  """
145
  log.info(">>> GOVERNANCE AGENT")
146
  path = ensure_list(state, "execution_path") + ["Governance"]
147
  pm = state.get("pmPlan", {}) or {}
148
  prag = state.get("pragmatistReport", {}) or {}
149
- reports = {"budget_ok": True, "issues": [], "approved_for_experiment": True}
150
-
151
- est_cost = pm.get("estimated_cost_usd", None)
152
- # Heuristic thresholds (these can be tuned)
153
- if est_cost is None:
154
- reports["issues"].append("Missing estimated_cost_usd.")
155
- reports["budget_ok"] = False
156
- reports["approved_for_experiment"] = False
157
- else:
158
- # flag if cost > budget in state (if budget exists)
159
- budget = state.get("budget") or state.get("current_budget") or None
160
- if budget:
161
  try:
162
- if float(est_cost) > float(budget):
163
- reports["issues"].append(f"Estimated cost ${est_cost} exceeds budget ${budget}.")
164
- reports["approved_for_experiment"] = False
 
 
 
 
 
 
 
 
 
 
165
  except Exception:
166
- pass
167
- if float(est_cost) > 1000:
168
- reports["issues"].append("Very high estimated cost; governance sign-off required.")
169
- reports["approved_for_experiment"] = False
170
 
171
- # rationalize pragmatist notes
172
  if prag and not prag.get("ok", True):
173
- reports["issues"].append("Pragmatist recommended simplifications.")
174
- reports["approved_for_experiment"] = False
175
 
176
- # Add a light LLM justification summary for governance dashboard
177
- rationale = None
178
  if llm:
179
  try:
180
  prompt = (
181
- "You are a governance assistant. Given the following context, produce a short justification (2-3 sentences) "
182
- "and list critical risks.\n\n"
183
- f"PM Plan: {json.dumps(pm, indent=2)}\n\nPragmatist: {json.dumps(prag, indent=2)}"
184
  )
185
  r = llm.invoke(prompt)
186
- rationale = getattr(r, "content", "") or ""
187
- # trim
188
- rationale = rationale.strip()[:2000]
189
  except Exception as e:
190
- rationale = f"Rationale generation failed: {e}"
191
 
192
- reports["rationale"] = rationale
193
- state_update = {"governanceReport": reports, "execution_path": path, "status_update": "Governance review complete"}
194
- return state_update
195
 
196
  def run_compliance_agent(state: AgentState) -> Dict[str, Any]:
197
  """
198
- Compliance: Static checks on experiment results & generated artifacts.
199
- - Checks for secrets in text outputs
200
- - Flags unsafe file types or missing docs
201
  """
202
  log.info(">>> COMPLIANCE AGENT")
203
  path = ensure_list(state, "execution_path") + ["Compliance"]
204
- exp_results = state.get("experimentResults", {}) or {}
205
- collected_paths = exp_results.get("paths", {}) if isinstance(exp_results, dict) else {}
206
  report = {"suspicious": False, "issues": [], "scanned": []}
207
 
208
- # Scan any text content returned in stdout/stderr
209
- if isinstance(exp_results, dict):
210
- for key in ("stdout", "stderr"):
211
- val = exp_results.get(key)
212
- if isinstance(val, str) and val.strip():
213
- scan = scan_text_for_secrets(val)
214
- if scan.get("suspicious"):
215
- report["suspicious"] = True
216
- report["issues"].append({"type": "text_secret", "where": key, "findings": scan["findings"]})
217
- report["scanned"].append({"type": "text", "where": key})
218
-
219
- # Scan files referenced in paths (if files exist, sample them)
220
- if isinstance(collected_paths, dict):
221
- for k, p in collected_paths.items():
222
- try:
223
- pstr = str(p)
224
- if os.path.exists(pstr):
225
- with open(pstr, "r", encoding="utf-8", errors="ignore") as fh:
226
- sample = fh.read(20000)
227
- scan = scan_text_for_secrets(sample)
228
- if scan.get("suspicious"):
229
- report["suspicious"] = True
230
- report["issues"].append({"type": "file_secret", "file": pstr, "findings": scan["findings"]})
231
- report["scanned"].append({"type": "file", "file": pstr})
232
- else:
233
- report["scanned"].append({"type": "path", "value": pstr, "exists": False})
234
- except Exception as e:
235
- report["scanned"].append({"error": str(e), "path": p})
236
- # Best-effort check for README / requirements in repo zip outputs
237
- if any(str(v).endswith(".zip") for v in collected_paths.values()):
238
- report["notes"] = ["Zip-based artifact(s) detected — manual review recommended."]
239
-
240
- state_update = {"complianceReport": report, "execution_path": path, "status_update": "Compliance checks complete"}
241
- return state_update
 
 
242
 
243
  def run_observer_agent(state: AgentState) -> Dict[str, Any]:
244
  """
245
- Observer: Lightweight log & metric summarizer.
246
- Reads performance logs and recent execution_path to produce quick risk summary.
 
247
  """
248
  log.info(">>> OBSERVER AGENT")
249
  path = ensure_list(state, "execution_path") + ["Observer"]
250
- # Get list of logs from config or default
251
- log_paths = []
252
- # If there's a performance logger path in ENV or state, include it
253
- if os.path.exists("logs/performance.log"):
254
- log_paths.append("logs/performance.log")
255
- if os.path.exists("logs/ai_lab.log"):
256
- log_paths.append("logs/ai_lab.log")
257
- # fallback
258
- summary = summarize_logs_for_observer(log_paths or None)
259
- # Also summarize key execution metrics
260
- exec_path = state.get("execution_path", []) or []
261
- cycles = ensure_int(state, "rework_cycles", 0)
262
- cost = state.get("current_cost", 0.0)
263
  obs = {
264
  "log_summary": summary[:4000],
265
- "execution_length": len(exec_path),
266
- "rework_cycles": cycles,
267
- "current_cost": cost,
268
  "status": state.get("status_update")
269
  }
270
- # Let LLM create a short next-action recommendation
 
271
  if llm:
272
  try:
273
  prompt = (
274
- "You are an Observer assistant. Given this runtime summary, provide 3 prioritized next actions to mitigate risk.\n\n"
275
  f"Runtime summary: {json.dumps(obs, indent=2)}\n\nReturn plain text."
276
  )
277
  r = llm.invoke(prompt)
278
- obs["llm_recommendations"] = getattr(r, "content", "").strip()[:1500]
279
  except Exception as e:
280
  obs["llm_recommendations_error"] = str(e)
281
 
282
- state_update = {"observerReport": obs, "execution_path": path, "status_update": "Observer summary created"}
283
- return state_update
284
 
285
  def run_knowledge_curator_agent(state: AgentState) -> Dict[str, Any]:
286
  """
287
- Knowledge Curator: ingest QA feedback, artifacts and create concise memories
288
- for future runs. Writes into memory_manager with tags.
 
289
  """
290
  log.info(">>> KNOWLEDGE CURATOR AGENT")
291
  path = ensure_list(state, "execution_path") + ["KnowledgeCurator"]
 
 
292
  draft = state.get("draftResponse", "") or ""
293
  qa_feedback = state.get("qaFeedback", "") or ""
294
- pm = state.get("pmPlan", {}) or {}
295
- artifacts = state.get("experimentResults", {}) or {}
296
- summary_text = f"Objective: {state.get('coreObjectivePrompt','')}\n\nPlan: {json.dumps(pm.get('plan_steps', []))}\n\nDraft: {draft[:2000]}\n\nQA: {qa_feedback[:1000]}"
 
 
 
297
  try:
298
  memory_manager.add_to_memory(summary_text, {"source": "knowledge_curator", "timestamp": datetime.utcnow().isoformat()})
299
- insights = {"added": True, "summary_snippet": summary_text[:1000]}
300
  except Exception as e:
301
  insights = {"added": False, "error": str(e)}
302
- state_update = {"knowledgeInsights": insights, "execution_path": path, "status_update": "Knowledge captured"}
303
- return state_update
304
 
305
- # --- Workflow wiring function ---
306
 
307
  def apply_upgrades():
308
  """
309
- Inject new nodes and edges into base_graph.main_workflow (LangGraph StateGraph),
310
- preserving the existing flow and not overturning current design.
311
-
312
- This version is defensive: StateGraph may not provide has_node/has_edge methods,
313
- so we attempt adds and ignore Duplicate/AlreadyExists errors.
314
  """
315
- log.info("Applying graph upgrades: adding Pragmatist, Governance, Compliance, Observer, KnowledgeCurator")
316
-
317
  try:
318
  mw = getattr(base_graph, "main_workflow", None)
319
  if mw is None:
320
- raise RuntimeError("base_graph.main_workflow not found. Ensure graph.py has created main_workflow.")
321
 
322
- # Nodes to add
323
  node_map = {
324
  "pragmatist_agent": run_pragmatist_agent,
325
  "governance_agent": run_governance_agent,
@@ -328,37 +327,34 @@ def apply_upgrades():
328
  "knowledge_curator_agent": run_knowledge_curator_agent
329
  }
330
 
331
- # Add nodes defensively
332
- for name, fn in node_map.items():
333
  try:
334
- mw.add_node(name, fn)
335
- log.info(f"Added node: {name}")
336
  except Exception as e:
337
- # If node already exists or other non-fatal issue, continue
338
- log.debug(f"Could not add node '{name}' (may already exist): {e}")
339
 
340
- # Connect edges: pm_agent -> pragmatist_agent -> governance_agent -> experimenter_agent
341
- # If governance rejects, route back to pm_agent
342
- def governance_decider(state: AgentState):
343
- gov = state.get("governanceReport", {}) or {}
344
- if gov.get("approved_for_experiment", True) is True:
345
- return "experimenter_agent"
346
- return "pm_agent"
347
-
348
- # Add edges defensively
349
  try:
350
  mw.add_edge("pm_agent", "pragmatist_agent")
351
- log.info("Added edge: pm_agent -> pragmatist_agent")
352
  except Exception as e:
353
- log.debug(f"Edge pm_agent -> pragmatist_agent not added: {e}")
354
 
355
  try:
356
  mw.add_edge("pragmatist_agent", "governance_agent")
357
- log.info("Added edge: pragmatist_agent -> governance_agent")
358
  except Exception as e:
359
- log.debug(f"Edge pragmatist_agent -> governance_agent not added: {e}")
 
 
 
 
 
 
 
360
 
361
- # Add conditional edges for governance_decider
362
  try:
363
  mw.add_conditional_edges("governance_agent", governance_decider, {
364
  "experimenter_agent": "experimenter_agent",
@@ -366,77 +362,74 @@ def apply_upgrades():
366
  })
367
  log.info("Added conditional edges for governance_agent")
368
  except Exception as e:
369
- log.debug(f"Could not add conditional edges for governance_agent: {e}")
370
 
371
- # After experimenter -> compliance_agent -> synthesis_agent
372
  try:
373
  mw.add_edge("experimenter_agent", "compliance_agent")
374
- log.info("Added edge: experimenter_agent -> compliance_agent")
375
  except Exception as e:
376
- log.debug(f"Edge experimenter_agent -> compliance_agent not added: {e}")
377
 
378
  try:
379
  mw.add_edge("compliance_agent", "synthesis_agent")
380
- log.info("Added edge: compliance_agent -> synthesis_agent")
381
  except Exception as e:
382
- log.debug(f"Edge compliance_agent -> synthesis_agent not added: {e}")
383
 
384
- # Ensure synthesis -> qa still exists (likely present) and add qa -> observer_agent
385
  try:
386
  mw.add_edge("synthesis_agent", "qa_agent")
387
- log.info("Ensured edge: synthesis_agent -> qa_agent")
388
  except Exception as e:
389
- log.debug(f"Edge synthesis_agent -> qa_agent not added/exists: {e}")
390
 
391
  try:
392
  mw.add_edge("qa_agent", "observer_agent")
393
- log.info("Added edge: qa_agent -> observer_agent")
394
  except Exception as e:
395
- log.debug(f"Edge qa_agent -> observer_agent not added: {e}")
396
 
397
  try:
398
  mw.add_edge("observer_agent", "archivist_agent")
399
- log.info("Added edge: observer_agent -> archivist_agent")
400
  except Exception as e:
401
- log.debug(f"Edge observer_agent -> archivist_agent not added: {e}")
402
 
403
- # Insert knowledge curator between archivist and END
404
  try:
405
  mw.add_edge("archivist_agent", "knowledge_curator_agent")
406
- log.info("Added edge: archivist_agent -> knowledge_curator_agent")
407
  except Exception as e:
408
- log.debug(f"Edge archivist_agent -> knowledge_curator_agent not added: {e}")
409
 
410
  try:
411
  mw.add_edge("knowledge_curator_agent", base_graph.END)
412
- log.info("Added edge: knowledge_curator_agent -> END")
413
  except Exception as e:
414
- log.debug(f"Edge knowledge_curator_agent -> END not added: {e}")
415
 
416
- # Ensure disclaimer path is preserved (safe-guard: add qa -> disclaimer if missing)
417
  try:
418
  mw.add_edge("qa_agent", "disclaimer_agent")
419
- log.info("Ensured edge: qa_agent -> disclaimer_agent")
420
  except Exception as e:
421
- log.debug(f"Edge qa_agent -> disclaimer_agent not added/exists: {e}")
422
 
423
- # Recompile the main app to include changes
424
  try:
425
  base_graph.main_app = mw.compile()
426
- log.info("Recompiled main_workflow -> main_app successfully.")
427
  except Exception as e:
428
- log.warning(f"Could not recompile main_workflow: {e}")
429
 
430
  log.info("Graph upgrades applied successfully.")
431
  return True
432
 
433
  except Exception as e:
434
- log.exception(f"Failed to apply graph upgrades: {e}")
435
  return False
436
 
437
- # optional convenience: apply on import if desired (commented out by default)
438
- # apply_upgrades()
439
-
440
- if __name__ == "__main__":
441
- ok = apply_upgrades()
442
- print("Apply upgrades:", ok)
 
4
  Adds Pragmatist, Governance, Compliance, Observer, and Knowledge Curator agents
5
  to the existing LangGraph workflow implemented in graph.py
6
 
7
+ Designed to be defensive: attempts to add nodes/edges and ignores duplicates/errors,
8
+ then recompiles the main_app so the new agents are available.
9
+
10
+ Save next to graph.py and call apply_upgrades() during startup.
11
  """
12
 
13
  import os
14
  import re
15
  import json
 
16
  import logging
17
  from datetime import datetime
18
  from typing import Optional, Dict, Any
19
 
20
  # Import existing graph machinery & helpers from your codebase
21
  import graph as base_graph
22
+ from graph import AgentState, ensure_list, ensure_int
23
  from memory_manager import memory_manager
24
  from logging_config import get_logger
25
 
26
  log = get_logger(__name__)
 
 
27
  llm = getattr(base_graph, "llm", None)
28
 
29
+ # --- Utility helpers ---
 
 
 
 
 
 
 
30
 
31
  def simple_cost_feasibility_check(pm_plan: Dict[str, Any]) -> Dict[str, Any]:
32
  """
33
+ Heuristic cost/complexity check for a PM plan.
34
+ Returns a minimal report dict.
 
35
  """
36
  report = {"ok": True, "notes": []}
37
+ try:
38
+ est_cost = float(pm_plan.get("estimated_cost_usd", 0) or 0)
39
+ except Exception:
40
+ est_cost = None
41
+
42
  exp_type = pm_plan.get("experiment_type", "word")
43
+ if est_cost is None or est_cost == 0:
44
+ report["notes"].append("No reliable estimated_cost_usd provided.")
45
  report["ok"] = False
46
  else:
47
+ if est_cost > 500:
48
+ report["notes"].append(f"High estimated cost: ${est_cost}. Governance advised.")
 
49
  report["ok"] = False
50
+ elif est_cost > 200:
51
+ report["notes"].append(f"Moderately high estimated cost: ${est_cost}. Consider simplifications.")
52
+
53
+ if exp_type in ("repo", "notebook", "script"):
54
+ report["notes"].append(f"Artifact type '{exp_type}' indicates engineering-heavy work.")
55
  return report
56
 
57
  def scan_text_for_secrets(text: str) -> Dict[str, Any]:
58
  """
59
+ Lightweight heuristic scanner for secrets/tokens in text.
60
+ Not a replacement for dedicated scanners but useful as early warning.
61
  """
62
  findings = []
63
  if not text:
64
  return {"suspicious": False, "findings": findings}
65
+ patterns = [
66
+ r"AKIA[0-9A-Z]{16}", # AWS
67
+ r"-----BEGIN PRIVATE KEY-----", # private key marker
68
+ r"AIza[0-9A-Za-z-_]{35}", # Google style
69
  r"(?i)secret[_-]?(key|token)\b",
70
+ r"(?i)password\s*[:=]\s*['\"][^'\"]{6,}['\"]"
 
 
71
  ]
72
+ for p in patterns:
73
  for m in re.finditer(p, text):
74
  findings.append({"pattern": p, "match": m.group(0)})
75
+ return {"suspicious": len(findings) > 0, "findings": findings}
 
76
 
77
  def summarize_logs_for_observer(log_paths: Optional[list] = None, sample_lines: int = 200) -> str:
78
  """
79
+ Very small log summarizer: counts ERROR/WARNING and returns last lines.
 
80
  """
81
  if not log_paths:
82
+ candidates = ["logs/performance.log", "logs/ai_lab.log", "performance.log"]
83
+ log_paths = [p for p in candidates if os.path.exists(p)]
84
+ parts = []
85
+ errs = 0
86
+ warns = 0
 
 
 
 
 
87
  for p in log_paths:
88
  try:
89
  with open(p, "r", encoding="utf-8", errors="ignore") as fh:
90
  lines = fh.readlines()[-sample_lines:]
91
+ content = "".join(lines)
92
+ errs += content.upper().count("ERROR")
93
+ warns += content.upper().count("WARNING")
94
+ parts.append(f"--- {p} (last {len(lines)} lines) ---\n{content[:2000]}")
95
  except Exception as e:
96
+ parts.append(f"Could not read {p}: {e}")
97
+ header = f"Log summary: {errs} ERROR(s), {warns} WARNING(s)"
98
+ return header + "\n\n" + "\n\n".join(parts)
99
 
100
  # --- New agent implementations ---
101
 
102
  def run_pragmatist_agent(state: AgentState) -> Dict[str, Any]:
103
  """
104
+ Pragmatist Agent:
105
+ - Performs quick feasibility/cost checks for the PM plan
106
+ - Optionally asks LLM for 1-5 simplification suggestions
107
+ - Writes 'pragmatistReport' into state
108
  """
109
  log.info(">>> PRAGMATIST AGENT")
110
  path = ensure_list(state, "execution_path") + ["Pragmatist"]
111
  pm = state.get("pmPlan", {}) or {}
112
  report = simple_cost_feasibility_check(pm)
113
+
114
+ # If the plan is flagged, ask the LLM for pragmatic simplifications (best-effort)
115
+ if not report.get("ok", True) and llm:
 
 
 
 
 
 
116
  try:
117
+ prompt = (
118
+ "You are a pragmatic engineering reviewer. Given this plan, suggest up to 5"
119
+ " concrete simplifications to preserve core user value while reducing cost/complexity.\n\n"
120
+ f"Plan: {json.dumps(pm, indent=2)}\n\nReturn JSON: {{'recommendations': [...], 'confidence': 'low|medium|high'}}"
121
+ )
122
  r = llm.invoke(prompt)
123
+ content = getattr(r, "content", "") or ""
124
+ parsed = base_graph.parse_json_from_llm(content)
125
+ if isinstance(parsed, dict):
126
+ report["llm_recommendations"] = parsed.get("recommendations") or parsed
127
+ else:
128
+ report["llm_recommendations_text"] = content.strip()[:1000]
129
  except Exception as e:
130
+ report["llm_recommendations_error"] = str(e)
131
+
132
+ return {"pragmatistReport": report, "execution_path": path, "status_update": "Pragmatist review complete"}
133
 
134
  def run_governance_agent(state: AgentState) -> Dict[str, Any]:
135
  """
136
+ Governance Agent:
137
+ - Validates budget vs estimated_cost_usd
138
+ - Considers pragmatistReport notes
139
+ - Produces governanceReport with approved_for_experiment boolean
140
  """
141
  log.info(">>> GOVERNANCE AGENT")
142
  path = ensure_list(state, "execution_path") + ["Governance"]
143
  pm = state.get("pmPlan", {}) or {}
144
  prag = state.get("pragmatistReport", {}) or {}
145
+ report = {"budget_ok": True, "issues": [], "approved_for_experiment": True}
146
+
147
+ try:
148
+ est = pm.get("estimated_cost_usd", None)
149
+ if est is None:
150
+ report["issues"].append("Missing estimated_cost_usd.")
151
+ report["budget_ok"] = False
152
+ report["approved_for_experiment"] = False
153
+ else:
 
 
 
154
  try:
155
+ est_f = float(est)
156
+ # If the caller provided a budget in state, enforce it
157
+ budget = state.get("current_budget") or state.get("budget") or None
158
+ if budget:
159
+ try:
160
+ if est_f > float(budget):
161
+ report["issues"].append(f"Estimated ${est_f} exceeds budget ${budget}.")
162
+ report["approved_for_experiment"] = False
163
+ except Exception:
164
+ pass
165
+ if est_f > 1000:
166
+ report["issues"].append("Very high estimated cost; manual governance required.")
167
+ report["approved_for_experiment"] = False
168
  except Exception:
169
+ report["issues"].append("Could not parse estimated_cost_usd.")
170
+ except Exception as e:
171
+ report["issues"].append(f"Governance encountered an error: {e}")
172
+ report["approved_for_experiment"] = False
173
 
174
+ # Factor in pragmatist findings
175
  if prag and not prag.get("ok", True):
176
+ report["issues"].append("Pragmatist recommended simplifications.")
177
+ report["approved_for_experiment"] = False
178
 
179
+ # LLM-produced rationale (optional, best-effort)
 
180
  if llm:
181
  try:
182
  prompt = (
183
+ "You are a governance assistant. Summarize in 2-3 sentences whether the plan is safe to run and list 3 critical risks.\n\n"
184
+ f"Plan: {json.dumps(pm, indent=2)}\n\nPragmatist: {json.dumps(prag, indent=2)}"
 
185
  )
186
  r = llm.invoke(prompt)
187
+ report["rationale"] = getattr(r, "content", "")[:2000]
 
 
188
  except Exception as e:
189
+ report["rationale_error"] = str(e)
190
 
191
+ return {"governanceReport": report, "execution_path": path, "status_update": "Governance review complete"}
 
 
192
 
193
  def run_compliance_agent(state: AgentState) -> Dict[str, Any]:
194
  """
195
+ Compliance Agent:
196
+ - Scans experimentResults' stdout/stderr and files for secrets or suspicious content
197
+ - Produces complianceReport with suspicious flag and issues
198
  """
199
  log.info(">>> COMPLIANCE AGENT")
200
  path = ensure_list(state, "execution_path") + ["Compliance"]
201
+ exp = state.get("experimentResults", {}) or {}
 
202
  report = {"suspicious": False, "issues": [], "scanned": []}
203
 
204
+ # scan stdout/stderr
205
+ for key in ("stdout", "stderr"):
206
+ val = exp.get(key)
207
+ if isinstance(val, str) and val.strip():
208
+ scan = scan_text_for_secrets(val)
209
+ if scan.get("suspicious"):
210
+ report["suspicious"] = True
211
+ report["issues"].append({"type": "text_secret", "where": key, "findings": scan["findings"]})
212
+ report["scanned"].append({"type": "text", "where": key})
213
+
214
+ # scan file paths reported
215
+ paths = {}
216
+ if isinstance(exp, dict) and "paths" in exp:
217
+ paths = exp.get("paths") or {}
218
+ if isinstance(paths, dict):
219
+ for k, p in paths.items():
220
+ try:
221
+ pstr = str(p)
222
+ if os.path.exists(pstr) and os.path.isfile(pstr):
223
+ with open(pstr, "r", encoding="utf-8", errors="ignore") as fh:
224
+ sample = fh.read(20000)
225
+ scan = scan_text_for_secrets(sample)
226
+ if scan.get("suspicious"):
227
+ report["suspicious"] = True
228
+ report["issues"].append({"type": "file_secret", "file": pstr, "findings": scan["findings"]})
229
+ report["scanned"].append({"type": "file", "file": pstr})
230
+ else:
231
+ report["scanned"].append({"type": "path", "value": pstr, "exists": os.path.exists(pstr)})
232
+ except Exception as e:
233
+ report["scanned"].append({"file": p, "error": str(e)})
234
+
235
+ # If repo zip(s) detected, flag for manual review
236
+ if any(str(v).lower().endswith(".zip") for v in (paths.values() if isinstance(paths, dict) else [])):
237
+ report["notes"] = ["Zip-based or repo artifact detected — recommend manual review."]
238
+
239
+ return {"complianceReport": report, "execution_path": path, "status_update": "Compliance checks complete"}
240
 
241
  def run_observer_agent(state: AgentState) -> Dict[str, Any]:
242
  """
243
+ Observer Agent:
244
+ - Reads available logs (best-effort) and summarizes ERROR/WARNING counts
245
+ - Produces short LLM recommendations for prioritized actions
246
  """
247
  log.info(">>> OBSERVER AGENT")
248
  path = ensure_list(state, "execution_path") + ["Observer"]
249
+
250
+ # choose likely log files
251
+ log_candidates = []
252
+ for candidate in ["logs/performance.log", "logs/ai_lab.log", "performance.log"]:
253
+ if os.path.exists(candidate):
254
+ log_candidates.append(candidate)
255
+
256
+ summary = summarize_logs_for_observer(log_candidates or None)
257
+ exec_len = len(state.get("execution_path", []) or [])
258
+ rework_cycles = ensure_int(state, "rework_cycles", 0)
259
+ current_cost = state.get("current_cost", 0.0)
260
+
 
261
  obs = {
262
  "log_summary": summary[:4000],
263
+ "execution_length": exec_len,
264
+ "rework_cycles": rework_cycles,
265
+ "current_cost": current_cost,
266
  "status": state.get("status_update")
267
  }
268
+
269
+ # let LLM recommend 1-3 prioritized mitigation actions
270
  if llm:
271
  try:
272
  prompt = (
273
+ "You are an Observer assistant. Given this runtime summary, provide 3 prioritized next actions to mitigate the top risks.\n\n"
274
  f"Runtime summary: {json.dumps(obs, indent=2)}\n\nReturn plain text."
275
  )
276
  r = llm.invoke(prompt)
277
+ obs["llm_recommendations"] = getattr(r, "content", "")[:1500]
278
  except Exception as e:
279
  obs["llm_recommendations_error"] = str(e)
280
 
281
+ return {"observerReport": obs, "execution_path": path, "status_update": "Observer summary created"}
 
282
 
283
  def run_knowledge_curator_agent(state: AgentState) -> Dict[str, Any]:
284
  """
285
+ Knowledge Curator Agent:
286
+ - Creates a concise memory summary combining objective, plan, draft, and QA feedback
287
+ - Writes into memory_manager for future reuse
288
  """
289
  log.info(">>> KNOWLEDGE CURATOR AGENT")
290
  path = ensure_list(state, "execution_path") + ["KnowledgeCurator"]
291
+ core = state.get("coreObjectivePrompt", "") or state.get("userInput", "")
292
+ pm = state.get("pmPlan", {}) or {}
293
  draft = state.get("draftResponse", "") or ""
294
  qa_feedback = state.get("qaFeedback", "") or ""
295
+ summary_text = (
296
+ f"Objective: {core}\n\n"
297
+ f"Plan Steps: {json.dumps(pm.get('plan_steps', []))}\n\n"
298
+ f"Draft (first 1500 chars): {draft[:1500]}\n\n"
299
+ f"QA Feedback: {qa_feedback[:1000]}"
300
+ )
301
  try:
302
  memory_manager.add_to_memory(summary_text, {"source": "knowledge_curator", "timestamp": datetime.utcnow().isoformat()})
303
+ insights = {"added": True, "summary_snippet": summary_text[:500]}
304
  except Exception as e:
305
  insights = {"added": False, "error": str(e)}
306
+ return {"knowledgeInsights": insights, "execution_path": path, "status_update": "Knowledge captured"}
 
307
 
308
+ # --- Wiring / injection into existing main_workflow ---
309
 
310
  def apply_upgrades():
311
  """
312
+ Inject nodes and edges into base_graph.main_workflow (LangGraph StateGraph).
313
+ Attempts to add nodes/edges defensively and recompiles the main_app.
314
+ Returns True on success (no exceptions thrown), False otherwise.
 
 
315
  """
316
+ log.info("Applying graph upgrades: Pragmatist, Governance, Compliance, Observer, KnowledgeCurator")
 
317
  try:
318
  mw = getattr(base_graph, "main_workflow", None)
319
  if mw is None:
320
+ raise RuntimeError("base_graph.main_workflow not found. Ensure graph.py created main_workflow before applying upgrades.")
321
 
 
322
  node_map = {
323
  "pragmatist_agent": run_pragmatist_agent,
324
  "governance_agent": run_governance_agent,
 
327
  "knowledge_curator_agent": run_knowledge_curator_agent
328
  }
329
 
330
+ # Add nodes (defensive)
331
+ for nm, fn in node_map.items():
332
  try:
333
+ mw.add_node(nm, fn)
334
+ log.info("Added node: %s", nm)
335
  except Exception as e:
336
+ log.debug("Could not add node %s (may already exist): %s", nm, e)
 
337
 
338
+ # Connect pm -> pragmatist -> governance -> (experimenter or back to pm)
 
 
 
 
 
 
 
 
339
  try:
340
  mw.add_edge("pm_agent", "pragmatist_agent")
341
+ log.info("Edge: pm_agent -> pragmatist_agent")
342
  except Exception as e:
343
+ log.debug("pm_agent -> pragmatist_agent: %s", e)
344
 
345
  try:
346
  mw.add_edge("pragmatist_agent", "governance_agent")
347
+ log.info("Edge: pragmatist_agent -> governance_agent")
348
  except Exception as e:
349
+ log.debug("pragmatist_agent -> governance_agent: %s", e)
350
+
351
+ # conditional from governance_agent
352
+ def governance_decider(state: AgentState):
353
+ gov = state.get("governanceReport", {}) or {}
354
+ if gov.get("approved_for_experiment", True) is True:
355
+ return "experimenter_agent"
356
+ return "pm_agent"
357
 
 
358
  try:
359
  mw.add_conditional_edges("governance_agent", governance_decider, {
360
  "experimenter_agent": "experimenter_agent",
 
362
  })
363
  log.info("Added conditional edges for governance_agent")
364
  except Exception as e:
365
+ log.debug("Could not add conditional edges for governance_agent: %s", e)
366
 
367
+ # experimenter -> compliance -> synthesis
368
  try:
369
  mw.add_edge("experimenter_agent", "compliance_agent")
370
+ log.info("Edge: experimenter_agent -> compliance_agent")
371
  except Exception as e:
372
+ log.debug("experimenter_agent -> compliance_agent: %s", e)
373
 
374
  try:
375
  mw.add_edge("compliance_agent", "synthesis_agent")
376
+ log.info("Edge: compliance_agent -> synthesis_agent")
377
  except Exception as e:
378
+ log.debug("compliance_agent -> synthesis_agent: %s", e)
379
 
380
+ # synthesis -> qa (likely already present) and then qa -> observer -> archivist
381
  try:
382
  mw.add_edge("synthesis_agent", "qa_agent")
383
+ log.info("Edge: synthesis_agent -> qa_agent ensured/added")
384
  except Exception as e:
385
+ log.debug("synthesis_agent -> qa_agent: %s", e)
386
 
387
  try:
388
  mw.add_edge("qa_agent", "observer_agent")
389
+ log.info("Edge: qa_agent -> observer_agent")
390
  except Exception as e:
391
+ log.debug("qa_agent -> observer_agent: %s", e)
392
 
393
  try:
394
  mw.add_edge("observer_agent", "archivist_agent")
395
+ log.info("Edge: observer_agent -> archivist_agent")
396
  except Exception as e:
397
+ log.debug("observer_agent -> archivist_agent: %s", e)
398
 
399
+ # archivist -> knowledge_curator -> END
400
  try:
401
  mw.add_edge("archivist_agent", "knowledge_curator_agent")
402
+ log.info("Edge: archivist_agent -> knowledge_curator_agent")
403
  except Exception as e:
404
+ log.debug("archivist_agent -> knowledge_curator_agent: %s", e)
405
 
406
  try:
407
  mw.add_edge("knowledge_curator_agent", base_graph.END)
408
+ log.info("Edge: knowledge_curator_agent -> END")
409
  except Exception as e:
410
+ log.debug("knowledge_curator_agent -> END: %s", e)
411
 
412
+ # Ensure qa -> disclaimer path still exists (add defensively)
413
  try:
414
  mw.add_edge("qa_agent", "disclaimer_agent")
415
+ log.info("Edge: qa_agent -> disclaimer_agent ensured/added")
416
  except Exception as e:
417
+ log.debug("qa_agent -> disclaimer_agent: %s", e)
418
 
419
+ # Recompile main_app
420
  try:
421
  base_graph.main_app = mw.compile()
422
+ log.info("Recompiled main_workflow -> main_app")
423
  except Exception as e:
424
+ log.warning("Could not recompile main_workflow: %s", e)
425
 
426
  log.info("Graph upgrades applied successfully.")
427
  return True
428
 
429
  except Exception as e:
430
+ log.exception("Failed to apply graph upgrades: %s", e)
431
  return False
432
 
433
+ # optional: auto-apply on import (commented out to avoid surprising side-effects)
434
+ # if __name__ == "__main__":
435
+ # print("Applying upgrades:", apply_upgrades())