empathy-framework 4.9.1__py3-none-any.whl → 5.0.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.
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/METADATA +1 -1
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/RECORD +47 -26
- empathy_os/__init__.py +1 -1
- empathy_os/cache/hash_only.py +6 -3
- empathy_os/cache/hybrid.py +6 -3
- empathy_os/cli_legacy.py +27 -1
- empathy_os/cli_minimal.py +512 -15
- empathy_os/cli_router.py +145 -113
- empathy_os/cli_unified.py +25 -0
- empathy_os/dashboard/__init__.py +42 -0
- empathy_os/dashboard/app.py +512 -0
- empathy_os/dashboard/simple_server.py +403 -0
- empathy_os/dashboard/standalone_server.py +536 -0
- empathy_os/memory/__init__.py +19 -5
- empathy_os/memory/short_term.py +4 -70
- empathy_os/memory/types.py +2 -2
- empathy_os/models/__init__.py +3 -0
- empathy_os/models/adaptive_routing.py +437 -0
- empathy_os/models/registry.py +4 -4
- empathy_os/socratic/ab_testing.py +1 -1
- empathy_os/telemetry/__init__.py +29 -1
- empathy_os/telemetry/agent_coordination.py +478 -0
- empathy_os/telemetry/agent_tracking.py +350 -0
- empathy_os/telemetry/approval_gates.py +563 -0
- empathy_os/telemetry/event_streaming.py +405 -0
- empathy_os/telemetry/feedback_loop.py +557 -0
- empathy_os/vscode_bridge 2.py +173 -0
- empathy_os/workflows/__init__.py +4 -4
- empathy_os/workflows/base.py +495 -43
- empathy_os/workflows/history.py +3 -5
- empathy_os/workflows/output.py +410 -0
- empathy_os/workflows/progress.py +324 -22
- empathy_os/workflows/progressive/README 2.md +454 -0
- empathy_os/workflows/progressive/__init__ 2.py +92 -0
- empathy_os/workflows/progressive/cli 2.py +242 -0
- empathy_os/workflows/progressive/core 2.py +488 -0
- empathy_os/workflows/progressive/orchestrator 2.py +701 -0
- empathy_os/workflows/progressive/reports 2.py +528 -0
- empathy_os/workflows/progressive/telemetry 2.py +280 -0
- empathy_os/workflows/progressive/test_gen 2.py +514 -0
- empathy_os/workflows/progressive/workflow 2.py +628 -0
- empathy_os/workflows/routing.py +5 -0
- empathy_os/workflows/security_audit.py +189 -0
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/WHEEL +0 -0
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/top_level.txt +0 -0
empathy_os/workflows/progress.py
CHANGED
|
@@ -11,12 +11,35 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import json
|
|
14
|
-
|
|
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
|
|
365
|
-
#
|
|
366
|
-
|
|
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
|
-
"""
|
|
413
|
+
"""Console-based progress reporter optimized for IDE environments.
|
|
391
414
|
|
|
392
|
-
|
|
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
|
-
|
|
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
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
if self.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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()
|