empathy-framework 5.1.1__py3-none-any.whl → 5.3.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.
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +79 -6
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +83 -64
- empathy_os/__init__.py +1 -1
- empathy_os/cache/hybrid.py +5 -1
- empathy_os/cli/commands/batch.py +8 -0
- empathy_os/cli/commands/profiling.py +4 -0
- empathy_os/cli/commands/workflow.py +8 -4
- empathy_os/cli_router.py +9 -0
- empathy_os/config.py +15 -2
- empathy_os/core_modules/__init__.py +15 -0
- empathy_os/dashboard/simple_server.py +62 -30
- empathy_os/mcp/__init__.py +10 -0
- empathy_os/mcp/server.py +506 -0
- empathy_os/memory/control_panel.py +1 -131
- empathy_os/memory/control_panel_support.py +145 -0
- empathy_os/memory/encryption.py +159 -0
- empathy_os/memory/long_term.py +46 -631
- empathy_os/memory/long_term_types.py +99 -0
- empathy_os/memory/mixins/__init__.py +25 -0
- empathy_os/memory/mixins/backend_init_mixin.py +249 -0
- empathy_os/memory/mixins/capabilities_mixin.py +208 -0
- empathy_os/memory/mixins/handoff_mixin.py +208 -0
- empathy_os/memory/mixins/lifecycle_mixin.py +49 -0
- empathy_os/memory/mixins/long_term_mixin.py +352 -0
- empathy_os/memory/mixins/promotion_mixin.py +109 -0
- empathy_os/memory/mixins/short_term_mixin.py +182 -0
- empathy_os/memory/short_term.py +61 -12
- empathy_os/memory/simple_storage.py +302 -0
- empathy_os/memory/storage_backend.py +167 -0
- empathy_os/memory/types.py +8 -3
- empathy_os/memory/unified.py +21 -1120
- empathy_os/meta_workflows/cli_commands/__init__.py +56 -0
- empathy_os/meta_workflows/cli_commands/agent_commands.py +321 -0
- empathy_os/meta_workflows/cli_commands/analytics_commands.py +442 -0
- empathy_os/meta_workflows/cli_commands/config_commands.py +232 -0
- empathy_os/meta_workflows/cli_commands/memory_commands.py +182 -0
- empathy_os/meta_workflows/cli_commands/template_commands.py +354 -0
- empathy_os/meta_workflows/cli_commands/workflow_commands.py +382 -0
- empathy_os/meta_workflows/cli_meta_workflows.py +52 -1802
- empathy_os/models/telemetry/__init__.py +71 -0
- empathy_os/models/telemetry/analytics.py +594 -0
- empathy_os/models/telemetry/backend.py +196 -0
- empathy_os/models/telemetry/data_models.py +431 -0
- empathy_os/models/telemetry/storage.py +489 -0
- empathy_os/orchestration/__init__.py +35 -0
- empathy_os/orchestration/execution_strategies.py +481 -0
- empathy_os/orchestration/meta_orchestrator.py +488 -1
- empathy_os/routing/workflow_registry.py +36 -0
- empathy_os/telemetry/agent_coordination.py +2 -3
- empathy_os/telemetry/agent_tracking.py +26 -7
- empathy_os/telemetry/approval_gates.py +18 -24
- empathy_os/telemetry/cli.py +19 -724
- empathy_os/telemetry/commands/__init__.py +14 -0
- empathy_os/telemetry/commands/dashboard_commands.py +696 -0
- empathy_os/telemetry/event_streaming.py +7 -3
- empathy_os/telemetry/feedback_loop.py +28 -15
- empathy_os/tools.py +183 -0
- empathy_os/workflows/__init__.py +5 -0
- empathy_os/workflows/autonomous_test_gen.py +860 -161
- empathy_os/workflows/base.py +6 -2
- empathy_os/workflows/code_review.py +4 -1
- empathy_os/workflows/document_gen/__init__.py +25 -0
- empathy_os/workflows/document_gen/config.py +30 -0
- empathy_os/workflows/document_gen/report_formatter.py +162 -0
- empathy_os/workflows/{document_gen.py → document_gen/workflow.py} +5 -184
- empathy_os/workflows/output.py +4 -1
- empathy_os/workflows/progress.py +8 -2
- empathy_os/workflows/security_audit.py +2 -2
- empathy_os/workflows/security_audit_phase3.py +7 -4
- empathy_os/workflows/seo_optimization.py +633 -0
- empathy_os/workflows/test_gen/__init__.py +52 -0
- empathy_os/workflows/test_gen/ast_analyzer.py +249 -0
- empathy_os/workflows/test_gen/config.py +88 -0
- empathy_os/workflows/test_gen/data_models.py +38 -0
- empathy_os/workflows/test_gen/report_formatter.py +289 -0
- empathy_os/workflows/test_gen/test_templates.py +381 -0
- empathy_os/workflows/test_gen/workflow.py +655 -0
- empathy_os/workflows/test_gen.py +42 -1905
- empathy_os/cli/parsers/cache 2.py +0 -65
- empathy_os/cli_router 2.py +0 -416
- empathy_os/dashboard/app 2.py +0 -512
- empathy_os/dashboard/simple_server 2.py +0 -403
- empathy_os/dashboard/standalone_server 2.py +0 -536
- empathy_os/memory/types 2.py +0 -441
- empathy_os/models/adaptive_routing 2.py +0 -437
- empathy_os/models/telemetry.py +0 -1660
- empathy_os/project_index/scanner_parallel 2.py +0 -291
- empathy_os/telemetry/agent_coordination 2.py +0 -478
- empathy_os/telemetry/agent_tracking 2.py +0 -350
- empathy_os/telemetry/approval_gates 2.py +0 -563
- empathy_os/telemetry/event_streaming 2.py +0 -405
- empathy_os/telemetry/feedback_loop 2.py +0 -557
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
"""Report generation and result storage for progressive workflows.
|
|
2
|
-
|
|
3
|
-
This module provides utilities for:
|
|
4
|
-
1. Generating human-readable progression reports
|
|
5
|
-
2. Saving detailed results to disk
|
|
6
|
-
3. Formatting cost analysis
|
|
7
|
-
4. Creating progression visualizations
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import logging
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
from empathy_os.workflows.progressive.core import (
|
|
16
|
-
ProgressiveWorkflowResult,
|
|
17
|
-
Tier,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def generate_progression_report(result: ProgressiveWorkflowResult) -> str:
|
|
24
|
-
"""Generate human-readable progression report.
|
|
25
|
-
|
|
26
|
-
Creates a detailed ASCII report showing:
|
|
27
|
-
- Tier-by-tier breakdown
|
|
28
|
-
- Quality scores and success rates
|
|
29
|
-
- Cost analysis and savings
|
|
30
|
-
- Escalation decisions
|
|
31
|
-
- Final results summary
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
result: Progressive workflow result
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
Formatted report string
|
|
38
|
-
|
|
39
|
-
Example:
|
|
40
|
-
>>> print(generate_progression_report(result))
|
|
41
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
-
🎯 PROGRESSIVE ESCALATION REPORT
|
|
43
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
-
...
|
|
45
|
-
"""
|
|
46
|
-
report = []
|
|
47
|
-
|
|
48
|
-
# Header
|
|
49
|
-
report.append("━" * 60)
|
|
50
|
-
report.append("🎯 PROGRESSIVE ESCALATION REPORT")
|
|
51
|
-
report.append("━" * 60)
|
|
52
|
-
report.append("")
|
|
53
|
-
|
|
54
|
-
# Summary
|
|
55
|
-
report.append(f"Workflow: {result.workflow_name}")
|
|
56
|
-
report.append(f"Task ID: {result.task_id}")
|
|
57
|
-
report.append(f"Duration: {_format_duration(result.total_duration)}")
|
|
58
|
-
report.append(f"Total Cost: ${result.total_cost:.2f}")
|
|
59
|
-
report.append("")
|
|
60
|
-
|
|
61
|
-
# Cost savings
|
|
62
|
-
if result.cost_savings > 0:
|
|
63
|
-
report.append(f"Cost Savings: ${result.cost_savings:.2f} ({result.cost_savings_percent:.0f}% vs all-Premium)")
|
|
64
|
-
report.append("")
|
|
65
|
-
|
|
66
|
-
report.append("TIER BREAKDOWN:")
|
|
67
|
-
report.append("")
|
|
68
|
-
|
|
69
|
-
# Tier-by-tier breakdown
|
|
70
|
-
for tier_result in result.tier_results:
|
|
71
|
-
tier_emoji = {
|
|
72
|
-
Tier.CHEAP: "💰",
|
|
73
|
-
Tier.CAPABLE: "📊",
|
|
74
|
-
Tier.PREMIUM: "💎"
|
|
75
|
-
}[tier_result.tier]
|
|
76
|
-
|
|
77
|
-
report.append(f"{tier_emoji} {tier_result.tier.value.upper()} Tier ({tier_result.model})")
|
|
78
|
-
report.append(f" • Items: {len(tier_result.generated_items)}")
|
|
79
|
-
report.append(f" • Attempts: {tier_result.attempt}")
|
|
80
|
-
|
|
81
|
-
success_count = tier_result.success_count
|
|
82
|
-
total_items = len(tier_result.generated_items)
|
|
83
|
-
success_rate = tier_result.success_rate * 100
|
|
84
|
-
|
|
85
|
-
report.append(f" • Success: {success_count}/{total_items} ({success_rate:.0f}%)")
|
|
86
|
-
report.append(f" • Quality: CQS={tier_result.quality_score:.1f}")
|
|
87
|
-
report.append(f" • Cost: ${tier_result.cost:.2f}")
|
|
88
|
-
report.append(f" • Duration: {_format_duration(tier_result.duration)}")
|
|
89
|
-
|
|
90
|
-
if tier_result.escalated:
|
|
91
|
-
report.append(f" • Escalated: {tier_result.escalation_reason}")
|
|
92
|
-
|
|
93
|
-
report.append("")
|
|
94
|
-
|
|
95
|
-
report.append("━" * 60)
|
|
96
|
-
report.append("")
|
|
97
|
-
report.append("FINAL RESULTS:")
|
|
98
|
-
|
|
99
|
-
total_items = sum(len(r.generated_items) for r in result.tier_results)
|
|
100
|
-
total_successful = sum(r.success_count for r in result.tier_results)
|
|
101
|
-
|
|
102
|
-
status_icon = "✅" if result.success else "❌"
|
|
103
|
-
status_text = "Success" if result.success else "Incomplete"
|
|
104
|
-
|
|
105
|
-
report.append(f"{status_icon} {total_successful}/{total_items} items completed")
|
|
106
|
-
report.append(f"{status_icon} Overall CQS: {result.final_result.quality_score:.0f}")
|
|
107
|
-
report.append(f"{status_icon} Status: {status_text}")
|
|
108
|
-
report.append("")
|
|
109
|
-
|
|
110
|
-
report.append("━" * 60)
|
|
111
|
-
report.append("")
|
|
112
|
-
report.append("Detailed results saved to:")
|
|
113
|
-
report.append(f".empathy/progressive_runs/{result.task_id}/")
|
|
114
|
-
report.append("")
|
|
115
|
-
|
|
116
|
-
return "\n".join(report)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def save_results_to_disk(result: ProgressiveWorkflowResult, storage_path: str) -> None:
|
|
120
|
-
"""Save detailed results to disk.
|
|
121
|
-
|
|
122
|
-
Creates a directory structure:
|
|
123
|
-
<storage_path>/<task_id>/
|
|
124
|
-
├── summary.json
|
|
125
|
-
├── tier_0_cheap.json
|
|
126
|
-
├── tier_1_capable.json
|
|
127
|
-
├── tier_2_premium.json (if used)
|
|
128
|
-
└── report.txt
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
result: Progressive workflow result
|
|
132
|
-
storage_path: Base directory for storage
|
|
133
|
-
|
|
134
|
-
Example:
|
|
135
|
-
>>> save_results_to_disk(result, ".empathy/progressive_runs")
|
|
136
|
-
# Creates .empathy/progressive_runs/test-gen-20260117-143022/...
|
|
137
|
-
"""
|
|
138
|
-
task_dir = Path(storage_path) / result.task_id
|
|
139
|
-
task_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
# Use the created task directory
|
|
143
|
-
validated_dir = task_dir
|
|
144
|
-
|
|
145
|
-
# Save summary
|
|
146
|
-
summary = {
|
|
147
|
-
"workflow": result.workflow_name,
|
|
148
|
-
"task_id": result.task_id,
|
|
149
|
-
"timestamp": result.tier_results[0].timestamp.isoformat() if result.tier_results else None,
|
|
150
|
-
"total_cost": result.total_cost,
|
|
151
|
-
"total_duration": result.total_duration,
|
|
152
|
-
"cost_savings": result.cost_savings,
|
|
153
|
-
"cost_savings_percent": result.cost_savings_percent,
|
|
154
|
-
"success": result.success,
|
|
155
|
-
"tier_count": len(result.tier_results),
|
|
156
|
-
"final_cqs": result.final_result.quality_score if result.final_result else 0
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
summary_file = validated_dir / "summary.json"
|
|
160
|
-
summary_file.write_text(json.dumps(summary, indent=2))
|
|
161
|
-
|
|
162
|
-
# Save each tier result
|
|
163
|
-
for i, tier_result in enumerate(result.tier_results):
|
|
164
|
-
tier_data = {
|
|
165
|
-
"tier": tier_result.tier.value,
|
|
166
|
-
"model": tier_result.model,
|
|
167
|
-
"attempt": tier_result.attempt,
|
|
168
|
-
"timestamp": tier_result.timestamp.isoformat(),
|
|
169
|
-
"quality_score": tier_result.quality_score,
|
|
170
|
-
"success_count": tier_result.success_count,
|
|
171
|
-
"success_rate": tier_result.success_rate,
|
|
172
|
-
"cost": tier_result.cost,
|
|
173
|
-
"duration": tier_result.duration,
|
|
174
|
-
"escalated": tier_result.escalated,
|
|
175
|
-
"escalation_reason": tier_result.escalation_reason,
|
|
176
|
-
"failure_analysis": {
|
|
177
|
-
"syntax_errors": len(tier_result.failure_analysis.syntax_errors),
|
|
178
|
-
"test_pass_rate": tier_result.failure_analysis.test_pass_rate,
|
|
179
|
-
"coverage": tier_result.failure_analysis.coverage_percent,
|
|
180
|
-
"assertion_depth": tier_result.failure_analysis.assertion_depth,
|
|
181
|
-
"confidence": tier_result.failure_analysis.confidence_score
|
|
182
|
-
},
|
|
183
|
-
"item_count": len(tier_result.generated_items)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
tier_file = validated_dir / f"tier_{i}_{tier_result.tier.value}.json"
|
|
187
|
-
tier_file.write_text(json.dumps(tier_data, indent=2))
|
|
188
|
-
|
|
189
|
-
# Save human-readable report
|
|
190
|
-
report_file = validated_dir / "report.txt"
|
|
191
|
-
report_file.write_text(generate_progression_report(result))
|
|
192
|
-
|
|
193
|
-
logger.info(f"Saved progressive results to {validated_dir}")
|
|
194
|
-
|
|
195
|
-
except ValueError as e:
|
|
196
|
-
logger.error(f"Failed to save results: {e}")
|
|
197
|
-
raise
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _format_duration(seconds: float) -> str:
|
|
201
|
-
"""Format duration in human-readable form.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
seconds: Duration in seconds
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
Formatted string (e.g., "1m 23s", "45s")
|
|
208
|
-
|
|
209
|
-
Example:
|
|
210
|
-
>>> _format_duration(83.5)
|
|
211
|
-
'1m 24s'
|
|
212
|
-
>>> _format_duration(12.3)
|
|
213
|
-
'12s'
|
|
214
|
-
"""
|
|
215
|
-
if seconds < 60:
|
|
216
|
-
return f"{int(seconds)}s"
|
|
217
|
-
|
|
218
|
-
minutes = int(seconds // 60)
|
|
219
|
-
remaining_seconds = int(seconds % 60)
|
|
220
|
-
|
|
221
|
-
return f"{minutes}m {remaining_seconds}s"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def load_result_from_disk(task_id: str, storage_path: str = ".empathy/progressive_runs") -> dict[str, Any]:
|
|
225
|
-
"""Load saved result from disk.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
task_id: Task ID to load
|
|
229
|
-
storage_path: Base storage directory
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
Dictionary with summary and tier results
|
|
233
|
-
|
|
234
|
-
Raises:
|
|
235
|
-
FileNotFoundError: If task_id not found
|
|
236
|
-
|
|
237
|
-
Example:
|
|
238
|
-
>>> result = load_result_from_disk("test-gen-20260117-143022")
|
|
239
|
-
>>> print(result["summary"]["total_cost"])
|
|
240
|
-
0.95
|
|
241
|
-
"""
|
|
242
|
-
task_dir = Path(storage_path) / task_id
|
|
243
|
-
|
|
244
|
-
if not task_dir.exists():
|
|
245
|
-
raise FileNotFoundError(f"Task {task_id} not found in {storage_path}")
|
|
246
|
-
|
|
247
|
-
# Load summary
|
|
248
|
-
summary_file = task_dir / "summary.json"
|
|
249
|
-
if not summary_file.exists():
|
|
250
|
-
raise FileNotFoundError(f"Summary file not found for task {task_id}")
|
|
251
|
-
|
|
252
|
-
summary = json.loads(summary_file.read_text())
|
|
253
|
-
|
|
254
|
-
# Load tier results
|
|
255
|
-
tier_results = []
|
|
256
|
-
for tier_file in sorted(task_dir.glob("tier_*.json")):
|
|
257
|
-
tier_data = json.loads(tier_file.read_text())
|
|
258
|
-
tier_results.append(tier_data)
|
|
259
|
-
|
|
260
|
-
# Load report
|
|
261
|
-
report_file = task_dir / "report.txt"
|
|
262
|
-
report = report_file.read_text() if report_file.exists() else ""
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
"summary": summary,
|
|
266
|
-
"tier_results": tier_results,
|
|
267
|
-
"report": report
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def list_saved_results(storage_path: str = ".empathy/progressive_runs") -> list[dict[str, Any]]:
|
|
272
|
-
"""List all saved progressive results.
|
|
273
|
-
|
|
274
|
-
Args:
|
|
275
|
-
storage_path: Base storage directory
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
List of result summaries sorted by timestamp (newest first)
|
|
279
|
-
|
|
280
|
-
Example:
|
|
281
|
-
>>> results = list_saved_results()
|
|
282
|
-
>>> for r in results:
|
|
283
|
-
... print(f"{r['task_id']}: ${r['total_cost']:.2f}")
|
|
284
|
-
"""
|
|
285
|
-
storage_dir = Path(storage_path)
|
|
286
|
-
|
|
287
|
-
if not storage_dir.exists():
|
|
288
|
-
return []
|
|
289
|
-
|
|
290
|
-
summaries = []
|
|
291
|
-
|
|
292
|
-
for task_dir in storage_dir.iterdir():
|
|
293
|
-
if not task_dir.is_dir():
|
|
294
|
-
continue
|
|
295
|
-
|
|
296
|
-
summary_file = task_dir / "summary.json"
|
|
297
|
-
if not summary_file.exists():
|
|
298
|
-
continue
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
summary = json.loads(summary_file.read_text())
|
|
302
|
-
summaries.append(summary)
|
|
303
|
-
except (json.JSONDecodeError, OSError) as e:
|
|
304
|
-
logger.warning(f"Failed to load summary from {task_dir}: {e}")
|
|
305
|
-
|
|
306
|
-
# Sort by timestamp (newest first)
|
|
307
|
-
summaries.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
308
|
-
|
|
309
|
-
return summaries
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def cleanup_old_results(
|
|
313
|
-
storage_path: str = ".empathy/progressive_runs",
|
|
314
|
-
retention_days: int = 30,
|
|
315
|
-
dry_run: bool = False
|
|
316
|
-
) -> tuple[int, int]:
|
|
317
|
-
"""Clean up old progressive workflow results.
|
|
318
|
-
|
|
319
|
-
Args:
|
|
320
|
-
storage_path: Base storage directory
|
|
321
|
-
retention_days: Number of days to retain results (default: 30)
|
|
322
|
-
dry_run: If True, only report what would be deleted without deleting
|
|
323
|
-
|
|
324
|
-
Returns:
|
|
325
|
-
Tuple of (deleted_count, retained_count)
|
|
326
|
-
|
|
327
|
-
Example:
|
|
328
|
-
>>> deleted, retained = cleanup_old_results(retention_days=7)
|
|
329
|
-
>>> print(f"Deleted {deleted} old results, kept {retained}")
|
|
330
|
-
"""
|
|
331
|
-
from datetime import datetime, timedelta
|
|
332
|
-
|
|
333
|
-
storage_dir = Path(storage_path)
|
|
334
|
-
|
|
335
|
-
if not storage_dir.exists():
|
|
336
|
-
return (0, 0)
|
|
337
|
-
|
|
338
|
-
cutoff_date = datetime.now() - timedelta(days=retention_days)
|
|
339
|
-
deleted_count = 0
|
|
340
|
-
retained_count = 0
|
|
341
|
-
|
|
342
|
-
for task_dir in storage_dir.iterdir():
|
|
343
|
-
if not task_dir.is_dir():
|
|
344
|
-
continue
|
|
345
|
-
|
|
346
|
-
summary_file = task_dir / "summary.json"
|
|
347
|
-
if not summary_file.exists():
|
|
348
|
-
continue
|
|
349
|
-
|
|
350
|
-
try:
|
|
351
|
-
summary = json.loads(summary_file.read_text())
|
|
352
|
-
timestamp_str = summary.get("timestamp")
|
|
353
|
-
|
|
354
|
-
if not timestamp_str:
|
|
355
|
-
logger.warning(f"No timestamp in {task_dir}, skipping")
|
|
356
|
-
retained_count += 1
|
|
357
|
-
continue
|
|
358
|
-
|
|
359
|
-
timestamp = datetime.fromisoformat(timestamp_str)
|
|
360
|
-
|
|
361
|
-
if timestamp < cutoff_date:
|
|
362
|
-
# Old result, delete it
|
|
363
|
-
if not dry_run:
|
|
364
|
-
import shutil
|
|
365
|
-
shutil.rmtree(task_dir)
|
|
366
|
-
logger.info(f"Deleted old result: {task_dir.name}")
|
|
367
|
-
else:
|
|
368
|
-
logger.info(f"Would delete: {task_dir.name}")
|
|
369
|
-
deleted_count += 1
|
|
370
|
-
else:
|
|
371
|
-
retained_count += 1
|
|
372
|
-
|
|
373
|
-
except (json.JSONDecodeError, ValueError, OSError) as e:
|
|
374
|
-
logger.warning(f"Error processing {task_dir}: {e}")
|
|
375
|
-
retained_count += 1
|
|
376
|
-
|
|
377
|
-
return (deleted_count, retained_count)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def generate_cost_analytics(
|
|
381
|
-
storage_path: str = ".empathy/progressive_runs"
|
|
382
|
-
) -> dict[str, Any]:
|
|
383
|
-
"""Generate cost optimization analytics from saved results.
|
|
384
|
-
|
|
385
|
-
Analyzes historical progressive workflow runs to provide insights:
|
|
386
|
-
- Total cost savings
|
|
387
|
-
- Average escalation rate
|
|
388
|
-
- Most cost-effective workflow types
|
|
389
|
-
- Tier usage distribution
|
|
390
|
-
- Success rates by tier
|
|
391
|
-
|
|
392
|
-
Args:
|
|
393
|
-
storage_path: Base storage directory
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
Dictionary with analytics data
|
|
397
|
-
|
|
398
|
-
Example:
|
|
399
|
-
>>> analytics = generate_cost_analytics()
|
|
400
|
-
>>> print(f"Total savings: ${analytics['total_savings']:.2f}")
|
|
401
|
-
>>> print(f"Avg escalation rate: {analytics['avg_escalation_rate']:.1%}")
|
|
402
|
-
"""
|
|
403
|
-
results = list_saved_results(storage_path)
|
|
404
|
-
|
|
405
|
-
if not results:
|
|
406
|
-
return {
|
|
407
|
-
"total_runs": 0,
|
|
408
|
-
"total_cost": 0.0,
|
|
409
|
-
"total_savings": 0.0,
|
|
410
|
-
"avg_savings_percent": 0.0
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
total_runs = len(results)
|
|
414
|
-
total_cost = sum(r.get("total_cost", 0) for r in results)
|
|
415
|
-
total_savings = sum(r.get("cost_savings", 0) for r in results)
|
|
416
|
-
|
|
417
|
-
# Calculate average savings percent (weighted by cost)
|
|
418
|
-
weighted_savings = sum(
|
|
419
|
-
r.get("cost_savings_percent", 0) * r.get("total_cost", 0)
|
|
420
|
-
for r in results
|
|
421
|
-
)
|
|
422
|
-
avg_savings_percent = weighted_savings / total_cost if total_cost > 0 else 0
|
|
423
|
-
|
|
424
|
-
# Tier usage statistics
|
|
425
|
-
tier_usage = {"cheap": 0, "capable": 0, "premium": 0}
|
|
426
|
-
tier_costs = {"cheap": 0.0, "capable": 0.0, "premium": 0.0}
|
|
427
|
-
escalation_count = 0
|
|
428
|
-
|
|
429
|
-
for result in results:
|
|
430
|
-
tier_count = result.get("tier_count", 0)
|
|
431
|
-
if tier_count > 1:
|
|
432
|
-
escalation_count += 1
|
|
433
|
-
|
|
434
|
-
escalation_rate = escalation_count / total_runs if total_runs > 0 else 0
|
|
435
|
-
|
|
436
|
-
# Success rate
|
|
437
|
-
successful_runs = sum(1 for r in results if r.get("success", False))
|
|
438
|
-
success_rate = successful_runs / total_runs if total_runs > 0 else 0
|
|
439
|
-
|
|
440
|
-
# Average final CQS
|
|
441
|
-
avg_cqs = sum(r.get("final_cqs", 0) for r in results) / total_runs if total_runs > 0 else 0
|
|
442
|
-
|
|
443
|
-
# Per-workflow analytics
|
|
444
|
-
workflow_stats: dict[str, dict[str, Any]] = {}
|
|
445
|
-
for result in results:
|
|
446
|
-
workflow = result.get("workflow", "unknown")
|
|
447
|
-
if workflow not in workflow_stats:
|
|
448
|
-
workflow_stats[workflow] = {
|
|
449
|
-
"runs": 0,
|
|
450
|
-
"total_cost": 0.0,
|
|
451
|
-
"total_savings": 0.0,
|
|
452
|
-
"successes": 0
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
stats = workflow_stats[workflow]
|
|
456
|
-
stats["runs"] += 1
|
|
457
|
-
stats["total_cost"] += result.get("total_cost", 0)
|
|
458
|
-
stats["total_savings"] += result.get("cost_savings", 0)
|
|
459
|
-
if result.get("success", False):
|
|
460
|
-
stats["successes"] += 1
|
|
461
|
-
|
|
462
|
-
# Calculate per-workflow averages
|
|
463
|
-
for stats in workflow_stats.values():
|
|
464
|
-
stats["avg_cost"] = stats["total_cost"] / stats["runs"]
|
|
465
|
-
stats["avg_savings"] = stats["total_savings"] / stats["runs"]
|
|
466
|
-
stats["success_rate"] = stats["successes"] / stats["runs"]
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
"total_runs": total_runs,
|
|
470
|
-
"total_cost": round(total_cost, 2),
|
|
471
|
-
"total_savings": round(total_savings, 2),
|
|
472
|
-
"avg_savings_percent": round(avg_savings_percent, 1),
|
|
473
|
-
"escalation_rate": round(escalation_rate, 2),
|
|
474
|
-
"success_rate": round(success_rate, 2),
|
|
475
|
-
"avg_final_cqs": round(avg_cqs, 1),
|
|
476
|
-
"tier_usage": tier_usage,
|
|
477
|
-
"tier_costs": tier_costs,
|
|
478
|
-
"workflow_stats": workflow_stats
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def format_cost_analytics_report(analytics: dict[str, Any]) -> str:
|
|
483
|
-
"""Format cost analytics as human-readable report.
|
|
484
|
-
|
|
485
|
-
Args:
|
|
486
|
-
analytics: Analytics data from generate_cost_analytics()
|
|
487
|
-
|
|
488
|
-
Returns:
|
|
489
|
-
Formatted report string
|
|
490
|
-
|
|
491
|
-
Example:
|
|
492
|
-
>>> analytics = generate_cost_analytics()
|
|
493
|
-
>>> print(format_cost_analytics_report(analytics))
|
|
494
|
-
"""
|
|
495
|
-
report = []
|
|
496
|
-
|
|
497
|
-
report.append("━" * 60)
|
|
498
|
-
report.append("📊 PROGRESSIVE ESCALATION ANALYTICS")
|
|
499
|
-
report.append("━" * 60)
|
|
500
|
-
report.append("")
|
|
501
|
-
|
|
502
|
-
# Overall statistics
|
|
503
|
-
report.append("OVERALL STATISTICS:")
|
|
504
|
-
report.append(f" Total Runs: {analytics['total_runs']}")
|
|
505
|
-
report.append(f" Total Cost: ${analytics['total_cost']:.2f}")
|
|
506
|
-
report.append(f" Total Savings: ${analytics['total_savings']:.2f}")
|
|
507
|
-
report.append(f" Avg Savings: {analytics['avg_savings_percent']:.1f}%")
|
|
508
|
-
report.append(f" Escalation Rate: {analytics['escalation_rate']:.1%}")
|
|
509
|
-
report.append(f" Success Rate: {analytics['success_rate']:.1%}")
|
|
510
|
-
report.append(f" Avg Final CQS: {analytics['avg_final_cqs']:.1f}")
|
|
511
|
-
report.append("")
|
|
512
|
-
|
|
513
|
-
# Per-workflow breakdown
|
|
514
|
-
if analytics.get("workflow_stats"):
|
|
515
|
-
report.append("PER-WORKFLOW BREAKDOWN:")
|
|
516
|
-
report.append("")
|
|
517
|
-
|
|
518
|
-
for workflow, stats in sorted(analytics["workflow_stats"].items()):
|
|
519
|
-
report.append(f" {workflow}:")
|
|
520
|
-
report.append(f" Runs: {stats['runs']}")
|
|
521
|
-
report.append(f" Avg Cost: ${stats['avg_cost']:.2f}")
|
|
522
|
-
report.append(f" Avg Savings: ${stats['avg_savings']:.2f}")
|
|
523
|
-
report.append(f" Success Rate: {stats['success_rate']:.1%}")
|
|
524
|
-
report.append("")
|
|
525
|
-
|
|
526
|
-
report.append("━" * 60)
|
|
527
|
-
|
|
528
|
-
return "\n".join(report)
|