openanchor 0.1.0__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.
openanchor/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ OpenAnchor: Token Consumption Intelligence Platform
3
+
4
+ OpenAnchor provides observability, attribution, pattern detection, and
5
+ optimization intelligence for AI token consumption.
6
+
7
+ Built on PyTokenCalc (token accounting foundation).
8
+
9
+ Quick Start:
10
+ from openanchor import TokenCollector, Analytics
11
+
12
+ collector = TokenCollector()
13
+ event = collector.capture_event(
14
+ call_id="call_1",
15
+ model="gpt-4",
16
+ provider="openai",
17
+ input_tokens=100,
18
+ output_tokens=50
19
+ )
20
+
21
+ # Analyze
22
+ from openanchor import AttributionModel
23
+ attribution = AttributionModel(collector.store)
24
+ breakdown = attribution.analyze_call("call_1")
25
+ """
26
+
27
+ from .models import (
28
+ TokenEvent,
29
+ TokenConsumption,
30
+ Attribution,
31
+ SessionStats,
32
+ OperationType,
33
+ RequestPhase,
34
+ )
35
+ from .collector import TokenCollector
36
+ from .storage import EventStore, SqliteEventStore
37
+ from .attribution import AttributionModel
38
+ from .analytics import Analytics
39
+
40
+ __version__ = "0.1.0"
41
+ __author__ = "Georgi Mammen Mullassery"
42
+ __license__ = "MIT"
43
+
44
+ __all__ = [
45
+ "TokenEvent",
46
+ "TokenConsumption",
47
+ "Attribution",
48
+ "SessionStats",
49
+ "OperationType",
50
+ "RequestPhase",
51
+ "TokenCollector",
52
+ "EventStore",
53
+ "SqliteEventStore",
54
+ "AttributionModel",
55
+ "Analytics",
56
+ ]
@@ -0,0 +1,252 @@
1
+ """Analytics and query APIs for token insights."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Dict, List, Any, Optional
5
+ from collections import defaultdict
6
+ import logging
7
+
8
+ from .collector import TokenCollector
9
+ from .attribution import AttributionModel
10
+ from .models import TokenEvent, SessionStats
11
+ from .storage import EventStore
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Analytics:
17
+ """High-level analytics and query API."""
18
+
19
+ def __init__(self, collector: TokenCollector, attribution: AttributionModel):
20
+ """
21
+ Initialize analytics engine.
22
+
23
+ Args:
24
+ collector: Token collector
25
+ attribution: Attribution model
26
+ """
27
+ self.collector = collector
28
+ self.attribution = attribution
29
+ self.store = collector.store
30
+
31
+ # Session-level queries
32
+
33
+ def get_session_stats(self, session_id: str) -> SessionStats:
34
+ """Get overall statistics for a session."""
35
+ return self.collector.compute_session_stats(session_id)
36
+
37
+ def get_all_sessions(self) -> List[str]:
38
+ """Get all session IDs."""
39
+ events = self.store.all_events()
40
+ sessions = set()
41
+ for event in events:
42
+ if event.session_id:
43
+ sessions.add(event.session_id)
44
+ return sorted(sessions)
45
+
46
+ # Call-level queries
47
+
48
+ def get_call_stats(self, call_id: str) -> Dict[str, Any]:
49
+ """Get statistics for a single call."""
50
+ return self.collector.compute_call_stats(call_id)
51
+
52
+ # Token breakdown queries
53
+
54
+ def get_tokens_by_operation(self, session_id: str) -> Dict[str, int]:
55
+ """Get token consumption by operation type."""
56
+ return self.attribution.get_operation_breakdown(session_id)
57
+
58
+ def get_tokens_by_phase(self, session_id: str) -> Dict[str, int]:
59
+ """Get token consumption by request/response phase."""
60
+ return self.attribution.get_phase_breakdown(session_id)
61
+
62
+ def get_tokens_by_model(self, session_id: str) -> Dict[str, int]:
63
+ """Get token consumption by model."""
64
+ return self.attribution.get_model_distribution(session_id)
65
+
66
+ def get_tokens_by_prompt_template(self, session_id: str) -> Dict[str, int]:
67
+ """Get token consumption by prompt template."""
68
+ events = self.store.get_events_by_session(session_id)
69
+ breakdown: Dict[str, int] = defaultdict(int)
70
+
71
+ for event in events:
72
+ template = event.prompt_template or "untagged"
73
+ breakdown[template] += event.tokens.total_tokens
74
+
75
+ return dict(breakdown)
76
+
77
+ # Efficiency queries
78
+
79
+ def rank_prompts_by_efficiency(
80
+ self, session_id: str, top_n: int = 10
81
+ ) -> List[tuple]:
82
+ """Rank prompt templates by efficiency (quality per token)."""
83
+ return self.attribution.rank_prompts_by_efficiency(session_id, top_n)
84
+
85
+ def get_prompt_stats(self, session_id: str) -> Dict[str, Dict[str, float]]:
86
+ """Get efficiency stats for all prompts in a session."""
87
+ return self.attribution.get_prompt_efficiency(session_id)
88
+
89
+ # Time-based queries
90
+
91
+ def get_tokens_by_hour(self, session_id: str) -> Dict[str, int]:
92
+ """Get token consumption by hour."""
93
+ events = self.store.get_events_by_session(session_id)
94
+
95
+ breakdown: Dict[str, int] = defaultdict(int)
96
+ for event in events:
97
+ hour_key = event.timestamp.strftime("%Y-%m-%d %H:00")
98
+ breakdown[hour_key] += event.tokens.total_tokens
99
+
100
+ return dict(sorted(breakdown.items()))
101
+
102
+ def get_tokens_by_day(self, session_id: str) -> Dict[str, int]:
103
+ """Get token consumption by day."""
104
+ events = self.store.get_events_by_session(session_id)
105
+
106
+ breakdown: Dict[str, int] = defaultdict(int)
107
+ for event in events:
108
+ day_key = event.timestamp.strftime("%Y-%m-%d")
109
+ breakdown[day_key] += event.tokens.total_tokens
110
+
111
+ return dict(sorted(breakdown.items()))
112
+
113
+ # Performance queries
114
+
115
+ def get_latency_stats(self, session_id: str) -> Dict[str, float]:
116
+ """Get latency statistics for a session."""
117
+ events = self.store.get_events_by_session(session_id)
118
+
119
+ latencies = [e.latency_ms for e in events if e.latency_ms]
120
+ if not latencies:
121
+ return {"count": 0}
122
+
123
+ latencies.sort()
124
+ total = sum(latencies)
125
+
126
+ return {
127
+ "count": len(latencies),
128
+ "avg_ms": total / len(latencies),
129
+ "median_ms": latencies[len(latencies) // 2],
130
+ "min_ms": min(latencies),
131
+ "max_ms": max(latencies),
132
+ "p95_ms": latencies[int(len(latencies) * 0.95)],
133
+ "p99_ms": latencies[int(len(latencies) * 0.99)],
134
+ }
135
+
136
+ def get_quality_stats(self, session_id: str) -> Dict[str, float]:
137
+ """Get quality score statistics for a session."""
138
+ events = self.store.get_events_by_session(session_id)
139
+
140
+ scores = [e.quality_score for e in events if e.quality_score is not None]
141
+ if not scores:
142
+ return {"count": 0}
143
+
144
+ scores.sort()
145
+ total = sum(scores)
146
+
147
+ return {
148
+ "count": len(scores),
149
+ "avg": total / len(scores),
150
+ "median": scores[len(scores) // 2],
151
+ "min": min(scores),
152
+ "max": max(scores),
153
+ }
154
+
155
+ # Comparison queries
156
+
157
+ def compare_sessions(self, session_ids: List[str]) -> Dict[str, Dict[str, Any]]:
158
+ """Compare statistics across sessions."""
159
+ comparison: Dict[str, Dict[str, Any]] = {}
160
+
161
+ for session_id in session_ids:
162
+ stats = self.get_session_stats(session_id)
163
+ comparison[session_id] = {
164
+ "total_tokens": stats.total_tokens,
165
+ "total_calls": stats.total_calls,
166
+ "avg_latency_ms": stats.avg_latency_ms,
167
+ "avg_quality": stats.avg_quality_score,
168
+ "tokens_per_call": (
169
+ stats.total_tokens / stats.total_calls if stats.total_calls > 0 else 0
170
+ ),
171
+ }
172
+
173
+ return comparison
174
+
175
+ # Diagnostic queries
176
+
177
+ def get_problematic_calls(
178
+ self, session_id: str, min_tokens: int = 5000, max_quality: float = 0.8
179
+ ) -> List[Dict[str, Any]]:
180
+ """
181
+ Find calls that consumed many tokens but had low quality.
182
+
183
+ Args:
184
+ session_id: Session to analyze
185
+ min_tokens: Minimum tokens to consider problematic
186
+ max_quality: Maximum quality score to flag
187
+
188
+ Returns:
189
+ List of problematic calls
190
+ """
191
+ events = self.store.get_events_by_session(session_id)
192
+
193
+ problematic = []
194
+ call_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {"tokens": 0})
195
+
196
+ for event in events:
197
+ call_id = event.call_id
198
+ call_stats[call_id]["tokens"] += event.tokens.total_tokens
199
+
200
+ if event.quality_score is not None:
201
+ if "quality_scores" not in call_stats[call_id]:
202
+ call_stats[call_id]["quality_scores"] = []
203
+ call_stats[call_id]["quality_scores"].append(event.quality_score)
204
+
205
+ for call_id, stats in call_stats.items():
206
+ if stats["tokens"] < min_tokens:
207
+ continue
208
+
209
+ avg_quality = (
210
+ sum(stats["quality_scores"]) / len(stats["quality_scores"])
211
+ if "quality_scores" in stats and stats["quality_scores"]
212
+ else 1.0
213
+ )
214
+
215
+ if avg_quality <= max_quality:
216
+ problematic.append(
217
+ {
218
+ "call_id": call_id,
219
+ "total_tokens": stats["tokens"],
220
+ "avg_quality": avg_quality,
221
+ "quality_ratio": (
222
+ avg_quality / stats["tokens"] if stats["tokens"] > 0 else 0
223
+ ),
224
+ }
225
+ )
226
+
227
+ return sorted(
228
+ problematic,
229
+ key=lambda x: x["quality_ratio"],
230
+ )
231
+
232
+ def get_summary(self, session_id: str) -> Dict[str, Any]:
233
+ """Get a comprehensive summary of session activity."""
234
+ stats = self.get_session_stats(session_id)
235
+ op_breakdown = self.get_tokens_by_operation(session_id)
236
+ phase_breakdown = self.get_tokens_by_phase(session_id)
237
+ latency_stats = self.get_latency_stats(session_id)
238
+ quality_stats = self.get_quality_stats(session_id)
239
+
240
+ return {
241
+ "session_id": session_id,
242
+ "total_tokens": stats.total_tokens,
243
+ "total_calls": stats.total_calls,
244
+ "duration_seconds": stats.duration_seconds,
245
+ "by_operation": op_breakdown,
246
+ "by_phase": phase_breakdown,
247
+ "latency_stats": latency_stats,
248
+ "quality_stats": quality_stats,
249
+ "tokens_per_call": (
250
+ stats.total_tokens / stats.total_calls if stats.total_calls > 0 else 0
251
+ ),
252
+ }
@@ -0,0 +1,265 @@
1
+ """Token attribution analysis (6D breakdown)."""
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional
5
+ from collections import defaultdict
6
+ import logging
7
+
8
+ from .models import Attribution, OperationType, RequestPhase, TokenEvent
9
+ from .storage import EventStore
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AttributionModel:
15
+ """Performs 6-dimensional token attribution analysis."""
16
+
17
+ def __init__(self, store: EventStore):
18
+ """
19
+ Initialize attribution model.
20
+
21
+ Args:
22
+ store: Event store to analyze
23
+ """
24
+ self.store = store
25
+
26
+ def analyze_call(self, call_id: str) -> Attribution:
27
+ """
28
+ Analyze token attribution for a single call.
29
+
30
+ Breaks down tokens across 6 dimensions:
31
+ 1. WHEN: Request vs response phase
32
+ 2. WHERE: Operation type (GitHub, PDF, reasoning, etc.)
33
+ 3. HOW: Sub-operation (GitHub read/write/search)
34
+ 4. WHICH: Prompt template used
35
+ 5. SESSION/PHASE: Temporal grouping
36
+ 6. WHY: Patterns and recommendations (v0.2+)
37
+
38
+ Args:
39
+ call_id: Call to analyze
40
+
41
+ Returns:
42
+ Attribution breakdown
43
+ """
44
+ events = self.store.get_events_by_call(call_id)
45
+
46
+ if not events:
47
+ return Attribution(call_id=call_id)
48
+
49
+ # Check cache first
50
+ cached = self.store.get_attribution(call_id)
51
+ if cached:
52
+ return cached
53
+
54
+ # Breakdown by phase
55
+ by_phase: Dict[RequestPhase, int] = defaultdict(int)
56
+ # Breakdown by operation type
57
+ by_operation: Dict[OperationType, int] = defaultdict(int)
58
+ # Breakdown by prompt template
59
+ by_prompt_template: Dict[str, int] = defaultdict(int)
60
+
61
+ total_tokens = 0
62
+
63
+ for event in events:
64
+ tokens = event.tokens.total_tokens
65
+ total_tokens += tokens
66
+
67
+ # WHEN: Request vs response
68
+ by_phase[event.phase] += tokens
69
+
70
+ # WHERE & HOW: Operation type
71
+ by_operation[event.operation_type] += tokens
72
+
73
+ # WHICH: Prompt template
74
+ if event.prompt_template:
75
+ by_prompt_template[event.prompt_template] += tokens
76
+ else:
77
+ by_prompt_template["untagged"] += tokens
78
+
79
+ attribution = Attribution(
80
+ call_id=call_id,
81
+ by_phase=dict(by_phase),
82
+ by_operation=dict(by_operation),
83
+ by_prompt_template=dict(by_prompt_template),
84
+ total_tokens=total_tokens,
85
+ )
86
+
87
+ # Cache the result
88
+ self.store.add_attribution(attribution)
89
+
90
+ return attribution
91
+
92
+ def get_operation_breakdown(self, session_id: str) -> Dict[str, int]:
93
+ """
94
+ Get token breakdown by operation type for a session.
95
+
96
+ Returns:
97
+ Dict mapping operation names to token counts
98
+ """
99
+ events = self.store.get_events_by_session(session_id)
100
+
101
+ breakdown: Dict[str, int] = defaultdict(int)
102
+ for event in events:
103
+ op_name = event.operation_type.value
104
+ breakdown[op_name] += event.tokens.total_tokens
105
+
106
+ return dict(breakdown)
107
+
108
+ def get_phase_breakdown(self, session_id: str) -> Dict[str, int]:
109
+ """
110
+ Get token breakdown by request/response phase.
111
+
112
+ Returns:
113
+ Dict with 'request' and 'response' token counts
114
+ """
115
+ events = self.store.get_events_by_session(session_id)
116
+
117
+ breakdown: Dict[str, int] = {"request": 0, "response": 0}
118
+ for event in events:
119
+ phase_name = event.phase.value
120
+ breakdown[phase_name] += event.tokens.total_tokens
121
+
122
+ return breakdown
123
+
124
+ def get_prompt_efficiency(self, session_id: str) -> Dict[str, Dict[str, float]]:
125
+ """
126
+ Rank prompts by efficiency (tokens per quality point).
127
+
128
+ Returns:
129
+ Dict mapping prompt templates to efficiency metrics
130
+ """
131
+ events = self.store.get_events_by_session(session_id)
132
+
133
+ prompt_stats: Dict[str, Dict[str, List[float]]] = defaultdict(
134
+ lambda: {"tokens": [], "quality": []}
135
+ )
136
+
137
+ for event in events:
138
+ if event.prompt_template:
139
+ prompt_stats[event.prompt_template]["tokens"].append(
140
+ event.tokens.total_tokens
141
+ )
142
+ if event.quality_score is not None:
143
+ prompt_stats[event.prompt_template]["quality"].append(
144
+ event.quality_score
145
+ )
146
+
147
+ # Calculate efficiency
148
+ efficiency: Dict[str, Dict[str, float]] = {}
149
+ for prompt, stats in prompt_stats.items():
150
+ if not stats["tokens"]:
151
+ continue
152
+
153
+ avg_tokens = sum(stats["tokens"]) / len(stats["tokens"])
154
+ avg_quality = (
155
+ sum(stats["quality"]) / len(stats["quality"])
156
+ if stats["quality"]
157
+ else 0.0
158
+ )
159
+
160
+ efficiency[prompt] = {
161
+ "avg_tokens": avg_tokens,
162
+ "avg_quality": avg_quality,
163
+ "efficiency_score": (
164
+ avg_quality / avg_tokens if avg_tokens > 0 else 0.0
165
+ ),
166
+ "usage_count": len(stats["tokens"]),
167
+ }
168
+
169
+ return efficiency
170
+
171
+ def get_model_distribution(self, session_id: str) -> Dict[str, int]:
172
+ """
173
+ Get token distribution across models.
174
+
175
+ Returns:
176
+ Dict mapping model names to token counts
177
+ """
178
+ events = self.store.get_events_by_session(session_id)
179
+
180
+ distribution: Dict[str, int] = defaultdict(int)
181
+ for event in events:
182
+ distribution[event.model] += event.tokens.total_tokens
183
+
184
+ return dict(distribution)
185
+
186
+ def rank_prompts_by_efficiency(
187
+ self, session_id: str, top_n: int = 10
188
+ ) -> List[tuple]:
189
+ """
190
+ Rank prompts by efficiency (quality per token).
191
+
192
+ Args:
193
+ session_id: Session to analyze
194
+ top_n: Number of top prompts to return
195
+
196
+ Returns:
197
+ List of (prompt_name, efficiency_score) tuples, sorted by efficiency
198
+ """
199
+ efficiency = self.get_prompt_efficiency(session_id)
200
+
201
+ if not efficiency:
202
+ return []
203
+
204
+ # Sort by efficiency score (descending)
205
+ sorted_prompts = sorted(
206
+ efficiency.items(), key=lambda x: x[1]["efficiency_score"], reverse=True
207
+ )
208
+
209
+ return [(prompt, data["efficiency_score"]) for prompt, data in sorted_prompts][
210
+ :top_n
211
+ ]
212
+
213
+ def get_attribution_summary(self, call_id: str) -> Dict[str, str]:
214
+ """
215
+ Get a human-readable summary of token attribution.
216
+
217
+ Args:
218
+ call_id: Call to summarize
219
+
220
+ Returns:
221
+ Dict with readable breakdown
222
+ """
223
+ attr = self.analyze_call(call_id)
224
+
225
+ summary: Dict[str, str] = {
226
+ "total_tokens": str(attr.total_tokens),
227
+ }
228
+
229
+ # Phase breakdown
230
+ if attr.by_phase:
231
+ phase_strs = [
232
+ f"{phase.value}: {tokens} ({100 * tokens / attr.total_tokens:.1f}%)"
233
+ for phase, tokens in sorted(
234
+ attr.by_phase.items(),
235
+ key=lambda x: x[1],
236
+ reverse=True,
237
+ )
238
+ ]
239
+ summary["by_phase"] = " | ".join(phase_strs)
240
+
241
+ # Operation breakdown
242
+ if attr.by_operation:
243
+ op_strs = [
244
+ f"{op.value}: {tokens} ({100 * tokens / attr.total_tokens:.1f}%)"
245
+ for op, tokens in sorted(
246
+ attr.by_operation.items(),
247
+ key=lambda x: x[1],
248
+ reverse=True,
249
+ )
250
+ ]
251
+ summary["by_operation"] = " | ".join(op_strs)
252
+
253
+ # Prompt breakdown
254
+ if attr.by_prompt_template:
255
+ prompt_strs = [
256
+ f"{prompt}: {tokens} ({100 * tokens / attr.total_tokens:.1f}%)"
257
+ for prompt, tokens in sorted(
258
+ attr.by_prompt_template.items(),
259
+ key=lambda x: x[1],
260
+ reverse=True,
261
+ )
262
+ ]
263
+ summary["by_prompt"] = " | ".join(prompt_strs)
264
+
265
+ return summary