empathy-framework 3.9.2__py3-none-any.whl → 3.10.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 (32) hide show
  1. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/METADATA +37 -5
  2. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/RECORD +32 -32
  3. empathy_llm_toolkit/agent_factory/crews/health_check.py +36 -29
  4. empathy_os/__init__.py +1 -1
  5. empathy_os/cache/hybrid.py +69 -9
  6. empathy_os/cli.py +183 -18
  7. empathy_os/cli_unified.py +113 -4
  8. empathy_os/config.py +7 -4
  9. empathy_os/hot_reload/integration.py +2 -1
  10. empathy_os/hot_reload/watcher.py +8 -4
  11. empathy_os/hot_reload/websocket.py +2 -1
  12. empathy_os/models/telemetry.py +900 -2
  13. empathy_os/test_generator/generator.py +1 -1
  14. empathy_os/tier_recommender.py +3 -3
  15. empathy_os/workflows/base.py +5 -2
  16. empathy_os/workflows/health_check.py +37 -0
  17. empathy_os/workflows/new_sample_workflow1.py +3 -3
  18. empathy_os/workflows/tier_tracking.py +1 -1
  19. empathy_software_plugin/wizards/advanced_debugging_wizard.py +9 -6
  20. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +1 -1
  21. empathy_software_plugin/wizards/debugging/config_loaders.py +6 -2
  22. empathy_software_plugin/wizards/debugging/language_patterns.py +4 -2
  23. empathy_software_plugin/wizards/debugging/linter_parsers.py +1 -1
  24. empathy_software_plugin/wizards/performance/profiler_parsers.py +7 -7
  25. empathy_software_plugin/wizards/security/vulnerability_scanner.py +1 -1
  26. empathy_software_plugin/wizards/security_analysis_wizard.py +2 -2
  27. empathy_software_plugin/wizards/testing/quality_analyzer.py +3 -9
  28. empathy_software_plugin/wizards/testing/test_suggester.py +1 -1
  29. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/WHEEL +0 -0
  30. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/entry_points.txt +0 -0
  31. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/licenses/LICENSE +0 -0
  32. {empathy_framework-3.9.2.dist-info → empathy_framework-3.10.1.dist-info}/top_level.txt +0 -0
empathy_os/cli.py CHANGED
@@ -41,11 +41,15 @@ from empathy_os.workflows import list_workflows as get_workflow_list
41
41
  # Import telemetry CLI commands
42
42
  try:
43
43
  from empathy_os.telemetry.cli import (
44
+ cmd_agent_performance,
45
+ cmd_task_routing_report,
44
46
  cmd_telemetry_compare,
45
47
  cmd_telemetry_export,
46
48
  cmd_telemetry_reset,
47
49
  cmd_telemetry_savings,
48
50
  cmd_telemetry_show,
51
+ cmd_test_status,
52
+ cmd_tier1_status,
49
53
  )
50
54
 
51
55
  TELEMETRY_CLI_AVAILABLE = True
@@ -2148,7 +2152,20 @@ def cmd_workflow(args):
2148
2152
 
2149
2153
  wf_config = WorkflowConfig.load()
2150
2154
  provider = wf_config.default_provider
2151
- workflow = workflow_cls(provider=provider)
2155
+
2156
+ # Initialize workflow with tier fallback if requested
2157
+ use_tier_fallback = getattr(args, "use_recommended_tier", False)
2158
+ workflow_kwargs = {
2159
+ "provider": provider,
2160
+ "enable_tier_fallback": use_tier_fallback,
2161
+ }
2162
+
2163
+ # Add health-check specific parameters
2164
+ if name == "health-check":
2165
+ health_score_threshold = getattr(args, "health_score_threshold", 100)
2166
+ workflow_kwargs["health_score_threshold"] = health_score_threshold
2167
+
2168
+ workflow = workflow_cls(**workflow_kwargs)
2152
2169
 
2153
2170
  # Parse input
2154
2171
  input_data = {}
