narcolepticchicken commited on
Commit
de021eb
·
verified ·
1 Parent(s): f8a2d6d

Upload aco/telemetry.py

Browse files
Files changed (1) hide show
  1. aco/telemetry.py +123 -0
aco/telemetry.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cost Telemetry Collector - Module 1."""
2
+
3
+ import json
4
+ import os
5
+ from typing import List, Dict, Any, Optional
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from .trace_schema import AgentTrace, TraceStep, ModelCall, ToolCall, VerifierCall, TaskType, Outcome, FailureTag
10
+
11
+
12
+ class CostTelemetryCollector:
13
+ """Collects structured telemetry from agent runs and persists normalized traces."""
14
+
15
+ def __init__(self, storage_path: str = "./traces"):
16
+ self.storage_path = Path(storage_path)
17
+ self.storage_path.mkdir(parents=True, exist_ok=True)
18
+ self._pending: Dict[str, AgentTrace] = {}
19
+
20
+ def start_trace(self, trace_id: str, user_request: str, task_type: TaskType) -> AgentTrace:
21
+ trace = AgentTrace(
22
+ trace_id=trace_id,
23
+ user_request=user_request,
24
+ task_type=task_type,
25
+ )
26
+ self._pending[trace_id] = trace
27
+ return trace
28
+
29
+ def add_step(
30
+ self,
31
+ trace_id: str,
32
+ step_id: str,
33
+ model_call: ModelCall,
34
+ tool_calls: Optional[List[ToolCall]] = None,
35
+ verifier_calls: Optional[List[VerifierCall]] = None,
36
+ context_size_tokens: int = 0,
37
+ context_sources: Optional[List[str]] = None,
38
+ retry_count: int = 0,
39
+ recovery_action: Optional[str] = None,
40
+ artifacts_created: Optional[List[str]] = None,
41
+ step_outcome: Optional[Outcome] = None,
42
+ ) -> None:
43
+ trace = self._pending.get(trace_id)
44
+ if not trace:
45
+ raise ValueError(f"Trace {trace_id} not found")
46
+
47
+ step = TraceStep(
48
+ step_id=step_id,
49
+ timestamp=datetime.utcnow(),
50
+ task_type=trace.task_type,
51
+ model_call=model_call,
52
+ tool_calls=tool_calls or [],
53
+ verifier_calls=verifier_calls or [],
54
+ context_size_tokens=context_size_tokens,
55
+ context_sources=context_sources or [],
56
+ retry_count=retry_count,
57
+ recovery_action=recovery_action,
58
+ artifacts_created=artifacts_created or [],
59
+ step_outcome=step_outcome,
60
+ )
61
+ trace.steps.append(step)
62
+
63
+ def finalize_trace(
64
+ self,
65
+ trace_id: str,
66
+ final_outcome: Outcome,
67
+ failure_tags: Optional[List[FailureTag]] = None,
68
+ user_satisfaction: Optional[float] = None,
69
+ total_cost_saved_vs_frontier: Optional[float] = None,
70
+ optimal_cost: Optional[float] = None,
71
+ metadata: Optional[Dict[str, Any]] = None,
72
+ ) -> AgentTrace:
73
+ trace = self._pending.pop(trace_id)
74
+ trace.final_outcome = final_outcome
75
+ trace.failure_tags = failure_tags or []
76
+ trace.user_satisfaction = user_satisfaction
77
+ trace.total_cost_saved_vs_frontier = total_cost_saved_vs_frontier
78
+ trace.optimal_cost = optimal_cost
79
+ trace.metadata = metadata or {}
80
+ trace.total_cost = trace.total_cost_computed
81
+
82
+ self._persist(trace)
83
+ return trace
84
+
85
+ def _persist(self, trace: AgentTrace) -> None:
86
+ filepath = self.storage_path / f"{trace.trace_id}.json"
87
+ with open(filepath, "w") as f:
88
+ json.dump(trace.to_dict(), f, indent=2, default=str)
89
+
90
+ def load_trace(self, trace_id: str) -> Optional[AgentTrace]:
91
+ filepath = self.storage_path / f"{trace_id}.json"
92
+ if not filepath.exists():
93
+ return None
94
+ with open(filepath, "r") as f:
95
+ data = json.load(f)
96
+ # Simplified deserialization - full version would reconstruct dataclasses
97
+ return data
98
+
99
+ def list_traces(self) -> List[str]:
100
+ return [p.stem for p in self.storage_path.glob("*.json")]
101
+
102
+ def get_stats(self) -> Dict[str, Any]:
103
+ traces = []
104
+ for tid in self.list_traces():
105
+ t = self.load_trace(tid)
106
+ if t:
107
+ traces.append(t)
108
+
109
+ if not traces:
110
+ return {"count": 0}
111
+
112
+ total_cost = sum(t.get("total_cost", 0) for t in traces if isinstance(t, dict))
113
+ total_steps = sum(len(t.get("steps", [])) for t in traces if isinstance(t, dict))
114
+
115
+ return {
116
+ "count": len(traces),
117
+ "avg_cost": total_cost / len(traces),
118
+ "avg_steps": total_steps / len(traces),
119
+ "success_rate": sum(
120
+ 1 for t in traces
121
+ if isinstance(t, dict) and t.get("final_outcome") == "success"
122
+ ) / len(traces),
123
+ }