empathy-framework 5.0.3__py3-none-any.whl → 5.1.1__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 (59) hide show
  1. {empathy_framework-5.0.3.dist-info → empathy_framework-5.1.1.dist-info}/METADATA +259 -142
  2. {empathy_framework-5.0.3.dist-info → empathy_framework-5.1.1.dist-info}/RECORD +58 -28
  3. empathy_framework-5.1.1.dist-info/licenses/LICENSE +201 -0
  4. empathy_framework-5.1.1.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  5. empathy_os/__init__.py +1 -1
  6. empathy_os/cli/commands/batch.py +5 -5
  7. empathy_os/cli/commands/routing.py +1 -1
  8. empathy_os/cli/commands/workflow.py +2 -1
  9. empathy_os/cli/parsers/cache 2.py +65 -0
  10. empathy_os/cli_minimal.py +3 -3
  11. empathy_os/cli_router 2.py +416 -0
  12. empathy_os/cli_router.py +12 -0
  13. empathy_os/dashboard/__init__.py +1 -2
  14. empathy_os/dashboard/app 2.py +512 -0
  15. empathy_os/dashboard/app.py +1 -1
  16. empathy_os/dashboard/simple_server 2.py +403 -0
  17. empathy_os/dashboard/standalone_server 2.py +536 -0
  18. empathy_os/memory/types 2.py +441 -0
  19. empathy_os/meta_workflows/intent_detector.py +71 -0
  20. empathy_os/models/__init__.py +19 -0
  21. empathy_os/models/adaptive_routing 2.py +437 -0
  22. empathy_os/models/auth_cli.py +444 -0
  23. empathy_os/models/auth_strategy.py +450 -0
  24. empathy_os/project_index/scanner_parallel 2.py +291 -0
  25. empathy_os/telemetry/agent_coordination 2.py +478 -0
  26. empathy_os/telemetry/agent_coordination.py +3 -3
  27. empathy_os/telemetry/agent_tracking 2.py +350 -0
  28. empathy_os/telemetry/agent_tracking.py +1 -2
  29. empathy_os/telemetry/approval_gates 2.py +563 -0
  30. empathy_os/telemetry/event_streaming 2.py +405 -0
  31. empathy_os/telemetry/event_streaming.py +3 -3
  32. empathy_os/telemetry/feedback_loop 2.py +557 -0
  33. empathy_os/telemetry/feedback_loop.py +1 -1
  34. empathy_os/vscode_bridge 2.py +173 -0
  35. empathy_os/workflows/__init__.py +8 -0
  36. empathy_os/workflows/autonomous_test_gen.py +569 -0
  37. empathy_os/workflows/bug_predict.py +45 -0
  38. empathy_os/workflows/code_review.py +92 -22
  39. empathy_os/workflows/document_gen.py +594 -62
  40. empathy_os/workflows/llm_base.py +363 -0
  41. empathy_os/workflows/perf_audit.py +69 -0
  42. empathy_os/workflows/progressive/README 2.md +454 -0
  43. empathy_os/workflows/progressive/__init__ 2.py +92 -0
  44. empathy_os/workflows/progressive/cli 2.py +242 -0
  45. empathy_os/workflows/progressive/core 2.py +488 -0
  46. empathy_os/workflows/progressive/orchestrator 2.py +701 -0
  47. empathy_os/workflows/progressive/reports 2.py +528 -0
  48. empathy_os/workflows/progressive/telemetry 2.py +280 -0
  49. empathy_os/workflows/progressive/test_gen 2.py +514 -0
  50. empathy_os/workflows/progressive/workflow 2.py +628 -0
  51. empathy_os/workflows/release_prep.py +54 -0
  52. empathy_os/workflows/security_audit.py +154 -79
  53. empathy_os/workflows/test_gen.py +60 -0
  54. empathy_os/workflows/test_gen_behavioral.py +477 -0
  55. empathy_os/workflows/test_gen_parallel.py +341 -0
  56. empathy_framework-5.0.3.dist-info/licenses/LICENSE +0 -139
  57. {empathy_framework-5.0.3.dist-info → empathy_framework-5.1.1.dist-info}/WHEEL +0 -0
  58. {empathy_framework-5.0.3.dist-info → empathy_framework-5.1.1.dist-info}/entry_points.txt +0 -0
  59. {empathy_framework-5.0.3.dist-info → empathy_framework-5.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,528 @@
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)