@@ -2244,25 +2261,76 @@ def cmd_workflow(args):
2244
2261
  }
2245
2262
  print(json_mod.dumps(output, indent=2))
2246
2263
  # Display the actual results - this is what users want to see
2247
- elif result.success:
2248
- if output_content:
2249
- print(f"\n{output_content}\n")
2250
- else:
2251
- print("\n✓ Workflow completed successfully.\n")
2252
2264
  else:
2253
- # Extract error from various result types
2254
- error_msg = getattr(result, "error", None)
2255
- if not error_msg:
2256
- # Check for blockers (CodeReviewPipelineResult)
2257
- blockers = getattr(result, "blockers", [])
2258
- if blockers:
2259
- error_msg = "; ".join(blockers)
2265
+ # Show tier progression if tier fallback was used
2266
+ if use_tier_fallback and hasattr(workflow, "_tier_progression"):
2267
+ tier_progression = workflow._tier_progression
2268
+ if tier_progression:
2269
+ print("\n" + "=" * 60)
2270
+ print(" TIER PROGRESSION (Intelligent Fallback)")
2271
+ print("=" * 60)
2272
+
2273
+ # Group by stage
2274
+ stage_tiers: dict[str, list[tuple[str, bool]]] = {}
2275
+ for stage, tier, success in tier_progression:
2276
+ if stage not in stage_tiers:
2277
+ stage_tiers[stage] = []
2278
+ stage_tiers[stage].append((tier, success))
2279
+
2280
+ # Display progression for each stage
2281
+ for stage, attempts in stage_tiers.items():
2282
+ status = "✓" if any(success for _, success in attempts) else "✗"
2283
+ print(f"\n{status} Stage: {stage}")
2284
+
2285
+ for idx, (tier, success) in enumerate(attempts, 1):
2286
+ attempt_status = "✓ SUCCESS" if success else "✗ FAILED"
2287
+ if idx == 1:
2288
+ print(f" Attempt {idx}: {tier.upper():8} → {attempt_status}")
2289
+ else:
2290
+ prev_tier = attempts[idx - 2][0]
2291
+ print(
2292
+ f" Attempt {idx}: {tier.upper():8} → {attempt_status} "
2293
+ f"(upgraded from {prev_tier.upper()})"
2294
+ )
2295
+
2296
+ # Calculate cost savings (only if result has stages attribute)
2297
+ if hasattr(result, "stages") and result.stages:
2298
+ actual_cost = sum(stage.cost for stage in result.stages if stage.cost)
2299
+ # Estimate what cost would be if all stages used PREMIUM
2300
+ premium_cost = actual_cost * 3 # Conservative estimate
2301
+
2302
+ savings = premium_cost - actual_cost
2303
+ savings_pct = (savings / premium_cost * 100) if premium_cost > 0 else 0
2304
+
2305
+ print("\n" + "-" * 60)
2306
+ print("💰 Cost Savings:")
2307
+ print(f" Actual cost: ${actual_cost:.4f}")
2308
+ print(f" Premium cost: ${premium_cost:.4f} (if all PREMIUM)")
2309
+ print(f" Savings: ${savings:.4f} ({savings_pct:.1f}%)")
2310
+ print("=" * 60 + "\n")
2311
+
2312
+ # Display workflow result
2313
+ if result.success:
2314
+ if output_content:
2315
+ print(f"\n{output_content}\n")
2260
2316
  else:
2261
- # Check metadata for error
2262
- metadata = getattr(result, "metadata", {})
2263
- error_msg = metadata.get("error") if isinstance(metadata, dict) else None
2264
- error_msg = error_msg or "Unknown error"
2265
- print(f"\n✗ Workflow failed: {error_msg}\n")
2317
+ print("\n✓ Workflow completed successfully.\n")
2318
+ else:
2319
+ # Extract error from various result types
2320
+ error_msg = getattr(result, "error", None)
2321
+ if not error_msg:
2322
+ # Check for blockers (CodeReviewPipelineResult)
2323
+ blockers = getattr(result, "blockers", [])
2324
+ if blockers:
2325
+ error_msg = "; ".join(blockers)
2326
+ else:
2327
+ # Check metadata for error
2328
+ metadata = getattr(result, "metadata", {})
2329
+ error_msg = (
2330
+ metadata.get("error") if isinstance(metadata, dict) else None
2331
+ )
2332
+ error_msg = error_msg or "Unknown error"
2333
+ print(f"\n✗ Workflow failed: {error_msg}\n")
2266
2334
 
