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.
- agent_ops_cockpit/agent.py +44 -77
- agent_ops_cockpit/cache/semantic_cache.py +10 -21
- agent_ops_cockpit/cli/main.py +105 -153
- agent_ops_cockpit/eval/load_test.py +33 -50
- agent_ops_cockpit/eval/quality_climber.py +88 -93
- agent_ops_cockpit/eval/red_team.py +84 -25
- agent_ops_cockpit/mcp_server.py +26 -93
- agent_ops_cockpit/ops/arch_review.py +221 -147
- agent_ops_cockpit/ops/auditors/base.py +50 -0
- agent_ops_cockpit/ops/auditors/behavioral.py +31 -0
- agent_ops_cockpit/ops/auditors/compliance.py +35 -0
- agent_ops_cockpit/ops/auditors/dependency.py +48 -0
- agent_ops_cockpit/ops/auditors/finops.py +48 -0
- agent_ops_cockpit/ops/auditors/graph.py +49 -0
- agent_ops_cockpit/ops/auditors/pivot.py +51 -0
- agent_ops_cockpit/ops/auditors/reasoning.py +67 -0
- agent_ops_cockpit/ops/auditors/reliability.py +53 -0
- agent_ops_cockpit/ops/auditors/security.py +87 -0
- agent_ops_cockpit/ops/auditors/sme_v12.py +76 -0
- agent_ops_cockpit/ops/auditors/sovereignty.py +74 -0
- agent_ops_cockpit/ops/auditors/sre_a2a.py +179 -0
- agent_ops_cockpit/ops/benchmarker.py +97 -0
- agent_ops_cockpit/ops/cost_optimizer.py +15 -24
- agent_ops_cockpit/ops/discovery.py +214 -0
- agent_ops_cockpit/ops/evidence_bridge.py +30 -63
- agent_ops_cockpit/ops/frameworks.py +124 -1
- agent_ops_cockpit/ops/git_portal.py +74 -0
- agent_ops_cockpit/ops/mcp_hub.py +19 -42
- agent_ops_cockpit/ops/orchestrator.py +477 -277
- agent_ops_cockpit/ops/policy_engine.py +38 -38
- agent_ops_cockpit/ops/reliability.py +121 -52
- agent_ops_cockpit/ops/remediator.py +54 -0
- agent_ops_cockpit/ops/secret_scanner.py +34 -22
- agent_ops_cockpit/ops/swarm.py +17 -27
- agent_ops_cockpit/ops/ui_auditor.py +67 -6
- agent_ops_cockpit/ops/watcher.py +41 -70
- agent_ops_cockpit/ops/watchlist.json +30 -0
- agent_ops_cockpit/optimizer.py +161 -384
- agent_ops_cockpit/tests/test_arch_review.py +6 -6
- agent_ops_cockpit/tests/test_discovery.py +96 -0
- agent_ops_cockpit/tests/test_ops_core.py +56 -0
- agent_ops_cockpit/tests/test_orchestrator_fleet.py +73 -0
- agent_ops_cockpit/tests/test_persona_architect.py +75 -0
- agent_ops_cockpit/tests/test_persona_finops.py +31 -0
- agent_ops_cockpit/tests/test_persona_security.py +55 -0
- agent_ops_cockpit/tests/test_persona_sre.py +43 -0
- agent_ops_cockpit/tests/test_persona_ux.py +42 -0
- agent_ops_cockpit/tests/test_quality_climber.py +2 -2
- agent_ops_cockpit/tests/test_remediator.py +75 -0
- agent_ops_cockpit/tests/test_ui_auditor.py +52 -0
- agentops_cockpit-0.9.8.dist-info/METADATA +172 -0
- agentops_cockpit-0.9.8.dist-info/RECORD +71 -0
- agent_ops_cockpit/tests/test_optimizer.py +0 -68
- agent_ops_cockpit/tests/test_red_team.py +0 -35
- agent_ops_cockpit/tests/test_secret_scanner.py +0 -24
- agentops_cockpit-0.9.5.dist-info/METADATA +0 -246
- agentops_cockpit-0.9.5.dist-info/RECORD +0 -47
- {agentops_cockpit-0.9.5.dist-info → agentops_cockpit-0.9.8.dist-info}/WHEEL +0 -0
- {agentops_cockpit-0.9.5.dist-info → agentops_cockpit-0.9.8.dist-info}/entry_points.txt +0 -0
- {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 =
|
|
19
|
-
|
|
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
|
|
35
|
-
return f
|
|
36
|
-
return
|
|
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
|