empathy-framework 3.7.0__py3-none-any.whl → 3.8.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.
Files changed (274) hide show
  1. coach_wizards/code_reviewer_README.md +60 -0
  2. coach_wizards/code_reviewer_wizard.py +180 -0
  3. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +148 -11
  4. empathy_framework-3.8.0.dist-info/RECORD +333 -0
  5. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +5 -1
  6. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  7. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  8. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  9. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  10. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  11. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  12. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  13. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  14. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  15. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  16. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  17. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  18. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  19. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  20. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  21. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  22. empathy_llm_toolkit/agent_factory/base.py +305 -0
  23. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  24. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  25. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  26. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  27. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  28. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  29. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  30. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  31. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  32. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  33. empathy_llm_toolkit/cli/__init__.py +8 -0
  34. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  35. empathy_llm_toolkit/code_health.py +150 -3
  36. empathy_llm_toolkit/config/__init__.py +29 -0
  37. empathy_llm_toolkit/config/unified.py +295 -0
  38. empathy_llm_toolkit/routing/__init__.py +32 -0
  39. empathy_llm_toolkit/routing/model_router.py +362 -0
  40. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  41. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  42. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  43. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  44. empathy_llm_toolkit/security/README.md +262 -0
  45. empathy_llm_toolkit/security/__init__.py +62 -0
  46. empathy_llm_toolkit/security/audit_logger.py +929 -0
  47. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  48. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  49. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  50. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  51. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  52. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  53. empathy_llm_toolkit/wizards/__init__.py +38 -0
  54. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  55. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  56. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  57. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  58. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  59. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  60. empathy_os/__init__.py +52 -52
  61. empathy_os/adaptive/__init__.py +13 -0
  62. empathy_os/adaptive/task_complexity.py +127 -0
  63. empathy_os/cache/__init__.py +117 -0
  64. empathy_os/cache/base.py +166 -0
  65. empathy_os/cache/dependency_manager.py +253 -0
  66. empathy_os/cache/hash_only.py +248 -0
  67. empathy_os/cache/hybrid.py +390 -0
  68. empathy_os/cache/storage.py +282 -0
  69. empathy_os/cli.py +118 -8
  70. empathy_os/cli_unified.py +121 -1
  71. empathy_os/config/__init__.py +63 -0
  72. empathy_os/config/xml_config.py +239 -0
  73. empathy_os/config.py +2 -1
  74. empathy_os/dashboard/__init__.py +15 -0
  75. empathy_os/dashboard/server.py +743 -0
  76. empathy_os/memory/__init__.py +195 -0
  77. empathy_os/memory/claude_memory.py +466 -0
  78. empathy_os/memory/config.py +224 -0
  79. empathy_os/memory/control_panel.py +1298 -0
  80. empathy_os/memory/edges.py +179 -0
  81. empathy_os/memory/graph.py +567 -0
  82. empathy_os/memory/long_term.py +1194 -0
  83. empathy_os/memory/nodes.py +179 -0
  84. empathy_os/memory/redis_bootstrap.py +540 -0
  85. empathy_os/memory/security/__init__.py +31 -0
  86. empathy_os/memory/security/audit_logger.py +930 -0
  87. empathy_os/memory/security/pii_scrubber.py +640 -0
  88. empathy_os/memory/security/secrets_detector.py +678 -0
  89. empathy_os/memory/short_term.py +2119 -0
  90. empathy_os/memory/storage/__init__.py +15 -0
  91. empathy_os/memory/summary_index.py +583 -0
  92. empathy_os/memory/unified.py +619 -0
  93. empathy_os/metrics/__init__.py +12 -0
  94. empathy_os/metrics/prompt_metrics.py +190 -0
  95. empathy_os/models/__init__.py +136 -0
  96. empathy_os/models/__main__.py +13 -0
  97. empathy_os/models/cli.py +655 -0
  98. empathy_os/models/empathy_executor.py +354 -0
  99. empathy_os/models/executor.py +252 -0
  100. empathy_os/models/fallback.py +671 -0
  101. empathy_os/models/provider_config.py +563 -0
  102. empathy_os/models/registry.py +382 -0
  103. empathy_os/models/tasks.py +302 -0
  104. empathy_os/models/telemetry.py +548 -0
  105. empathy_os/models/token_estimator.py +378 -0
  106. empathy_os/models/validation.py +274 -0
  107. empathy_os/monitoring/__init__.py +52 -0
  108. empathy_os/monitoring/alerts.py +23 -0
  109. empathy_os/monitoring/alerts_cli.py +268 -0
  110. empathy_os/monitoring/multi_backend.py +271 -0
  111. empathy_os/monitoring/otel_backend.py +363 -0
  112. empathy_os/optimization/__init__.py +19 -0
  113. empathy_os/optimization/context_optimizer.py +272 -0
  114. empathy_os/plugins/__init__.py +28 -0
  115. empathy_os/plugins/base.py +361 -0
  116. empathy_os/plugins/registry.py +268 -0
  117. empathy_os/project_index/__init__.py +30 -0
  118. empathy_os/project_index/cli.py +335 -0
  119. empathy_os/project_index/crew_integration.py +430 -0
  120. empathy_os/project_index/index.py +425 -0
  121. empathy_os/project_index/models.py +501 -0
  122. empathy_os/project_index/reports.py +473 -0
  123. empathy_os/project_index/scanner.py +538 -0
  124. empathy_os/prompts/__init__.py +61 -0
  125. empathy_os/prompts/config.py +77 -0
  126. empathy_os/prompts/context.py +177 -0
  127. empathy_os/prompts/parser.py +285 -0
  128. empathy_os/prompts/registry.py +313 -0
  129. empathy_os/prompts/templates.py +208 -0
  130. empathy_os/resilience/__init__.py +56 -0
  131. empathy_os/resilience/circuit_breaker.py +256 -0
  132. empathy_os/resilience/fallback.py +179 -0
  133. empathy_os/resilience/health.py +300 -0
  134. empathy_os/resilience/retry.py +209 -0
  135. empathy_os/resilience/timeout.py +135 -0
  136. empathy_os/routing/__init__.py +43 -0
  137. empathy_os/routing/chain_executor.py +433 -0
  138. empathy_os/routing/classifier.py +217 -0
  139. empathy_os/routing/smart_router.py +234 -0
  140. empathy_os/routing/wizard_registry.py +307 -0
  141. empathy_os/trust/__init__.py +28 -0
  142. empathy_os/trust/circuit_breaker.py +579 -0
  143. empathy_os/validation/__init__.py +19 -0
  144. empathy_os/validation/xml_validator.py +281 -0
  145. empathy_os/wizard_factory_cli.py +170 -0
  146. empathy_os/workflows/__init__.py +360 -0
  147. empathy_os/workflows/base.py +1660 -0
  148. empathy_os/workflows/bug_predict.py +962 -0
  149. empathy_os/workflows/code_review.py +960 -0
  150. empathy_os/workflows/code_review_adapters.py +310 -0
  151. empathy_os/workflows/code_review_pipeline.py +720 -0
  152. empathy_os/workflows/config.py +600 -0
  153. empathy_os/workflows/dependency_check.py +648 -0
  154. empathy_os/workflows/document_gen.py +1069 -0
  155. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  156. empathy_os/workflows/health_check.py +679 -0
  157. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  158. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  159. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  160. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  161. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  162. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  163. empathy_os/workflows/manage_documentation.py +804 -0
  164. empathy_os/workflows/new_sample_workflow1.py +146 -0
  165. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  166. empathy_os/workflows/perf_audit.py +687 -0
  167. empathy_os/workflows/pr_review.py +748 -0
  168. empathy_os/workflows/progress.py +445 -0
  169. empathy_os/workflows/progress_server.py +322 -0
  170. empathy_os/workflows/refactor_plan.py +693 -0
  171. empathy_os/workflows/release_prep.py +808 -0
  172. empathy_os/workflows/research_synthesis.py +404 -0
  173. empathy_os/workflows/secure_release.py +585 -0
  174. empathy_os/workflows/security_adapters.py +297 -0
  175. empathy_os/workflows/security_audit.py +1046 -0
  176. empathy_os/workflows/step_config.py +234 -0
  177. empathy_os/workflows/test5.py +125 -0
  178. empathy_os/workflows/test5_README.md +158 -0
  179. empathy_os/workflows/test_gen.py +1855 -0
  180. empathy_os/workflows/test_lifecycle.py +526 -0
  181. empathy_os/workflows/test_maintenance.py +626 -0
  182. empathy_os/workflows/test_maintenance_cli.py +590 -0
  183. empathy_os/workflows/test_maintenance_crew.py +821 -0
  184. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  185. empathy_software_plugin/cli/__init__.py +120 -0
  186. empathy_software_plugin/cli/inspect.py +362 -0
  187. empathy_software_plugin/cli.py +3 -1
  188. empathy_software_plugin/wizards/__init__.py +42 -0
  189. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  190. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  191. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  192. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  193. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  194. empathy_software_plugin/wizards/base_wizard.py +288 -0
  195. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  196. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  197. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  198. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  199. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  200. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  201. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  202. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  203. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  204. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  205. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  206. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  207. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  208. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  209. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  210. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  211. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  212. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  213. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  214. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  215. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  216. empathy_software_plugin/wizards/security/__init__.py +32 -0
  217. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  218. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  219. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  220. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  221. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  222. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  223. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  224. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  225. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  226. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  227. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  228. hot_reload/README.md +473 -0
  229. hot_reload/__init__.py +62 -0
  230. hot_reload/config.py +84 -0
  231. hot_reload/integration.py +228 -0
  232. hot_reload/reloader.py +298 -0
  233. hot_reload/watcher.py +179 -0
  234. hot_reload/websocket.py +176 -0
  235. scaffolding/README.md +589 -0
  236. scaffolding/__init__.py +35 -0
  237. scaffolding/__main__.py +14 -0
  238. scaffolding/cli.py +240 -0
  239. test_generator/__init__.py +38 -0
  240. test_generator/__main__.py +14 -0
  241. test_generator/cli.py +226 -0
  242. test_generator/generator.py +325 -0
  243. test_generator/risk_analyzer.py +216 -0
  244. workflow_patterns/__init__.py +33 -0
  245. workflow_patterns/behavior.py +249 -0
  246. workflow_patterns/core.py +76 -0
  247. workflow_patterns/output.py +99 -0
  248. workflow_patterns/registry.py +255 -0
  249. workflow_patterns/structural.py +288 -0
  250. workflow_scaffolding/__init__.py +11 -0
  251. workflow_scaffolding/__main__.py +12 -0
  252. workflow_scaffolding/cli.py +206 -0
  253. workflow_scaffolding/generator.py +265 -0
  254. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  255. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  256. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  257. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  258. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  259. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  260. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  261. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  262. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  263. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  264. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  265. agents/compliance_anticipation_agent.py +0 -1422
  266. agents/compliance_db.py +0 -339
  267. agents/epic_integration_wizard.py +0 -530
  268. agents/notifications.py +0 -291
  269. agents/trust_building_behaviors.py +0 -872
  270. empathy_framework-3.7.0.dist-info/RECORD +0 -105
  271. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
  272. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
  273. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
  274. /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