2267
2335
  except KeyError as e:
2268
2336
  print(f"Error: {e}")
@@ -2436,6 +2504,38 @@ def _cmd_telemetry_export(args):
2436
2504
  return cmd_telemetry_export(args)
2437
2505
 
2438
2506
 
2507
+ def _cmd_tier1_status(args):
2508
+ """Wrapper for tier1 status command."""
2509
+ if not TELEMETRY_CLI_AVAILABLE:
2510
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2511
+ return 1
2512
+ return cmd_tier1_status(args)
2513
+
2514
+
2515
+ def _cmd_task_routing_report(args):
2516
+ """Wrapper for task routing report command."""
2517
+ if not TELEMETRY_CLI_AVAILABLE:
2518
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2519
+ return 1
2520
+ return cmd_task_routing_report(args)
2521
+
2522
+
2523
+ def _cmd_test_status(args):
2524
+ """Wrapper for test status command."""
2525
+ if not TELEMETRY_CLI_AVAILABLE:
2526
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2527
+ return 1
2528
+ return cmd_test_status(args)
2529
+
2530
+
2531
+ def _cmd_agent_performance(args):
2532
+ """Wrapper for agent performance command."""
2533
+ if not TELEMETRY_CLI_AVAILABLE:
2534
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2535
+ return 1
2536
+ return cmd_agent_performance(args)
2537
+
2538
+
2439
2539
  def main():
2440
2540
  """Main CLI entry point"""
2441
2541
  # Configure Windows-compatible asyncio event loop policy
@@ -2929,6 +3029,60 @@ def main():
2929
3029
  )
2930
3030
  parser_telemetry_export.set_defaults(func=lambda args: _cmd_telemetry_export(args))
2931
3031
 
3032
+ # Tier 1 automation monitoring commands
3033
+
3034
+ # tier1 command - comprehensive status
3035
+ parser_tier1 = subparsers.add_parser(
3036
+ "tier1",
3037
+ help="Show Tier 1 automation status (tasks, tests, coverage, agents)",
3038
+ )
3039
+ parser_tier1.add_argument(
3040
+ "--hours",
3041
+ type=int,
3042
+ default=24,
3043
+ help="Hours to analyze (default: 24)",
3044
+ )
3045
+ parser_tier1.set_defaults(func=lambda args: _cmd_tier1_status(args))
3046
+
3047
+ # tasks command - task routing report
3048
+ parser_tasks = subparsers.add_parser(
3049
+ "tasks",
3050
+ help="Show task routing report",
3051
+ )
3052
+ parser_tasks.add_argument(
3053
+ "--hours",
3054
+ type=int,
3055
+ default=24,
3056
+ help="Hours to analyze (default: 24)",
3057
+ )
3058
+ parser_tasks.set_defaults(func=lambda args: _cmd_task_routing_report(args))
3059
+
3060
+ # tests command - test execution status
3061
+ parser_tests = subparsers.add_parser(
3062
+ "tests",
3063
+ help="Show test execution status",
3064
+ )
3065
+ parser_tests.add_argument(
3066
+ "--hours",
3067
+ type=int,
3068
+ default=24,
3069
+ help="Hours to analyze (default: 24)",
3070
+ )
3071
+ parser_tests.set_defaults(func=lambda args: _cmd_test_status(args))
3072
+
3073
+ # agents command - agent performance
3074
+ parser_agents = subparsers.add_parser(
3075
+ "agents",
3076
+ help="Show agent performance metrics",
3077
+ )
3078
+ parser_agents.add_argument(
3079
+ "--hours",
3080
+ type=int,
3081
+ default=168,
3082
+ help="Hours to analyze (default: 168 / 7 days)",
3083
+ )
3084
+ parser_agents.set_defaults(func=lambda args: _cmd_agent_performance(args))
3085
+
2932
3086
  # New command (project scaffolding)
