empathy-framework 4.9.0__py3-none-any.whl → 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/METADATA +64 -25
  2. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/RECORD +47 -26
  3. empathy_os/__init__.py +2 -2
  4. empathy_os/cache/hash_only.py +6 -3
  5. empathy_os/cache/hybrid.py +6 -3
  6. empathy_os/cli_legacy.py +27 -1
  7. empathy_os/cli_minimal.py +512 -15
  8. empathy_os/cli_router.py +145 -113
  9. empathy_os/cli_unified.py +25 -0
  10. empathy_os/dashboard/__init__.py +42 -0
  11. empathy_os/dashboard/app.py +512 -0
  12. empathy_os/dashboard/simple_server.py +403 -0
  13. empathy_os/dashboard/standalone_server.py +536 -0
  14. empathy_os/memory/__init__.py +19 -5
  15. empathy_os/memory/short_term.py +4 -70
  16. empathy_os/memory/types.py +2 -2
  17. empathy_os/models/__init__.py +3 -0
  18. empathy_os/models/adaptive_routing.py +437 -0
  19. empathy_os/models/registry.py +4 -4
  20. empathy_os/socratic/ab_testing.py +1 -1
  21. empathy_os/telemetry/__init__.py +29 -1
  22. empathy_os/telemetry/agent_coordination.py +478 -0
  23. empathy_os/telemetry/agent_tracking.py +350 -0
  24. empathy_os/telemetry/approval_gates.py +563 -0
  25. empathy_os/telemetry/event_streaming.py +405 -0
  26. empathy_os/telemetry/feedback_loop.py +557 -0
  27. empathy_os/vscode_bridge 2.py +173 -0
  28. empathy_os/workflows/__init__.py +4 -4
  29. empathy_os/workflows/base.py +495 -43
  30. empathy_os/workflows/history.py +3 -5
  31. empathy_os/workflows/output.py +410 -0
  32. empathy_os/workflows/progress.py +324 -22
  33. empathy_os/workflows/progressive/README 2.md +454 -0
  34. empathy_os/workflows/progressive/__init__ 2.py +92 -0
  35. empathy_os/workflows/progressive/cli 2.py +242 -0
  36. empathy_os/workflows/progressive/core 2.py +488 -0
  37. empathy_os/workflows/progressive/orchestrator 2.py +701 -0
  38. empathy_os/workflows/progressive/reports 2.py +528 -0
  39. empathy_os/workflows/progressive/telemetry 2.py +280 -0
  40. empathy_os/workflows/progressive/test_gen 2.py +514 -0
  41. empathy_os/workflows/progressive/workflow 2.py +628 -0
  42. empathy_os/workflows/routing.py +5 -0
  43. empathy_os/workflows/security_audit.py +189 -0
  44. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/WHEEL +0 -0
  45. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/entry_points.txt +0 -0
  46. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/licenses/LICENSE +0 -0
  47. {empathy_framework-4.9.0.dist-info → empathy_framework-5.0.0.dist-info}/top_level.txt +0 -0
@@ -11,12 +11,35 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  import json
14
- from collections.abc import Callable, Coroutine
14
+ import logging
15
+ import sys
16
+ import uuid
17
+ from collections.abc import Callable, Coroutine, Generator
18
+ from contextlib import contextmanager
15
19
  from dataclasses import dataclass, field
16
20
  from datetime import datetime
17
21
  from enum import Enum
18
22
  from typing import Any, Protocol
19
23
 
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Rich imports with fallback
27
+ try:
28
+ from rich.console import Console, Group
29
+ from rich.live import Live
30
+ from rich.panel import Panel
31
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn
32
+ from rich.table import Table
33
+ from rich.text import Text
34
+
35
+ RICH_AVAILABLE = True
36
+ except ImportError:
37
+ RICH_AVAILABLE = False
38
+ Console = None # type: ignore
39
+ Live = None # type: ignore
40
+ Panel = None # type: ignore
41
+ Progress = None # type: ignore
42
+
20
43
 
21
44
  class ProgressStatus(Enum):
22
45
  """Status of a workflow or stage."""
@@ -361,9 +384,9 @@ class ProgressTracker:
361
384
  for callback in self._callbacks:
362
385
  try:
363
386
  callback(update)
364
- except Exception as e:
365
- # Log but don't fail on callback errors
366
- print(f"Progress callback error: {e}")
387
+ except Exception: # noqa: BLE001
388
+ # INTENTIONAL: Callbacks are optional - never fail workflow on callback error
389
+ logger.warning("Progress callback error", exc_info=True)
367
390
 
368
391
  # Call async callbacks
369
392
  for async_callback in self._async_callbacks:
