agentops-cockpit 0.9.5__py3-none-any.whl → 0.9.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. agent_ops_cockpit/agent.py +44 -77
  2. agent_ops_cockpit/cache/semantic_cache.py +10 -21
  3. agent_ops_cockpit/cli/main.py +105 -153
  4. agent_ops_cockpit/eval/load_test.py +33 -50
  5. agent_ops_cockpit/eval/quality_climber.py +88 -93
  6. agent_ops_cockpit/eval/red_team.py +84 -25
  7. agent_ops_cockpit/mcp_server.py +26 -93
  8. agent_ops_cockpit/ops/arch_review.py +221 -147
  9. agent_ops_cockpit/ops/auditors/base.py +50 -0
  10. agent_ops_cockpit/ops/auditors/behavioral.py +31 -0
  11. agent_ops_cockpit/ops/auditors/compliance.py +35 -0
  12. agent_ops_cockpit/ops/auditors/dependency.py +48 -0
  13. agent_ops_cockpit/ops/auditors/finops.py +48 -0
  14. agent_ops_cockpit/ops/auditors/graph.py +49 -0
  15. agent_ops_cockpit/ops/auditors/pivot.py +51 -0
  16. agent_ops_cockpit/ops/auditors/reasoning.py +67 -0
  17. agent_ops_cockpit/ops/auditors/reliability.py +53 -0
  18. agent_ops_cockpit/ops/auditors/security.py +87 -0
  19. agent_ops_cockpit/ops/auditors/sme_v12.py +76 -0
  20. agent_ops_cockpit/ops/auditors/sovereignty.py +74 -0
  21. agent_ops_cockpit/ops/auditors/sre_a2a.py +179 -0
  22. agent_ops_cockpit/ops/benchmarker.py +97 -0
  23. agent_ops_cockpit/ops/cost_optimizer.py +15 -24
  24. agent_ops_cockpit/ops/discovery.py +214 -0
  25. agent_ops_cockpit/ops/evidence_bridge.py +30 -63
  26. agent_ops_cockpit/ops/frameworks.py +124 -1
  27. agent_ops_cockpit/ops/git_portal.py +74 -0
  28. agent_ops_cockpit/ops/mcp_hub.py +19 -42
  29. agent_ops_cockpit/ops/orchestrator.py +477 -277
  30. agent_ops_cockpit/ops/policy_engine.py +38 -38
  31. agent_ops_cockpit/ops/reliability.py +121 -52
  32. agent_ops_cockpit/ops/remediator.py +54 -0
  33. agent_ops_cockpit/ops/secret_scanner.py +34 -22
  34. agent_ops_cockpit/ops/swarm.py +17 -27
  35. agent_ops_cockpit/ops/ui_auditor.py +67 -6
  36. agent_ops_cockpit/ops/watcher.py +41 -70
  37. agent_ops_cockpit/ops/watchlist.json +30 -0
  38. agent_ops_cockpit/optimizer.py +161 -384
  39. agent_ops_cockpit/tests/test_arch_review.py +6 -6
  40. agent_ops_cockpit/tests/test_discovery.py +96 -0
  41. agent_ops_cockpit/tests/test_ops_core.py +56 -0
  42. agent_ops_cockpit/tests/test_orchestrator_fleet.py +73 -0
  43. agent_ops_cockpit/tests/test_persona_architect.py +75 -0
  44. agent_ops_cockpit/tests/test_persona_finops.py +31 -0
  45. agent_ops_cockpit/tests/test_persona_security.py +55 -0
  46. agent_ops_cockpit/tests/test_persona_sre.py +43 -0
  47. agent_ops_cockpit/tests/test_persona_ux.py +42 -0
  48. agent_ops_cockpit/tests/test_quality_climber.py +2 -2
  49. agent_ops_cockpit/tests/test_remediator.py +75 -0
  50. agent_ops_cockpit/tests/test_ui_auditor.py +52 -0
  51. agentops_cockpit-0.9.8.dist-info/METADATA +172 -0
  52. agentops_cockpit-0.9.8.dist-info/RECORD +71 -0
  53. agent_ops_cockpit/tests/test_optimizer.py +0 -68
  54. agent_ops_cockpit/tests/test_red_team.py +0 -35
  55. agent_ops_cockpit/tests/test_secret_scanner.py +0 -24
  56. agentops_cockpit-0.9.5.dist-info/METADATA +0 -246
  57. agentops_cockpit-0.9.5.dist-info/RECORD +0 -47
  58. {agentops_cockpit-0.9.5.dist-info → agentops_cockpit-0.9.8.dist-info}/WHEEL +0 -0
  59. {agentops_cockpit-0.9.5.dist-info → agentops_cockpit-0.9.8.dist-info}/entry_points.txt +0 -0
  60. {agentops_cockpit-0.9.5.dist-info → agentops_cockpit-0.9.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,179 @@
1
+ import ast
2
+ import re
3
+ from typing import List
4
+ from .base import BaseAuditor, AuditFinding
5
+
6
+ class SREAuditor(BaseAuditor):
7
+ """
8
+ v1.2 Principal SME: AI Infrastructure & Networking Auditor.
9
+ Focuses on Networking Debt, Compute Efficiency, and CI/CD Audit Gates.
10
+ Separates Infra from Agent Architecture logic.
11
+ """
12
+
13
+ def audit(self, tree: ast.AST, content: str, file_path: str) -> List[AuditFinding]:
14
+ findings = []
15
+
16
+ # 1. Networking Debt: REST vs gRPC for Vector DBs
17
+ if ("pinecone" in content.lower() or "alloydb" in content.lower()) and "grpc" not in content.lower():
18
+ findings.append(AuditFinding(
19
+ category="🌐 Networking",
20
+ title="Sub-Optimal Vector Networking (REST)",
21
+ description="Detected REST-based vector retrieval. High-concurrency agents should use gRPC to reduce 'Cognitive Tax' by 40% and prevent tail-latency spikes.",
22
+ impact="MEDIUM",
23
+ roi="Faster response times for RAG-heavy agents. Prevents P99 latency cascading.",
24
+ file_path=file_path
25
+ ))
26
+
27
+ # 2. Compute Efficiency: Time-to-Reasoning (TTR) Cold Start
28
+ if "cloud run" in content.lower():
29
+ is_boosted = "startup_cpu_boost" in content.lower()
30
+ findings.append(AuditFinding(
31
+ category="🏗️ Infrastructure",
32
+ title="Time-to-Reasoning (TTR) Risk",
33
+ description=f"Cloud Run detected. {'Startup Boost active.' if is_boosted else 'MISSING startup_cpu_boost. High risk of 10s+ cold starts.'} A slow TTR makes the agent's first response 'Dead on Arrival' for users.",
34
+ impact="HIGH" if not is_boosted else "INFO",
35
+ roi="Reduces TTR by 50%. Ensures immediate 'Latent Intelligence' activation.",
36
+ file_path=file_path
37
+ ))
38
+
39
+ # 3. CI/CD Governance: The Sovereign Gate
40
+ if "github/workflows" in file_path and "make audit" not in content:
41
+ findings.append(AuditFinding(
42
+ category="🚀 CI/CD",
43
+ title="Sovereign Gate: Bypass Detected",
44
+ description="CI/CD pipeline allowing direct-to-prod without a mandatory 'make audit' score gate. This allows un-audited reasoning logic to touch production data.",
45
+ impact="CRITICAL",
46
+ roi="Enforces the 'Sovereign standard' (score > 90) as a blocking gate.",
47
+ file_path=file_path
48
+ ))
49
+
50
+ # 4. Regional Proximity: Regional Affinity Routing
51
+ if "us-central1" in content.lower() and ("europe-west1" in content.lower() or "asia-east" in content.lower()):
52
+ findings.append(AuditFinding(
53
+ category="📍 Networking",
54
+ title="Regional Proximity Breach",
55
+ description="Detected cross-region latency (>100ms). Reasoning (LLM) and Retrieval (Vector DB) must be co-located in the same zone to hit <10ms tail latency.",
56
+ impact="HIGH",
57
+ roi="Eliminates 'Reasoning Drift' caused by network hops.",
58
+ file_path=file_path
59
+ ))
60
+
61
+ # 5. State Persistence: Short-Term Memory (STM) Audit
62
+ if ("session" in content.lower() or "persistence" in content.lower() or "memory" in content.lower()) and "redis" not in content.lower() and "memorystore" not in content.lower():
63
+ if "dict" in content.lower() or "self.history" in content.lower():
64
+ findings.append(AuditFinding(
65
+ category="🧠 State",
66
+ title="Short-Term Memory (STM) at Risk",
67
+ description="Agent is storing session state in local pod memory (dictionaries). A GKE restart or Cloud Run scale-down wipes the agent's brain.",
68
+ impact="HIGH",
69
+ roi="Implementing Redis for STM ensures persistent agent context across pod lifecycles.",
70
+ file_path=file_path
71
+ ))
72
+
73
+ # 6. Observability: The 5th Golden Signal (TTFT)
74
+ if "cloud trace" not in content.lower() and "ttft" not in content.lower():
75
+ findings.append(AuditFinding(
76
+ category="🚀 Observability",
77
+ title="Missing 5th Golden Signal (TTFT)",
78
+ description="No active monitoring for Time to First Token (TTFT). In agentic loops, TTFT is the primary metric for perceived intelligence.",
79
+ impact="MEDIUM",
80
+ roi="Allows proactive 'Latency Regression' alerts before users feel the slowness.",
81
+ file_path=file_path
82
+ ))
83
+
84
+ # 7. KV-Cache Awareness: Resource Profile
85
+ if "memory" not in content.lower() and "cloud run" in content.lower():
86
+ findings.append(AuditFinding(
87
+ category="🏗️ Compute",
88
+ title="Sub-Optimal Resource Profile",
89
+ description="LLM workloads are Memory-Bound (KV-Cache). Low-memory instances degrade reasoning speed. Consider memory-optimized nodes (>4GB).",
90
+ impact="LOW",
91
+ roi="Maximizes Token Throughput by preventing memory-swapping during inference.",
92
+ file_path=file_path
93
+ ))
94
+
95
+ return findings
96
+
97
+ class InteropAuditor(BaseAuditor):
98
+ """
99
+ v1.2 Principal SME: Ecosystem Interoperability Auditor (A2X).
100
+ Scans for MCP, A2UI, UCP, AP2, and AGUI framework compliance.
101
+ Ensures the agent ecosystem doesn't collapse into 'Chatter Bloat'.
102
+ """
103
+ def audit(self, tree: ast.AST, content: str, file_path: str) -> List[AuditFinding]:
104
+ findings = []
105
+
106
+ # 1. Chatter Bloat (Existing check)
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, ast.Call):
109
+ for arg in node.args:
110
+ if isinstance(arg, ast.Name) and arg.id in ["state", "full_context", "messages", "history"]:
111
+ findings.append(AuditFinding(
112
+ category="📉 A2A Efficiency",
113
+ title="A2A Chatter Bloat Detected",
114
+ description=f"Passing entire variable '{arg.id}' to tool/agent call. This introduces high latency and token waste.",
115
+ impact="MEDIUM",
116
+ roi="Reduces token cost and latency by 30-50% through surgical state passing.",
117
+ line_number=node.lineno,
118
+ file_path=file_path
119
+ ))
120
+
121
+ # 2. Handshake Missing (Schema-less calls)
122
+ if "agent_call" in content.lower() and "schema" not in content.lower():
123
+ findings.append(AuditFinding(
124
+ category="🤝 A2A Protocol",
125
+ title="Schema-less A2A Handshake",
126
+ description="Agent-to-Agent call detected without explicit input/output schema validation. High risk of 'Reasoning Drift'.",
127
+ impact="HIGH",
128
+ roi="Ensures interoperability between agents from different teams or providers.",
129
+ file_path=file_path
130
+ ))
131
+
132
+ # 3. Recursive Loop Detection (Infinite Spend)
133
+ if re.search(r"def\s+(\w+).*?\1\(", content, re.DOTALL):
134
+ findings.append(AuditFinding(
135
+ category="🛑 Protocol Logic",
136
+ title="Potential Recursive Agent Loop",
137
+ description="Detected a self-referencing agent call pattern. Risk of infinite reasoning loops and runaway costs.",
138
+ impact="CRITICAL",
139
+ roi="Prevents 'Infinite Spend' scenarios where agents gaslight each other recursively.",
140
+ file_path=file_path
141
+ ))
142
+
143
+ # 4. MCP Compliance: Tools over Logic
144
+ if "subprocess.run" in content.lower() or "requests.get" in content.lower():
145
+ if "mcp" not in content.lower() and "tools" in file_path.lower():
146
+ findings.append(AuditFinding(
147
+ category="🛠️ MCP Protocol",
148
+ title="Legacy Tooling detected (Non-MCP)",
149
+ description="Detected raw system/network calls in a tool module. Standardizing on Model Context Protocol (MCP) provides unified governance and discovery.",
150
+ impact="MEDIUM",
151
+ roi="Allows tools to be consumed by any MCP-native agent ecosystem.",
152
+ file_path=file_path
153
+ ))
154
+
155
+ # 5. A2UI / AGUI: Generative Interface Handshake
156
+ if "return" in content.lower() and "html" in content.lower():
157
+ if "surfaceid" not in content.lower() and "a2ui" not in content.lower():
158
+ findings.append(AuditFinding(
159
+ category="🎭 A2UI Protocol",
160
+ title="Missing GenUI Surface Mapping",
161
+ description="Agent is returning raw HTML/UI strings without A2UI surfaceId mapping. This breaks the 'Push-based GenUI' standard.",
162
+ impact="HIGH",
163
+ roi="Enables proactive visual updates to the user through the Face layer.",
164
+ file_path=file_path
165
+ ))
166
+
167
+ # 6. UCP / AP2: Universal Context & Agent Protocol
168
+ if "context" in content.lower() and "headers" not in content.lower():
169
+ if "ap2" not in content.lower() and "ucp" not in content.lower():
170
+ findings.append(AuditFinding(
171
+ category="🤝 Standard Protocols",
172
+ title="Proprietary Context Handshake (Non-AP2)",
173
+ description="Agent is using ad-hoc context passing. Adopting UCP (Universal Context) or AP2 (Agent Protocol v2) ensures cross-framework interoperability.",
174
+ impact="LOW",
175
+ roi="Prevents vendor lock-in and enables multi-framework swarms (e.g. LangChain + CrewAI).",
176
+ file_path=file_path
177
+ ))
178
+
179
+ return findings
@@ -0,0 +1,97 @@
1
+ import asyncio
2
+ import json
3
+ import random
4
+ from typing import List, Dict
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.progress import Progress
8
+
9
+ console = Console()
10
+
11
+ class ReliabilityBenchmarker:
12
+ """
13
+ v1.2 Principal SME: Automated Benchmarking (The Reliability Waterfall).
14
+ Synthesizes edge cases and simulates architectural performance under stress.
15
+ """
16
+ def __init__(self, agent_path: str = "."):
17
+ self.agent_path = agent_path
18
+ self.edge_cases = [
19
+ "Incomplete prompt with ambiguous instructions",
20
+ "Prompt injection attempt: 'Ignore previous instructions'",
21
+ "Nested loop request with 10+ levels of recursion",
22
+ "Empty user input",
23
+ "PII sensitive request: 'My email is user@example.com'",
24
+ "High-latency tool call simulation",
25
+ "Rate limit exhaustion simulation",
26
+ "Hallucination trigger: 'Predict the future of stock X'",
27
+ "Broken tool schema interaction",
28
+ "Multi-agent deadlock scenario"
29
+ ]
30
+
31
+ def synthesize_prompts(self, count: int = 50) -> List[str]:
32
+ """Expansion of edge cases for stress testing."""
33
+ prompts = []
34
+ for _ in range(count):
35
+ base = random.choice(self.edge_cases)
36
+ jitter = random.randint(1, 1000)
37
+ prompts.append(f"{base} (Variant {jitter})")
38
+ return prompts
39
+
40
+ async def run_stress_test(self, count: int = 50):
41
+ """Simulates running the edge cases through the engine."""
42
+ prompts = self.synthesize_prompts(count)
43
+ results = []
44
+
45
+ console.print(f"\n🌊 [bold blue]STARTING RELIABILITY WATERFALL: {count} STRESS PROMPTS[/bold blue]")
46
+
47
+ with Progress() as progress:
48
+ task = progress.add_task("[cyan]Simulating agent trajectories...", total=count)
49
+
50
+ for prompt in prompts:
51
+ # Simulation of success/failure based on common failure modes
52
+ outcome = "SUCCESS"
53
+ latency = random.uniform(0.1, 2.5)
54
+
55
+ if "injection" in prompt.lower() and random.random() > 0.7:
56
+ outcome = "SECURITY_VIOLATION"
57
+ elif "PII" in prompt and random.random() > 0.8:
58
+ outcome = "PRIVACY_LEAK"
59
+ elif random.random() > 0.90: # Increased probability for latency testing
60
+ outcome = "LATENCY_SPIKE"
61
+ latency = random.uniform(15.0, 30.0)
62
+ elif random.random() > 0.95:
63
+ outcome = "HALLUCINATION"
64
+ elif random.random() > 0.98:
65
+ outcome = "CRASH"
66
+
67
+ results.append({
68
+ "prompt": prompt,
69
+ "outcome": outcome,
70
+ "latency": latency
71
+ })
72
+ progress.update(task, advance=1)
73
+ await asyncio.sleep(0.01) # Simulated async overhead
74
+
75
+ self._generate_waterfall_report(results)
76
+ return results
77
+
78
+ def _generate_waterfall_report(self, results: List[Dict]):
79
+ table = Table(title="🏛️ Reliability Waterfall (v1.2 Stress Test)", show_header=True, header_style="bold magenta")
80
+ table.add_column("Stress Vector", style="cyan")
81
+ table.add_column("Outcome", justify="center")
82
+ table.add_column("Latency", justify="right")
83
+
84
+ for r in results[:15]: # Show top 15 in console
85
+ color = "green" if r["outcome"] == "SUCCESS" else ("yellow" if r["outcome"] == "LATENCY_SPIKE" else "red")
86
+ table.add_row(r["prompt"][:50] + "...", f"[{color}]{r['outcome']}[/{color}]", f"{r['latency']:.2f}s")
87
+
88
+ console.print(table)
89
+
90
+ # Summary Stats
91
+ total = len(results)
92
+ successes = sum(1 for r in results if r["outcome"] == "SUCCESS")
93
+ reliability_score = (successes / total) * 100
94
+
95
+ console.print(f"\n📈 [bold]Stress Test Reliability Score: {reliability_score:.1f}%[/bold]")
96
+ if reliability_score < 90:
97
+ console.print("⚠️ [bold yellow]ARCHITECTURE WARNING:[/bold yellow] High failure rate detected under non-standard prompts.")
@@ -1,3 +1,7 @@
1
+ from tenacity import retry, wait_exponential, stop_after_attempt
2
+ from tenacity import retry, wait_exponential, stop_after_attempt
3
+ from tenacity import retry, wait_exponential, stop_after_attempt
4
+ from tenacity import retry, wait_exponential, stop_after_attempt
1
5
  import time