2933
3087
  parser_new = subparsers.add_parser("new", help="Create a new project from a template")
2934
3088
  parser_new.add_argument(
@@ -3018,6 +3172,11 @@ def main():
3018
3172
  help="Force overwrite existing config file",
3019
3173
  )
3020
3174
  parser_workflow.add_argument("--json", action="store_true", help="Output as JSON")
3175
+ parser_workflow.add_argument(
3176
+ "--use-recommended-tier",
3177
+ action="store_true",
3178
+ help="Enable intelligent tier fallback: start with CHEAP tier and automatically upgrade if quality gates fail",
3179
+ )
3021
3180
  parser_workflow.add_argument(
3022
3181
  "--write-tests",
3023
3182
  action="store_true",
@@ -3028,6 +3187,12 @@ def main():
3028
3187
  default="tests/generated",
3029
3188
  help="(test-gen workflow) Output directory for generated tests",
3030
3189
  )
3190
+ parser_workflow.add_argument(
3191
+ "--health-score-threshold",
3192
+ type=int,
3193
+ default=95,
3194
+ help="(health-check workflow) Minimum health score required (0-100, default: 95 for very strict quality)",
3195
+ )
3031
3196
  parser_workflow.set_defaults(func=cmd_workflow)
3032
3197
 
3033
3198
  # Sync-claude command (sync patterns to Claude Code)
empathy_os/cli_unified.py CHANGED
@@ -462,11 +462,36 @@ def workflow_list():
462
462
  def workflow_run(
463
463
  name: str = typer.Argument(..., help="Workflow name"),
464
464
  path: Path = typer.Option(Path(), "--path", "-p", help="Path to run on"),
465
+ use_recommended_tier: bool = typer.Option(
466
+ False,
467
+ "--use-recommended-tier",
468
+ help="Enable intelligent tier fallback: start with CHEAP tier and automatically upgrade if quality gates fail",
469
+ ),
470
+ health_score_threshold: int = typer.Option(
471
+ 95,
472
+ "--health-score-threshold",
473
+ help="(health-check workflow) Minimum health score required (0-100, default: 95 for very strict quality)",
474
+ ),
465
475
  ):
466
476
  """Run a multi-model workflow."""
467
- subprocess.run(
468
- [sys.executable, "-m", "empathy_os.cli", "workflow", "run", name, str(path)], check=False
469
- )
477
+ cmd = [
478
+ sys.executable,
479
+ "-m",
480
+ "empathy_os.cli",
481
+ "workflow",
482
+ "run",
483
+ name,
484
+ "--input",
485
+ f'{{"path": "{path}"}}',
486
+ ]
487
+
488
+ if use_recommended_tier:
489
+ cmd.append("--use-recommended-tier")
490
+
491
+ if health_score_threshold != 95:
492
+ cmd.extend(["--health-score-threshold", str(health_score_threshold)])
493
+
494
+ subprocess.run(cmd, check=False)
470
495
 
471
496
 
472
497
  @workflow_app.command("create")
@@ -513,6 +538,85 @@ def workflow_recommend(
513
538
  )
514
539
 
515
540
 