@@ -387,39 +410,111 @@ class ProgressReporter(Protocol):
387
410
 
388
411
 
389
412
  class ConsoleProgressReporter:
390
- """Simple console-based progress reporter for CLI usage."""
413
+ """Console-based progress reporter optimized for IDE environments.
391
414
 
392
- def __init__(self, verbose: bool = False):
415
+ Provides clear, readable progress output that works reliably in:
416
+ - VSCode integrated terminal
417
+ - VSCode output panel
418
+ - IDE debug consoles
419
+ - Standard terminals
420
+
421
+ Uses Unicode symbols that render correctly in most environments.
422
+ """
423
+
424
+ def __init__(self, verbose: bool = False, show_tokens: bool = False):
425
+ """Initialize console progress reporter.
426
+
427
+ Args:
428
+ verbose: Show additional details (fallback info, errors)
429
+ show_tokens: Include token counts in output
430
+ """
393
431
  self.verbose = verbose
432
+ self.show_tokens = show_tokens
433
+ self._start_time: datetime | None = None
434
+ self._stage_times: dict[str, int] = {}
394
435
 
395
436
  def report(self, update: ProgressUpdate) -> None:
396
- """Print progress to console."""
397
- percent = f"{update.percent_complete:.0f}%"
437
+ """Print progress to console.
438
+
439
+ Args:
440
+ update: Progress update from the tracker
441
+ """
442
+ # Track start time for elapsed calculation
443
+ if self._start_time is None:
444
+ self._start_time = datetime.now()
445
+
446
+ percent = f"{update.percent_complete:3.0f}%"
398
447
  cost = f"${update.cost_so_far:.4f}"
448
+
449
+ # Status icons that work in most environments
399
450
  status_icon = {
400
451
  ProgressStatus.PENDING: "○",
401
- ProgressStatus.RUNNING: "",
402
- ProgressStatus.COMPLETED: "",
452
+ ProgressStatus.RUNNING: "",
453
+ ProgressStatus.COMPLETED: "",
403
454
  ProgressStatus.FAILED: "✗",
404
- ProgressStatus.SKIPPED: "",
405
- ProgressStatus.FALLBACK: "",
455
+ ProgressStatus.SKIPPED: "",
456
+ ProgressStatus.FALLBACK: "",
406
457
  ProgressStatus.RETRYING: "↻",
407
458
  }.get(update.status, "?")
408
459
 
409
460
  # Get current tier from running stage
410
461
  tier_info = ""
462
+ model_info = ""
411
463
  if update.current_stage and update.stages:
412
464
  for stage in update.stages:
413
- if stage.name == update.current_stage and stage.status == ProgressStatus.RUNNING:
414
- tier_info = f" [{stage.tier.upper()}]"
465
+ if stage.name == update.current_stage:
466
+ if stage.status == ProgressStatus.RUNNING:
467
+ tier_info = f" [{stage.tier.upper()}]"
468
+ if stage.model:
469
+ model_info = f" ({stage.model})"
470
+ # Track stage duration
471
+ if stage.duration_ms > 0:
472
+ self._stage_times[stage.name] = stage.duration_ms
415
473
  break
416
474
 
417
- print(f"[{percent}] {status_icon} {update.message}{tier_info} ({cost})")
418
-
419
- if self.verbose and update.fallback_info:
420
- print(f" Fallback: {update.fallback_info}")
421
- if self.verbose and update.error:
422
- print(f" Error: {update.error}")
475
+ # Build output line
476
+ elapsed = ""
477
+ if self._start_time:
478
+ elapsed_sec = (datetime.now() - self._start_time).total_seconds()
479
+ if elapsed_sec >= 1:
480
+ elapsed = f" [{elapsed_sec:.1f}s]"
481
+
482
+ tokens_str = ""
483
+ if self.show_tokens and update.tokens_so_far > 0:
484
+ tokens_str = f" | {update.tokens_so_far:,} tokens"
485
+
486
+ # Format: [100%] ✓ Completed optimize [PREMIUM] ($0.0279) [12.3s]
487
+ output = f"[{percent}] {status_icon} {update.message}{tier_info} ({cost}{tokens_str}){elapsed}"
488
+ print(output)
489
+
490
+ # Verbose output
491
+ if self.verbose:
492
+ if update.fallback_info:
493
+ print(f" ↳ Fallback: {update.fallback_info}")
494
+ if update.error:
495
+ print(f" ↳ Error: {update.error}")
496
+
497
+ # Print summary only on final workflow completion (not stage completion)
498
+ if update.status == ProgressStatus.COMPLETED and "workflow" in update.message.lower():
499
+ self._print_summary(update)
500
+
501
+ def _print_summary(self, update: ProgressUpdate) -> None:
502
+ """Print workflow completion summary."""
503
+ if not self._stage_times:
504
+ return
505
+
506
+ print("")
507
+ print("─" * 50)
508
+ print("Stage Summary:")
509
+ for stage in update.stages:
510
+ if stage.status == ProgressStatus.COMPLETED:
511
+ duration_ms = stage.duration_ms or self._stage_times.get(stage.name, 0)
512
+ duration_str = f"{duration_ms}ms" if duration_ms < 1000 else f"{duration_ms/1000:.1f}s"
513
+ cost_str = f"${stage.cost:.4f}" if stage.cost > 0 else "—"
514
+ print(f" {stage.name}: {duration_str} | {cost_str}")
515
+ elif stage.status == ProgressStatus.SKIPPED:
516
+ print(f" {stage.name}: skipped")
517
+ print("─" * 50)
423
518
 