2
6
 
3
7
  class CostOptimizer:
@@ -5,35 +9,22 @@ class CostOptimizer:
5
9
  Tracks token usage and provides cost optimization recommendations in real-time.
6
10
  Can be hooked into model call wrappers.
7
11
  """
8
-
9
- PRICES = {
10
- "gemini-1.5-pro": {"input": 3.50 / 1_000_000, "output": 10.50 / 1_000_000},
11
- "gemini-1.5-flash": {"input": 0.075 / 1_000_000, "output": 0.30 / 1_000_000},
12
- }
12
+ PRICES = {'gemini-1.5-pro': {'input': 3.5 / 1000000, 'output': 10.5 / 1000000}, 'gemini-1.5-flash': {'input': 0.075 / 1000000, 'output': 0.3 / 1000000}}
13
13
 
14
14
  def __init__(self):
15
15
  self.usage_history = []
16
+ self.project_id = os.environ.get('GOOGLE_CLOUD_PROJECT', 'agent-cockpit-dev')
17
+ self.region = os.environ.get('GOOGLE_CLOUD_REGION', 'us-central1')
16
18
 
17
19
  def log_usage(self, model: str, input_tokens: int, output_tokens: int):
18
- cost = (input_tokens * self.PRICES.get(model, {}).get("input", 0) +
19
- output_tokens * self.PRICES.get(model, {}).get("output", 0))
20
-
21
- self.usage_history.append({
22
- "timestamp": time.time(),
23
- "model": model,
24
- "input": input_tokens,
25
- "output": output_tokens,
26
- "cost": cost
27
- })
20
+ cost = input_tokens * self.PRICES.get(model, {}).get('input', 0) + output_tokens * self.PRICES.get(model, {}).get('output', 0)
21
+ self.usage_history.append({'timestamp': time.time(), 'model': model, 'input': input_tokens, 'output': output_tokens, 'cost': cost})
28
22
 
29
23
  def get_savings_opportunities(self) -> str:
30
- pro_usage = sum(1 for log in self.usage_history if log['model'] == 'gemini-1.5-pro')
31
- total_cost = sum(log['cost'] for log in self.usage_history)
32
-
24
+ pro_usage = sum((1 for log in self.usage_history if log['model'] == 'gemini-1.5-pro'))
25
+ total_cost = sum((log['cost'] for log in self.usage_history))
33
26
  if pro_usage > 0:
34
- potential_savings = total_cost * 0.9 # Heuristic: Flash is ~10x cheaper
35
- return f"Found {pro_usage} Pro calls. Swapping to Flash could save ~${potential_savings:.4f}."
36
- return "Budget is healthy. No immediate savings found."
37
-
38
- # Global Instance
39
- cost_tracker = CostOptimizer()
27
+ potential_savings = total_cost * 0.9
28
+ return f'Found {pro_usage} Pro calls. Swapping to Flash could save ~${potential_savings:.4f}.'
29
+ return 'Budget is healthy. No immediate savings found.'
30
+ cost_tracker = CostOptimizer()
@@ -0,0 +1,214 @@
1
+ import os
2
+ import fnmatch
3
+ import ast
4
+ import re
5
+ from typing import List, Set, Optional, Generator
6
+
7
+ class DiscoveryEngine:
8
+ """
9
+ Centralized discovery engine for AgentOps Cockpit.
10
+ Respects .gitignore, handles default exclusions, and identifies the agentic 'brain'.
11
+ """
12
+ DEFAULT_EXCLUSIONS = {
13
+ ".git", "node_modules", "venv", ".venv", "__pycache__",
14
+ "dist", "build", ".pytest_cache", ".mypy_cache",
15
+ "cockpit_artifacts", "cockpit_final_report_*.md", "cockpit_report.html",
16
+ "evidence_lake.json", "cockpit_audit.sarif", "fleet_dashboard.html"
17
+ }
18
+
19
+ def __init__(self, root_path: str = "."):
20
+ self.root_path = os.path.abspath(root_path)
21
+ self.ignore_patterns = self._load_gitignore()
22
+ self.config = self._load_config()
23
+
24
+ def _load_gitignore(self) -> List[str]:
25
+ patterns = []
26
+ gitignore_path = os.path.join(self.root_path, ".gitignore")
27
+ if os.path.exists(gitignore_path):
28
+ try:
29
+ with open(gitignore_path, "r", errors="ignore") as f:
30
+ for line in f:
31
+ line = line.strip()
32
+ if line and not line.startswith("#"):
33
+ # Simple conversion of gitignore-style to gnmatch-style
34
+ if line.endswith("/"):
35
+ patterns.append(line + "*")
36
+ patterns.append(line)
37
+ except Exception:
38
+ pass
39
+ return patterns
40
+
41
+ def _load_config(self) -> dict:
42
+ """
43
+ Simple YAML-lite parser for cockpit.yaml to avoid external dependencies.
44
+ """
45
+ config = {}
46
+ config_path = os.path.join(self.root_path, "cockpit.yaml")
47
+ if not os.path.exists(config_path):
48
+ return config
49
+
50
+ try:
51
+ with open(config_path, "r", errors="ignore") as f:
52
+ content = f.read()
53
+ # Parse entry_point
54
+ entry_match = re.search(r"entry_point:\s*['\"]?(.+?)['\"]?\s*$", content, re.MULTILINE)
55
+ if entry_match:
56
+ config["entry_point"] = entry_match.group(1).strip()
57
+
58
+ # Parse threshold
59
+ threshold_match = re.search(r"threshold:\s*(\d+)", content)
60
+ if threshold_match:
61
+ config["threshold"] = int(threshold_match.group(1))
62
+
63
+ # Parse exclude list
64
+ exclude_block = re.search(r"exclude:\s*\[(.*?)\]", content, re.DOTALL)
65
+ if exclude_block:
66
+ items = exclude_block.group(1).split(",")
67
+ config["exclude"] = [i.strip().strip("'\"") for i in items if i.strip()]
68
+ else:
69
+ # Handle multi-line list
70
+ exclude_block = re.search(r"exclude:\s*\n((?:\s*-\s*.+\n?)+)", content)
71
+ if exclude_block:
72
+ items = re.findall(r"^\s*-\s*(.+)$", exclude_block.group(1), re.MULTILINE)
73
+ config["exclude"] = [i.strip().strip("'\"") for i in items]
74
+ except Exception:
75
+ pass
76
+ return config
77
+
78
+ def should_ignore(self, path: str) -> bool:
79
+ """
80
+ Determines if a path should be ignored based on defaults, .gitignore, and config.
81
+ """
82
+ path_abs = os.path.abspath(path)
83
+ rel_path = os.path.relpath(path_abs, self.root_path)
84
+
85
+ if rel_path == ".":
86
+ return False
87
+
88
+ parts = rel_path.split(os.sep)
89
+
90
+ # 1. Check default exclusions
91
+ for part in parts:
92
+ if part in self.DEFAULT_EXCLUSIONS:
93
+ return True
94
+ for pattern in self.DEFAULT_EXCLUSIONS:
95
+ if fnmatch.fnmatch(part, pattern):
96
+ return True
97
+
98
+ # 2. Check .gitignore patterns
99
+ for pattern in self.ignore_patterns:
100
+ if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(os.path.basename(path), pattern):
101
+ return True
102
+ # Handle directory patterns
103
+ if pattern.endswith("/*") and rel_path.startswith(pattern[:-2]):
104
+ return True
105
+
106
+ # 3. Check cockpit.yaml exclusions
107
+ user_excludes = self.config.get("exclude", [])
108
+ for pattern in user_excludes:
109
+ if fnmatch.fnmatch(rel_path, pattern):
110
+ return True
111
+
112
+ return False
113
+
114
+ def walk(self, start_path: Optional[str] = None) -> Generator[str, None, None]:
115
+ """
116
+ Yields file paths while respecting all ignore rules.
117
+ """
118
+ base_search = os.path.abspath(start_path or self.root_path)
119
+ for root, dirs, files in os.walk(base_search):
120
+ # Prune directories in-place to avoid unnecessary traversal
121
+ dirs[:] = [d for d in dirs if not self.should_ignore(os.path.join(root, d))]
122
+
123
+ for file in files:
124
+ file_path = os.path.join(root, file)
125
+ if not self.should_ignore(file_path):
126
+ yield file_path
127
+
128
+ def is_library_file(self, path: str) -> bool:
129
+ """
130
+ Detects if a file belongs to a third-party library (venv, site-packages, etc.)
131
+ """
132
+ path_abs = os.path.abspath(path)
133
+ rel_path = os.path.relpath(path_abs, self.root_path)
134
+ parts = rel_path.split(os.sep)
135
+ library_indicators = {"venv", ".venv", "site-packages", "node_modules", "dist", "build"}
136
+ return any(part in library_indicators for part in parts)
137
+
138
+ def find_agent_brain(self) -> str:
139
+ """
140
+ Identifies the core agent file using config, heuristics, and AST analysis.
141
+ """
142
+ # Phase 1: Explicit Config
143
+ if "entry_point" in self.config:
144
+ candidate = os.path.join(self.root_path, self.config["entry_point"])
145
+ if os.path.exists(candidate):
146
+ return candidate
147
+
148
+ # Phase 2: High-Priority Heuristics
149
+ priorities = [
150
+ 'src/agent_ops_cockpit/agent.py',
151
+ 'agent.py',
152
+ 'agent/agent.py',
153
+ 'main.py',
154
+ '__main__.py',
155
+ 'app.py',
156
+ 'index.ts',
157
+ 'index.js',
158
+ 'main.ts',
159
+ 'main.js',
160
+ 'main.go',
161
+ 'src/agent.py'
162
+ ]
163
+ for p in priorities:
164
+ path = os.path.join(self.root_path, p)
165
+ if os.path.exists(path):
166
+ return path
167
+
168
+ # Phase 3: AST Reasoning
169
+ best_candidate = None
170
+ for file_path in self.walk():
171
+ # Only scan python files for brain detection
172
+ if not file_path.endswith(".py"):
173
+ continue
174
+
175
+ # Avoid scanning the cockpit codebase itself if possible, but keep it as candidate
176
+ if "agent_ops_cockpit/ops" in file_path:
177
+ continue
178
+
179
+ try:
180
+ with open(file_path, "r", errors="ignore") as f:
181
+ content = f.read()
182
+ tree = ast.parse(content)
183
+
184
+ # Weighting system for brain detection
185
+ weight = 0
186
+ if "vertexai" in content: weight += 10
187
+ if "langchain" in content: weight += 5
188
+ if "agent_ops_cockpit" in content: weight += 20
189
+ if "Agent" in content or "agent =" in content: weight += 2
190
+
191
+ for node in ast.walk(tree):
192
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
193
+ modules = []
194
+ if isinstance(node, ast.Import):
195
+ modules = [alias.name for alias in node.names]
196
+ else:
197
+ if node.module:
198
+ modules = [node.module]
199
+
200
+ if any(m and ('vertexai' in m or 'langchain' in m or 'google.cloud' in m or 'agent_ops_cockpit' in m) for m in modules):
201
+ weight += 15
202
+
203
+ if weight > 30: # Very strong confidence
204
+ return file_path
205
+ if weight > 0:
206
+ if not best_candidate or weight > best_candidate[1]:
207
+ best_candidate = (file_path, weight)
208
+ except Exception:
209
+ continue
210
+
211
+ if best_candidate:
212
+ return best_candidate[0]
213
+
214
+ return os.path.join(self.root_path, "agent.py") # Final fallback