541
+ # =============================================================================
542
+ # TELEMETRY SUBCOMMAND GROUP
543
+ # =============================================================================
544
+
545
+ telemetry_app = typer.Typer(help="View and manage local usage telemetry")
546
+ app.add_typer(telemetry_app, name="telemetry")
547
+
548
+
549
+ @telemetry_app.command("show")
550
+ def telemetry_show(
551
+ limit: int = typer.Option(20, "--limit", "-l", help="Number of entries to show"),
552
+ days: int | None = typer.Option(None, "--days", "-d", help="Only show last N days"),
553
+ ):
554
+ """Show recent LLM calls and usage stats."""
555
+ args = [sys.executable, "-m", "empathy_os.cli", "telemetry", "show", "--limit", str(limit)]
556
+ if days:
557
+ args.extend(["--days", str(days)])
558
+ subprocess.run(args, check=False)
559
+
560
+
561
+ @telemetry_app.command("savings")
562
+ def telemetry_savings(
563
+ days: int = typer.Option(30, "--days", "-d", help="Number of days to analyze"),
564
+ ):
565
+ """Calculate cost savings vs baseline (all PREMIUM)."""
566
+ subprocess.run(
567
+ [sys.executable, "-m", "empathy_os.cli", "telemetry", "savings", "--days", str(days)],
568
+ check=False,
569
+ )
570
+
571
+
572
+ @telemetry_app.command("compare")
573
+ def telemetry_compare(
574
+ period1: int = typer.Option(7, "--period1", "-p1", help="First period in days"),
575
+ period2: int = typer.Option(30, "--period2", "-p2", help="Second period in days"),
576
+ ):
577
+ """Compare usage across two time periods."""
578
+ subprocess.run(
579
+ [
580
+ sys.executable,
581
+ "-m",
582
+ "empathy_os.cli",
583
+ "telemetry",
584
+ "compare",
585
+ "--period1",
586
+ str(period1),
587
+ "--period2",
588
+ str(period2),
589
+ ],
590
+ check=False,
591
+ )
592
+
593
+
594
+ @telemetry_app.command("export")
595
+ def telemetry_export(
596
+ format_type: str = typer.Option("json", "--format", "-f", help="Export format (json, csv)"),
597
+ output: Path | None = typer.Option(None, "--output", "-o", help="Output file path"),
598
+ days: int | None = typer.Option(None, "--days", "-d", help="Only export last N days"),
599
+ ):
600
+ """Export telemetry data to JSON or CSV."""
601
+ args = [sys.executable, "-m", "empathy_os.cli", "telemetry", "export", "--format", format_type]
602
+ if output:
603
+ args.extend(["--output", str(output)])
604
+ if days:
605
+ args.extend(["--days", str(days)])
606
+ subprocess.run(args, check=False)
607
+
608
+
609
+ @telemetry_app.command("reset")
610
+ def telemetry_reset(
611
+ confirm: bool = typer.Option(False, "--confirm", help="Confirm deletion"),
612
+ ):
613
+ """Clear all telemetry data (use with caution)."""
614
+ args = [sys.executable, "-m", "empathy_os.cli", "telemetry", "reset"]
615
+ if confirm:
616
+ args.append("--confirm")
617
+ subprocess.run(args, check=False)
618
+
619
+
516
620
  # =============================================================================
517
621
  # TIER RECOMMENDATION SUBCOMMAND GROUP
518
622
  # =============================================================================
@@ -669,7 +773,12 @@ def cheatsheet():
669
773
  empathy wizard list Show available wizards
670
774
  empathy wizard run <name> Execute a wizard
671
775
  empathy wizard create <name> -d <domain> Create wizard (12x faster)
672
- empathy wizard list-patterns List available patterns""",
776
+ empathy wizard list-patterns List available patterns
777
+
778
+ [bold]Usage Telemetry[/bold]
779
+ empathy telemetry show View recent LLM calls & costs
780
+ empathy telemetry savings Calculate cost savings (tier routing)
781
+ empathy telemetry export Export usage data (JSON/CSV)""",
673
782
  title="[bold blue]Empathy Framework Cheatsheet[/bold blue]",
674
783
  ),
675
784
  )
empathy_os/config.py CHANGED
@@ -14,7 +14,10 @@ import json
14
14
  import os
15
15
  from dataclasses import asdict, dataclass, field
16
16
  from pathlib import Path