424
519
  async def report_async(self, update: ProgressUpdate) -> None:
425
520
  """Async version just calls sync."""
@@ -463,8 +558,6 @@ def create_progress_tracker(
463
558
  Configured ProgressTracker instance
464
559
 
465
560
  """
466
- import uuid
467
-
468
561
  tracker = ProgressTracker(
469
562
  workflow_name=workflow_name,
470
563
  workflow_id=uuid.uuid4().hex[:12],
@@ -475,3 +568,212 @@ def create_progress_tracker(
475
568
  tracker.add_callback(reporter.report)
476
569
 
477
570
  return tracker
571
+
572
+
573
+ class RichProgressReporter:
574
+ """Rich-based live progress display with spinner, progress bar, and metrics.
575
+
576
+ Provides real-time visual feedback during workflow execution:
577
+ - Progress bar showing stage completion (1/3, 2/3, etc.)
578
+ - Spinner during active LLM API calls
579
+ - Real-time cost and token display
580
+ - In-place updates (no terminal scrolling)
581
+
582
+ Requires Rich library. Falls back gracefully if unavailable.
583
+ """
584
+
585
+ def __init__(self, workflow_name: str, stage_names: list[str]) -> None:
586
+ """Initialize the Rich progress reporter.
587
+
588
+ Args:
589
+ workflow_name: Name of the workflow for display
590
+ stage_names: List of stage names for progress tracking
591
+ """
592
+ if not RICH_AVAILABLE:
593
+ raise RuntimeError("Rich library required for RichProgressReporter")
594
+
595
+ self.workflow_name = workflow_name
596
+ self.stage_names = stage_names
597
+ self.console = Console()
598
+ self._live: Live | None = None
599
+ self._progress: Progress | None = None
600
+ self._task_id: TaskID | None = None
601
+ self._current_stage = ""
602
+ self._cost = 0.0
603
+ self._tokens = 0
604
+ self._status = ProgressStatus.PENDING
605
+
606
+ def start(self) -> None:
607
+ """Start the live progress display."""
608
+ if not RICH_AVAILABLE or Progress is None or Live is None:
609
+ return
610
+
611
+ self._progress = Progress(
612
+ SpinnerColumn(),
613
+ TextColumn("[bold blue]{task.description}"),
614
+ BarColumn(bar_width=30),
615
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
616
+ TextColumn("({task.completed}/{task.total})"),
617
+ console=self.console,
618
+ transient=False,
619
+ )
620
+
621
+ self._task_id = self._progress.add_task(
622
+ self.workflow_name,
623
+ total=len(self.stage_names),
624
+ )
625
+
626
+ self._live = Live(
627
+ self._create_display(),
628
+ console=self.console,
629
+ refresh_per_second=4,
630
+ transient=False,
631
+ )
632
+ self._live.start()
633
+
634
+ def stop(self) -> None:
635
+ """Stop the live progress display."""
636
+ if self._live:
637
+ self._live.stop()
638
+ self._live = None
639
+
640
+ def report(self, update: ProgressUpdate) -> None:
641
+ """Handle a progress update.
642
+
643
+ Args:
644
+ update: Progress update from the tracker
645
+ """
646
+ self._current_stage = update.current_stage
647
+ self._cost = update.cost_so_far
648
+ self._tokens = update.tokens_so_far
649
+ self._status = update.status
650
+
651
+ # Update progress bar
652
+ if self._progress is not None and self._task_id is not None:
653
+ completed = sum(
654
+ 1 for s in update.stages if s.status == ProgressStatus.COMPLETED
655
+ )
656
+ self._progress.update(
657
+ self._task_id,
658
+ completed=completed,
659
+ description=f"{self.workflow_name}: {update.current_stage}",
660
+ )
661
+
662
+ # Refresh display
663
+ if self._live:
664
+ self._live.update(self._create_display())
665
+
666
+ async def report_async(self, update: ProgressUpdate) -> None:
667
+ """Async version of report."""
668
+ self.report(update)
669
+
670
+ def _create_display(self) -> Panel:
671
+ """Create the Rich display panel.
672
+
673
+ Returns:
674
+ Rich Panel containing progress information
675
+ """
676
+ if not RICH_AVAILABLE or Panel is None or Table is None:
677
+ raise RuntimeError("Rich not available")
678
+
679
+ # Build metrics table
680
+ metrics = Table(show_header=False, box=None, padding=(0, 2))
681
+ metrics.add_column("Label", style="dim")
682
+ metrics.add_column("Value", style="bold")
683
+
684
+ metrics.add_row("Cost:", f"${self._cost:.4f}")
685
+ metrics.add_row("Tokens:", f"{self._tokens:,}")
686
+ metrics.add_row("Stage:", self._current_stage or "Starting...")
687
+
688
+ # Status indicator
689
+ status_style = {
690
+ ProgressStatus.PENDING: "dim",
691
+ ProgressStatus.RUNNING: "blue",
692
+ ProgressStatus.COMPLETED: "green",
693
+ ProgressStatus.FAILED: "red",
694
+ ProgressStatus.FALLBACK: "yellow",
695
+ ProgressStatus.RETRYING: "yellow",
696
+ }.get(self._status, "white")
697
+
698
+ status_text = Text(self._status.value.upper(), style=status_style)
699
+
700
+ # Combine into panel
701
+ if self._progress is not None:
702
+ content = Group(self._progress, metrics)
703
+ else:
704
+ content = metrics
705
+
706
+ return Panel(
707
+ content,
708
+ title=f"[bold]{self.workflow_name}[/bold]",
709
+ subtitle=status_text,
710
+ border_style=status_style,
711
+ )
712
+
713
+ def __enter__(self) -> RichProgressReporter:
714
+ """Context manager entry."""
715
+ self.start()
716
+ return self
717
+
718
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
719
+ """Context manager exit."""
720
+ self.stop()
721
+
722
+
723
+ @contextmanager
724
+ def live_progress(
725
+ workflow_name: str,
726
+ stage_names: list[str],
727
+ console: Console | None = None,
728
+ ) -> Generator[tuple[ProgressTracker, RichProgressReporter | None], None, None]:
729
+ """Context manager for live progress display during workflow execution.
730
+
731
+ Provides a ProgressTracker with optional Rich-based live display.
732
+ Falls back gracefully when Rich is unavailable or output is not a TTY.
733
+
734
+ Args:
735
+ workflow_name: Name of the workflow
736
+ stage_names: List of stage names in order
737
+ console: Optional Rich Console (creates new one if not provided)
738
+
739
+ Yields:
740
+ Tuple of (ProgressTracker, RichProgressReporter or None)
741
+
742
+ Example:
743
+ with live_progress("Code Review", ["analyze", "review", "summarize"]) as (tracker, _):
744
+ tracker.start_workflow()
745
+ for stage in stages:
746
+ tracker.start_stage(stage)
747
+ # ... do work ...
748
+ tracker.complete_stage(stage, cost=0.01, tokens_in=100, tokens_out=50)
749
+ tracker.complete_workflow()
750
+ """
751
+ tracker = ProgressTracker(
752
+ workflow_name=workflow_name,
753
+ workflow_id=uuid.uuid4().hex[:12],
754
+ stage_names=stage_names,
755
+ )
756
+
757
+ reporter: RichProgressReporter | None = None
758
+
759
+ # Use Rich if available and output is a TTY
760
+ if RICH_AVAILABLE and sys.stdout.isatty():
761
+ try:
762
+ reporter = RichProgressReporter(workflow_name, stage_names)
763
+ tracker.add_callback(reporter.report)
764
+ reporter.start()
765
+ except Exception: # noqa: BLE001
766
+ # INTENTIONAL: Rich display is optional - fall back to console output
767
+ reporter = None
768
+ simple_reporter = ConsoleProgressReporter(verbose=False)
769
+ tracker.add_callback(simple_reporter.report)
770
+ else:
771
+ # No Rich or not a TTY - use simple console reporter
772
+ simple_reporter = ConsoleProgressReporter(verbose=False)
773
+ tracker.add_callback(simple_reporter.report)
774
+
775
+ try:
776
+ yield tracker, reporter
777
+ finally:
778
+ if reporter:
779
+ reporter.stop()