@@ -0,0 +1,548 @@
1
+ """Structured Telemetry for Multi-Model Workflows
2
+
3
+ Provides normalized schemas for tracking LLM calls and workflow runs:
4
+ - LLMCallRecord: Per-call metrics (model, tokens, cost, latency)
5
+ - WorkflowRunRecord: Per-workflow metrics (stages, total cost, duration)
6
+ - TelemetryBackend: Abstract interface for telemetry storage
7
+ - TelemetryStore: JSONL file-based backend (default)
8
+ - Analytics helpers for cost analysis and optimization
9
+
10
+ Copyright 2025 Smart-AI-Memory
11
+ Licensed under Fair Source License 0.9
12
+ """
13
+
14
+ import json
15
+ from dataclasses import asdict, dataclass, field
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Any, Protocol, runtime_checkable
19
+
20
+
21
+ @dataclass
22
+ class LLMCallRecord:
23
+ """Record of a single LLM API call.
24
+
25
+ Captures all relevant metrics for cost tracking, performance analysis,
26
+ and debugging.
27
+ """
28
+
29
+ # Identification
30
+ call_id: str
31
+ timestamp: str # ISO format
32
+
33
+ # Context
34
+ workflow_name: str | None = None
35
+ step_name: str | None = None
36
+ user_id: str | None = None
37
+ session_id: str | None = None
38
+
39
+ # Task routing
40
+ task_type: str = "unknown"
41
+ provider: str = "anthropic"
42
+ tier: str = "capable"
43
+ model_id: str = ""
44
+
45
+ # Token usage
46
+ input_tokens: int = 0
47
+ output_tokens: int = 0
48
+
49
+ # Cost (in USD)
50
+ estimated_cost: float = 0.0
51
+ actual_cost: float | None = None
52
+
53
+ # Performance
54
+ latency_ms: int = 0
55
+
56
+ # Fallback and resilience tracking
57
+ fallback_used: bool = False
58
+ fallback_chain: list[str] = field(default_factory=list)
59
+ original_provider: str | None = None
60
+ original_model: str | None = None
61
+ retry_count: int = 0 # Number of retries before success
62
+ circuit_breaker_state: str | None = None # "closed", "open", "half-open"
63
+
64
+ # Error tracking
65
+ success: bool = True
66
+ error_type: str | None = None
67
+ error_message: str | None = None
68
+
69
+ # Additional metadata
70
+ metadata: dict[str, Any] = field(default_factory=dict)
71
+
72
+ def to_dict(self) -> dict[str, Any]:
73
+ """Convert to dictionary for JSON serialization."""
74
+ return asdict(self)
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: dict[str, Any]) -> "LLMCallRecord":
78
+ """Create from dictionary."""
79
+ return cls(**data)
80
+
81
+
82
+ @dataclass
83
+ class WorkflowStageRecord:
84
+ """Record of a single workflow stage execution."""
85
+
86
+ stage_name: str
87
+ tier: str
88
+ model_id: str
89
+ input_tokens: int = 0
90
+ output_tokens: int = 0
91
+ cost: float = 0.0
92
+ latency_ms: int = 0
93
+ success: bool = True
94
+ skipped: bool = False
95
+ skip_reason: str | None = None
96
+ error: str | None = None
97
+
98
+
99
+ @dataclass
100
+ class WorkflowRunRecord:
101
+ """Record of a complete workflow execution.
102
+
103
+ Aggregates stage-level metrics and provides workflow-level analytics.
104
+ """
105
+
106
+ # Identification
107
+ run_id: str
108
+ workflow_name: str
109
+ started_at: str # ISO format
110
+ completed_at: str | None = None
111
+
112
+ # Context
113
+ user_id: str | None = None
114
+ session_id: str | None = None
115
+
116
+ # Stages
117
+ stages: list[WorkflowStageRecord] = field(default_factory=list)
118
+
119
+ # Aggregated metrics
120
+ total_input_tokens: int = 0
121
+ total_output_tokens: int = 0
122
+ total_cost: float = 0.0
123
+ baseline_cost: float = 0.0 # If all stages used premium
124
+ savings: float = 0.0
125
+ savings_percent: float = 0.0
126
+
127
+ # Performance
128
+ total_duration_ms: int = 0
129
+
130
+ # Status
131
+ success: bool = True
132
+ error: str | None = None
133
+
134
+ # Provider usage
135
+ providers_used: list[str] = field(default_factory=list)
136
+ tiers_used: list[str] = field(default_factory=list)
137
+
138
+ def to_dict(self) -> dict[str, Any]:
139
+ """Convert to dictionary for JSON serialization."""
140
+ data = asdict(self)
141
+ data["stages"] = [asdict(s) for s in self.stages]
142
+ return data
143
+
144
+ @classmethod
145
+ def from_dict(cls, data: dict[str, Any]) -> "WorkflowRunRecord":
146
+ """Create from dictionary."""
147
+ stages = [WorkflowStageRecord(**s) for s in data.pop("stages", [])]
148
+ return cls(stages=stages, **data)
149
+
150
+
151
+ @runtime_checkable
152
+ class TelemetryBackend(Protocol):
153
+ """Protocol for telemetry storage backends.
154
+
155
+ Implementations can store telemetry data in different backends:
156
+ - JSONL files (default, via TelemetryStore)
157
+ - Database (PostgreSQL, SQLite, etc.)
158
+ - Cloud services (DataDog, New Relic, etc.)
159
+ - Custom backends
160
+
161
+ Example implementing a custom backend:
162
+ >>> class DatabaseBackend:
163
+ ... def log_call(self, record: LLMCallRecord) -> None:
164
+ ... # Insert into database
165
+ ... pass
166
+ ...
167
+ ... def log_workflow(self, record: WorkflowRunRecord) -> None:
168
+ ... # Insert into database
169
+ ... pass
170
+ ...
171
+ ... def get_calls(self, since=None, workflow_name=None, limit=1000):
172
+ ... # Query database
173
+ ... return []
174
+ ...
175
+ ... def get_workflows(self, since=None, workflow_name=None, limit=100):
176
+ ... # Query database
177
+ ... return []
178
+ """
179
+
180
+ def log_call(self, record: LLMCallRecord) -> None:
181
+ """Log an LLM call record."""
182
+ ...
183
+
184
+ def log_workflow(self, record: WorkflowRunRecord) -> None:
185
+ """Log a workflow run record."""
186
+ ...
187
+
188
+ def get_calls(
189
+ self,
190
+ since: datetime | None = None,
191
+ workflow_name: str | None = None,
192
+ limit: int = 1000,
193
+ ) -> list[LLMCallRecord]:
194
+ """Get LLM call records with optional filters."""
195
+ ...
196
+
197
+ def get_workflows(
198
+ self,
199
+ since: datetime | None = None,
200
+ workflow_name: str | None = None,
201
+ limit: int = 100,
202
+ ) -> list[WorkflowRunRecord]:
203
+ """Get workflow run records with optional filters."""
204
+ ...
205
+
206
+
207
+ class TelemetryStore:
208
+ """JSONL file-based telemetry backend (default implementation).
209
+
210
+ Stores records in JSONL format for easy streaming and analysis.
211
+ Implements the TelemetryBackend protocol.
212
+ """
213
+
214
+ def __init__(self, storage_dir: str = ".empathy"):
215
+ """Initialize telemetry store.
216
+
217
+ Args:
218
+ storage_dir: Directory for telemetry files
219
+
220
+ """
221
+ self.storage_dir = Path(storage_dir)
222
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
223
+
224
+ self.calls_file = self.storage_dir / "llm_calls.jsonl"
225
+ self.workflows_file = self.storage_dir / "workflow_runs.jsonl"
226
+
227
+ def log_call(self, record: LLMCallRecord) -> None:
228
+ """Log an LLM call record."""
229
+ with open(self.calls_file, "a") as f:
230
+ f.write(json.dumps(record.to_dict()) + "\n")
231
+
232
+ def log_workflow(self, record: WorkflowRunRecord) -> None:
233
+ """Log a workflow run record."""
234
+ with open(self.workflows_file, "a") as f:
235
+ f.write(json.dumps(record.to_dict()) + "\n")
236
+
237
+ def get_calls(
238
+ self,
239
+ since: datetime | None = None,
240
+ workflow_name: str | None = None,
241
+ limit: int = 1000,
242
+ ) -> list[LLMCallRecord]:
243
+ """Get LLM call records.
244
+
245
+ Args:
246
+ since: Only return records after this time
247
+ workflow_name: Filter by workflow name
248
+ limit: Maximum records to return
249
+
250
+ Returns:
251
+ List of LLMCallRecord
252
+
253
+ """
254
+ records: list[LLMCallRecord] = []
255
+ if not self.calls_file.exists():
256
+ return records
257
+
258
+ with open(self.calls_file) as f:
259
+ for line in f:
260
+ if not line.strip():
261
+ continue
262
+ try:
263
+ data = json.loads(line)
264
+ record = LLMCallRecord.from_dict(data)
265
+
266
+ # Apply filters
267
+ if since:
268
+ record_time = datetime.fromisoformat(record.timestamp)
269
+ if record_time < since:
270
+ continue
271
+
272
+ if workflow_name and record.workflow_name != workflow_name:
273
+ continue
274
+
275
+ records.append(record)
276
+
277
+ if len(records) >= limit:
278
+ break
279
+ except (json.JSONDecodeError, KeyError):
280
+ continue
281
+
282
+ return records
283
+
284
+ def get_workflows(
285
+ self,
286
+ since: datetime | None = None,
287
+ workflow_name: str | None = None,
288
+ limit: int = 100,
289
+ ) -> list[WorkflowRunRecord]:
290
+ """Get workflow run records.
291
+
292
+ Args:
293
+ since: Only return records after this time
294
+ workflow_name: Filter by workflow name
295
+ limit: Maximum records to return
296
+
297
+ Returns:
298
+ List of WorkflowRunRecord
299
+
300
+ """
301
+ records: list[WorkflowRunRecord] = []
302
+ if not self.workflows_file.exists():
303
+ return records
304
+
305
+ with open(self.workflows_file) as f:
306
+ for line in f:
307
+ if not line.strip():
308
+ continue
309
+ try:
310
+ data = json.loads(line)
311
+ record = WorkflowRunRecord.from_dict(data)
312
+
313
+ # Apply filters
314
+ if since:
315
+ record_time = datetime.fromisoformat(record.started_at)
316
+ if record_time < since:
317
+ continue
318
+
319
+ if workflow_name and record.workflow_name != workflow_name:
320
+ continue
321
+
322
+ records.append(record)
323
+
324
+ if len(records) >= limit:
325
+ break
326
+ except (json.JSONDecodeError, KeyError):
327
+ continue
328
+
329
+ return records
330
+
331
+
332
+ class TelemetryAnalytics:
333
+ """Analytics helpers for telemetry data.
334
+
335
+ Provides insights into cost optimization, provider usage, and performance.
336
+ """
337
+
338
+ def __init__(self, store: TelemetryStore | None = None):
339
+ """Initialize analytics.
340
+
341
+ Args:
342
+ store: TelemetryStore to analyze (creates default if None)
343
+
344
+ """
345
+ self.store = store or TelemetryStore()
346
+
347
+ def top_expensive_workflows(
348
+ self,
349
+ n: int = 10,
350
+ since: datetime | None = None,
351
+ ) -> list[dict[str, Any]]:
352
+ """Get the most expensive workflows.
353
+
354
+ Args:
355
+ n: Number of workflows to return
356
+ since: Only consider workflows after this time
357
+
358
+ Returns:
359
+ List of dicts with workflow_name, total_cost, run_count
360
+
361
+ """
362
+ workflows = self.store.get_workflows(since=since, limit=10000)
363
+
364
+ # Aggregate by workflow name
365
+ costs: dict[str, dict[str, Any]] = {}
366
+ for wf in workflows:
367
+ if wf.workflow_name not in costs:
368
+ costs[wf.workflow_name] = {
369
+ "workflow_name": wf.workflow_name,
370
+ "total_cost": 0.0,
371
+ "run_count": 0,
372
+ "total_savings": 0.0,
373
+ "avg_duration_ms": 0,
374
+ }
375
+ costs[wf.workflow_name]["total_cost"] += wf.total_cost
376
+ costs[wf.workflow_name]["run_count"] += 1
377
+ costs[wf.workflow_name]["total_savings"] += wf.savings
378
+
379
+ # Calculate averages and sort
380
+ result = list(costs.values())
381
+ for item in result:
382
+ if item["run_count"] > 0:
383
+ item["avg_cost"] = item["total_cost"] / item["run_count"]
384
+
385
+ result.sort(key=lambda x: x["total_cost"], reverse=True)
386
+ return result[:n]
387
+
388
+ def provider_usage_summary(
389
+ self,
390
+ since: datetime | None = None,
391
+ ) -> dict[str, dict[str, Any]]:
392
+ """Get usage summary by provider.
393
+
394
+ Args:
395
+ since: Only consider calls after this time
396
+
397
+ Returns:
398
+ Dict mapping provider to usage stats
399
+
400
+ """
401
+ calls = self.store.get_calls(since=since, limit=100000)
402
+
403
+ summary: dict[str, dict[str, Any]] = {}
404
+ for call in calls:
405
+ if call.provider not in summary:
406
+ summary[call.provider] = {
407
+ "call_count": 0,
408
+ "total_tokens": 0,
409
+ "total_cost": 0.0,
410
+ "error_count": 0,
411
+ "avg_latency_ms": 0,
412
+ "by_tier": {"cheap": 0, "capable": 0, "premium": 0},
413
+ }
414
+
415
+ s = summary[call.provider]
416
+ s["call_count"] += 1
417
+ s["total_tokens"] += call.input_tokens + call.output_tokens
418
+ s["total_cost"] += call.estimated_cost
419
+ if not call.success:
420
+ s["error_count"] += 1
421
+ if call.tier in s["by_tier"]:
422
+ s["by_tier"][call.tier] += 1
423
+
424
+ # Calculate averages
425
+ for _provider, stats in summary.items():
426
+ if stats["call_count"] > 0:
427
+ stats["avg_cost"] = stats["total_cost"] / stats["call_count"]
428
+
429
+ return summary
430
+
431
+ def tier_distribution(
432
+ self,
433
+ since: datetime | None = None,
434
+ ) -> dict[str, dict[str, Any]]:
435
+ """Get call distribution by tier.
436
+
437
+ Args:
438
+ since: Only consider calls after this time
439
+
440
+ Returns:
441
+ Dict mapping tier to stats
442
+
443
+ """
444
+ calls = self.store.get_calls(since=since, limit=100000)
445
+
446
+ dist: dict[str, dict[str, Any]] = {
447
+ "cheap": {"count": 0, "cost": 0.0, "tokens": 0},
448
+ "capable": {"count": 0, "cost": 0.0, "tokens": 0},
449
+ "premium": {"count": 0, "cost": 0.0, "tokens": 0},
450
+ }
451
+
452
+ for call in calls:
453
+ if call.tier in dist:
454
+ dist[call.tier]["count"] += 1
455
+ dist[call.tier]["cost"] += call.estimated_cost
456
+ dist[call.tier]["tokens"] += call.input_tokens + call.output_tokens
457
+
458
+ total_calls = sum(d["count"] for d in dist.values())
459
+ for _tier, stats in dist.items():
460
+ stats["percent"] = (stats["count"] / total_calls * 100) if total_calls > 0 else 0
461
+
462
+ return dist
463
+
464
+ def fallback_stats(
465
+ self,
466
+ since: datetime | None = None,
467
+ ) -> dict[str, Any]:
468
+ """Get fallback usage statistics.
469
+
470
+ Args:
471
+ since: Only consider calls after this time
472
+
473
+ Returns:
474
+ Dict with fallback stats
475
+
476
+ """
477
+ calls = self.store.get_calls(since=since, limit=100000)
478
+
479
+ total = len(calls)
480
+ fallback_count = sum(1 for c in calls if c.fallback_used)
481
+ error_count = sum(1 for c in calls if not c.success)
482
+
483
+ # Count by original provider
484
+ by_provider: dict[str, int] = {}
485
+ for call in calls:
486
+ if call.fallback_used and call.original_provider:
487
+ by_provider[call.original_provider] = by_provider.get(call.original_provider, 0) + 1
488
+
489
+ return {
490
+ "total_calls": total,
491
+ "fallback_count": fallback_count,
492
+ "fallback_percent": (fallback_count / total * 100) if total > 0 else 0,
493
+ "error_count": error_count,
494
+ "error_rate": (error_count / total * 100) if total > 0 else 0,
495
+ "by_original_provider": by_provider,
496
+ }
497
+
498
+ def cost_savings_report(
499
+ self,
500
+ since: datetime | None = None,
501
+ ) -> dict[str, Any]:
502
+ """Generate cost savings report.
503
+
504
+ Args:
505
+ since: Only consider workflows after this time
506
+
507
+ Returns:
508
+ Dict with savings analysis
509
+
510
+ """
511
+ workflows = self.store.get_workflows(since=since, limit=10000)
512
+
513
+ total_cost = sum(wf.total_cost for wf in workflows)
514
+ total_baseline = sum(wf.baseline_cost for wf in workflows)
515
+ total_savings = sum(wf.savings for wf in workflows)
516
+
517
+ return {
518
+ "workflow_count": len(workflows),
519
+ "total_actual_cost": total_cost,
520
+ "total_baseline_cost": total_baseline,
521
+ "total_savings": total_savings,
522
+ "savings_percent": (
523
+ (total_savings / total_baseline * 100) if total_baseline > 0 else 0
524
+ ),
525
+ "avg_cost_per_workflow": total_cost / len(workflows) if workflows else 0,
526
+ }
527
+
528
+
529
+ # Singleton for global telemetry
530
+ _telemetry_store: TelemetryStore | None = None
531
+
532
+
533
+ def get_telemetry_store(storage_dir: str = ".empathy") -> TelemetryStore:
534
+ """Get or create the global telemetry store."""
535
+ global _telemetry_store
536
+ if _telemetry_store is None:
537
+ _telemetry_store = TelemetryStore(storage_dir)
538
+ return _telemetry_store
539
+
540
+
541
+ def log_llm_call(record: LLMCallRecord) -> None:
542
+ """Convenience function to log an LLM call."""
543
+ get_telemetry_store().log_call(record)
544
+
545
+
546
+ def log_workflow_run(record: WorkflowRunRecord) -> None:
547
+ """Convenience function to log a workflow run."""
548
+ get_telemetry_store().log_workflow(record)