17
- from typing import Any
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ if TYPE_CHECKING:
20
+ from empathy_os.workflows.config import ModelConfig
18
21
 
19
22
  try:
20
23
  import yaml
@@ -23,8 +26,6 @@ try:
23
26
  except ImportError:
24
27
  YAML_AVAILABLE = False
25
28
 
26
- from empathy_os.workflows.config import ModelConfig
27
-
28
29
 
29
30
  def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
30
31
  """Validate file path to prevent path traversal and arbitrary writes.
@@ -120,7 +121,7 @@ class EmpathyConfig:
120
121
  metadata: dict[str, Any] = field(default_factory=dict)
121
122
 
122
123
  # Model settings
123
- models: list[ModelConfig] = field(default_factory=list)
124
+ models: list["ModelConfig"] = field(default_factory=list)
124
125
  default_model: str | None = None
125
126
  log_path: str | None = None
126
127
  max_threads: int = 4
@@ -181,6 +182,8 @@ class EmpathyConfig:
181
182
 
182
183
  # Handle nested ModelConfig objects
183
184
  if filtered_data.get("models"):
185
+ from empathy_os.workflows.config import ModelConfig
186
+
184
187
  filtered_data["models"] = [ModelConfig(**m) for m in filtered_data["models"]]
185
188
 
186
189
  return cls(**filtered_data)
@@ -7,6 +7,7 @@ Licensed under Fair Source 0.9
7
7
  """
8
8
 
9
9
  import logging
10
+ from collections.abc import Callable
10
11
 
11
12
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
12
13
 
@@ -45,7 +46,7 @@ class HotReloadIntegration:
45
46
  def __init__(
46
47
  self,
47
48
  app: FastAPI,
48
- register_callback: callable,
49
+ register_callback: Callable[[str, type], bool],
49
50
  ):
50
51
  """Initialize hot-reload integration.
51
52
 
@@ -19,16 +19,16 @@ logger = logging.getLogger(__name__)
19
19
  class WizardFileHandler(FileSystemEventHandler):
20
20
  """Handles file system events for wizard files."""
21
21
 
22
- def __init__(self, reload_callback: Callable[[str], None]):
22
+ def __init__(self, reload_callback: Callable[[str, str], None]):
23
23
  """Initialize handler.
24
24
 
25
25
  Args:
26
- reload_callback: Function to call when wizard file changes
26
+ reload_callback: Function to call when wizard file changes (wizard_id, file_path)
27
27
 
28
28
  """
29
29
  super().__init__()
30
30
  self.reload_callback = reload_callback
31
- self._processing = set() # Prevent duplicate events
31
+ self._processing: set[str] = set() # Prevent duplicate events
32
32
 
33
33
  def on_modified(self, event: FileSystemEvent) -> None:
34
34
  """Handle file modification events.
@@ -40,7 +40,11 @@ class WizardFileHandler(FileSystemEventHandler):
40
40
  if event.is_directory:
41
41
  return
42
42
 
43
- file_path = event.src_path
43
+ # Convert file_path to str if it's bytes
44
+ file_path_raw = event.src_path
45
+ file_path = (
46
+ file_path_raw.decode("utf-8") if isinstance(file_path_raw, bytes) else file_path_raw
47
+ )
44
48
 
45
49
  # Only process Python files
46
50
  if not file_path.endswith(".py"):
@@ -8,6 +8,7 @@ Licensed under Fair Source 0.9
8
8
 
9
9
  import asyncio
10
10
  import logging
11
+ from collections.abc import Callable
11
12
  from typing import Any
12
13
 
13
14
  from fastapi import WebSocket
@@ -141,7 +142,7 @@ def get_notification_manager() -> ReloadNotificationManager:
141
142
  return _notification_manager
142
143
 
143
144
 
144
- def create_notification_callback() -> callable:
145
+ def create_notification_callback() -> Callable[[dict[str, Any]], None]:
145
146
  """Create a callback function for the wizard reloader.
146
147
 
147
148
  Returns: