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 +56 -0
- openanchor/analytics.py +252 -0
- openanchor/attribution.py +265 -0
- openanchor/collector.py +170 -0
- openanchor/models.py +178 -0
- openanchor/okf_cost_governance.py +141 -0
- openanchor/okf_optimization_tracking.py +176 -0
- openanchor/okf_token_profiles.py +108 -0
- openanchor/storage.py +269 -0
- openanchor-0.1.0.dist-info/METADATA +871 -0
- openanchor-0.1.0.dist-info/RECORD +14 -0
- openanchor-0.1.0.dist-info/WHEEL +5 -0
- openanchor-0.1.0.dist-info/licenses/LICENSE +21 -0
- openanchor-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
]
|
openanchor/analytics.py
ADDED
|
@@ -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
|