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.
- coach_wizards/code_reviewer_README.md +60 -0
- coach_wizards/code_reviewer_wizard.py +180 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +148 -11
- empathy_framework-3.8.0.dist-info/RECORD +333 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +5 -1
- empathy_healthcare_plugin/monitors/__init__.py +9 -0
- empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
- empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
- empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
- empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
- empathy_llm_toolkit/agent_factory/__init__.py +53 -0
- empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
- empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
- empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
- empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
- empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
- empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
- empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
- empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
- empathy_llm_toolkit/agent_factory/base.py +305 -0
- empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
- empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
- empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
- empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
- empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
- empathy_llm_toolkit/agent_factory/decorators.py +286 -0
- empathy_llm_toolkit/agent_factory/factory.py +558 -0
- empathy_llm_toolkit/agent_factory/framework.py +192 -0
- empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
- empathy_llm_toolkit/agent_factory/resilient.py +320 -0
- empathy_llm_toolkit/cli/__init__.py +8 -0
- empathy_llm_toolkit/cli/sync_claude.py +487 -0
- empathy_llm_toolkit/code_health.py +150 -3
- empathy_llm_toolkit/config/__init__.py +29 -0
- empathy_llm_toolkit/config/unified.py +295 -0
- empathy_llm_toolkit/routing/__init__.py +32 -0
- empathy_llm_toolkit/routing/model_router.py +362 -0
- empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
- empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
- empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
- empathy_llm_toolkit/security/README.md +262 -0
- empathy_llm_toolkit/security/__init__.py +62 -0
- empathy_llm_toolkit/security/audit_logger.py +929 -0
- empathy_llm_toolkit/security/audit_logger_example.py +152 -0
- empathy_llm_toolkit/security/pii_scrubber.py +640 -0
- empathy_llm_toolkit/security/secrets_detector.py +678 -0
- empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
- empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
- empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
- empathy_llm_toolkit/wizards/__init__.py +38 -0
- empathy_llm_toolkit/wizards/base_wizard.py +364 -0
- empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
- empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
- empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
- empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
- empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
- empathy_os/__init__.py +52 -52
- empathy_os/adaptive/__init__.py +13 -0
- empathy_os/adaptive/task_complexity.py +127 -0
- empathy_os/cache/__init__.py +117 -0
- empathy_os/cache/base.py +166 -0
- empathy_os/cache/dependency_manager.py +253 -0
- empathy_os/cache/hash_only.py +248 -0
- empathy_os/cache/hybrid.py +390 -0
- empathy_os/cache/storage.py +282 -0
- empathy_os/cli.py +118 -8
- empathy_os/cli_unified.py +121 -1
- empathy_os/config/__init__.py +63 -0
- empathy_os/config/xml_config.py +239 -0
- empathy_os/config.py +2 -1
- empathy_os/dashboard/__init__.py +15 -0
- empathy_os/dashboard/server.py +743 -0
- empathy_os/memory/__init__.py +195 -0
- empathy_os/memory/claude_memory.py +466 -0
- empathy_os/memory/config.py +224 -0
- empathy_os/memory/control_panel.py +1298 -0
- empathy_os/memory/edges.py +179 -0
- empathy_os/memory/graph.py +567 -0
- empathy_os/memory/long_term.py +1194 -0
- empathy_os/memory/nodes.py +179 -0
- empathy_os/memory/redis_bootstrap.py +540 -0
- empathy_os/memory/security/__init__.py +31 -0
- empathy_os/memory/security/audit_logger.py +930 -0
- empathy_os/memory/security/pii_scrubber.py +640 -0
- empathy_os/memory/security/secrets_detector.py +678 -0
- empathy_os/memory/short_term.py +2119 -0
- empathy_os/memory/storage/__init__.py +15 -0
- empathy_os/memory/summary_index.py +583 -0
- empathy_os/memory/unified.py +619 -0
- empathy_os/metrics/__init__.py +12 -0
- empathy_os/metrics/prompt_metrics.py +190 -0
- empathy_os/models/__init__.py +136 -0
- empathy_os/models/__main__.py +13 -0
- empathy_os/models/cli.py +655 -0
- empathy_os/models/empathy_executor.py +354 -0
- empathy_os/models/executor.py +252 -0
- empathy_os/models/fallback.py +671 -0
- empathy_os/models/provider_config.py +563 -0
- empathy_os/models/registry.py +382 -0
- empathy_os/models/tasks.py +302 -0
- empathy_os/models/telemetry.py +548 -0
- empathy_os/models/token_estimator.py +378 -0
- empathy_os/models/validation.py +274 -0
- empathy_os/monitoring/__init__.py +52 -0
- empathy_os/monitoring/alerts.py +23 -0
- empathy_os/monitoring/alerts_cli.py +268 -0
- empathy_os/monitoring/multi_backend.py +271 -0
- empathy_os/monitoring/otel_backend.py +363 -0
- empathy_os/optimization/__init__.py +19 -0
- empathy_os/optimization/context_optimizer.py +272 -0
- empathy_os/plugins/__init__.py +28 -0
- empathy_os/plugins/base.py +361 -0
- empathy_os/plugins/registry.py +268 -0
- empathy_os/project_index/__init__.py +30 -0
- empathy_os/project_index/cli.py +335 -0
- empathy_os/project_index/crew_integration.py +430 -0
- empathy_os/project_index/index.py +425 -0
- empathy_os/project_index/models.py +501 -0
- empathy_os/project_index/reports.py +473 -0
- empathy_os/project_index/scanner.py +538 -0
- empathy_os/prompts/__init__.py +61 -0
- empathy_os/prompts/config.py +77 -0
- empathy_os/prompts/context.py +177 -0
- empathy_os/prompts/parser.py +285 -0
- empathy_os/prompts/registry.py +313 -0
- empathy_os/prompts/templates.py +208 -0
- empathy_os/resilience/__init__.py +56 -0
- empathy_os/resilience/circuit_breaker.py +256 -0
- empathy_os/resilience/fallback.py +179 -0
- empathy_os/resilience/health.py +300 -0
- empathy_os/resilience/retry.py +209 -0
- empathy_os/resilience/timeout.py +135 -0
- empathy_os/routing/__init__.py +43 -0
- empathy_os/routing/chain_executor.py +433 -0
- empathy_os/routing/classifier.py +217 -0
- empathy_os/routing/smart_router.py +234 -0
- empathy_os/routing/wizard_registry.py +307 -0
- empathy_os/trust/__init__.py +28 -0
- empathy_os/trust/circuit_breaker.py +579 -0
- empathy_os/validation/__init__.py +19 -0
- empathy_os/validation/xml_validator.py +281 -0
- empathy_os/wizard_factory_cli.py +170 -0
- empathy_os/workflows/__init__.py +360 -0
- empathy_os/workflows/base.py +1660 -0
- empathy_os/workflows/bug_predict.py +962 -0
- empathy_os/workflows/code_review.py +960 -0
- empathy_os/workflows/code_review_adapters.py +310 -0
- empathy_os/workflows/code_review_pipeline.py +720 -0
- empathy_os/workflows/config.py +600 -0
- empathy_os/workflows/dependency_check.py +648 -0
- empathy_os/workflows/document_gen.py +1069 -0
- empathy_os/workflows/documentation_orchestrator.py +1205 -0
- empathy_os/workflows/health_check.py +679 -0
- empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
- empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
- empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
- empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
- empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
- empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
- empathy_os/workflows/manage_documentation.py +804 -0
- empathy_os/workflows/new_sample_workflow1.py +146 -0
- empathy_os/workflows/new_sample_workflow1_README.md +150 -0
- empathy_os/workflows/perf_audit.py +687 -0
- empathy_os/workflows/pr_review.py +748 -0
- empathy_os/workflows/progress.py +445 -0
- empathy_os/workflows/progress_server.py +322 -0
- empathy_os/workflows/refactor_plan.py +693 -0
- empathy_os/workflows/release_prep.py +808 -0
- empathy_os/workflows/research_synthesis.py +404 -0
- empathy_os/workflows/secure_release.py +585 -0
- empathy_os/workflows/security_adapters.py +297 -0
- empathy_os/workflows/security_audit.py +1046 -0
- empathy_os/workflows/step_config.py +234 -0
- empathy_os/workflows/test5.py +125 -0
- empathy_os/workflows/test5_README.md +158 -0
- empathy_os/workflows/test_gen.py +1855 -0
- empathy_os/workflows/test_lifecycle.py +526 -0
- empathy_os/workflows/test_maintenance.py +626 -0
- empathy_os/workflows/test_maintenance_cli.py +590 -0
- empathy_os/workflows/test_maintenance_crew.py +821 -0
- empathy_os/workflows/xml_enhanced_crew.py +285 -0
- empathy_software_plugin/cli/__init__.py +120 -0
- empathy_software_plugin/cli/inspect.py +362 -0
- empathy_software_plugin/cli.py +3 -1
- empathy_software_plugin/wizards/__init__.py +42 -0
- empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
- empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
- empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
- empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
- empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
- empathy_software_plugin/wizards/base_wizard.py +288 -0
- empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
- empathy_software_plugin/wizards/code_review_wizard.py +606 -0
- empathy_software_plugin/wizards/debugging/__init__.py +50 -0
- empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
- empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
- empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
- empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
- empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
- empathy_software_plugin/wizards/debugging/verification.py +369 -0
- empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
- empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
- empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
- empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
- empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
- empathy_software_plugin/wizards/performance/__init__.py +9 -0
- empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
- empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
- empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
- empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
- empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
- empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
- empathy_software_plugin/wizards/security/__init__.py +32 -0
- empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
- empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
- empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
- empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
- empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
- empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
- empathy_software_plugin/wizards/testing/__init__.py +27 -0
- empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
- empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
- empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
- empathy_software_plugin/wizards/testing_wizard.py +274 -0
- hot_reload/README.md +473 -0
- hot_reload/__init__.py +62 -0
- hot_reload/config.py +84 -0
- hot_reload/integration.py +228 -0
- hot_reload/reloader.py +298 -0
- hot_reload/watcher.py +179 -0
- hot_reload/websocket.py +176 -0
- scaffolding/README.md +589 -0
- scaffolding/__init__.py +35 -0
- scaffolding/__main__.py +14 -0
- scaffolding/cli.py +240 -0
- test_generator/__init__.py +38 -0
- test_generator/__main__.py +14 -0
- test_generator/cli.py +226 -0
- test_generator/generator.py +325 -0
- test_generator/risk_analyzer.py +216 -0
- workflow_patterns/__init__.py +33 -0
- workflow_patterns/behavior.py +249 -0
- workflow_patterns/core.py +76 -0
- workflow_patterns/output.py +99 -0
- workflow_patterns/registry.py +255 -0
- workflow_patterns/structural.py +288 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
- agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
- agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
- agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
- agents/compliance_anticipation_agent.py +0 -1422
- agents/compliance_db.py +0 -339
- agents/epic_integration_wizard.py +0 -530
- agents/notifications.py +0 -291
- agents/trust_building_behaviors.py +0 -872
- empathy_framework-3.7.0.dist-info/RECORD +0 -105
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
- /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)
|