empathy-framework 4.8.0__py3-none-any.whl → 4.9.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-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/METADATA +1 -1
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/RECORD +27 -38
- empathy_os/cache/hash_only.py +3 -6
- empathy_os/cache/hybrid.py +3 -6
- empathy_os/cli_legacy.py +1 -27
- empathy_os/cli_unified.py +0 -25
- empathy_os/memory/__init__.py +5 -19
- empathy_os/memory/short_term.py +132 -10
- empathy_os/memory/types.py +4 -0
- empathy_os/models/registry.py +4 -4
- empathy_os/project_index/scanner.py +3 -2
- empathy_os/socratic/ab_testing.py +1 -1
- empathy_os/workflow_commands.py +9 -9
- empathy_os/workflows/__init__.py +4 -4
- empathy_os/workflows/base.py +8 -54
- empathy_os/workflows/bug_predict.py +2 -2
- empathy_os/workflows/history.py +5 -3
- empathy_os/workflows/perf_audit.py +4 -4
- empathy_os/workflows/progress.py +22 -324
- empathy_os/workflows/routing.py +0 -5
- empathy_os/workflows/security_audit.py +0 -189
- empathy_os/workflows/security_audit_phase3.py +26 -2
- empathy_os/workflows/test_gen.py +7 -7
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/output.py +0 -410
- empathy_os/workflows/progressive/README 2.md +0 -454
- 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-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/WHEEL +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/top_level.txt +0 -0
empathy_os/workflow_commands.py
CHANGED
|
@@ -137,7 +137,7 @@ def morning_workflow(
|
|
|
137
137
|
print("-" * 40)
|
|
138
138
|
|
|
139
139
|
total_bugs = len(patterns.get("debugging", []))
|
|
140
|
-
resolved_bugs =
|
|
140
|
+
resolved_bugs = sum(1 for p in patterns.get("debugging", []) if p.get("status") == "resolved")
|
|
141
141
|
security_decisions = len(patterns.get("security", []))
|
|
142
142
|
|
|
143
143
|
print(f" Bug patterns: {total_bugs} ({resolved_bugs} resolved)")
|
|
@@ -207,14 +207,14 @@ def morning_workflow(
|
|
|
207
207
|
checks_passed += 1
|
|
208
208
|
print(" Lint: OK")
|
|
209
209
|
else:
|
|
210
|
-
issues =
|
|
210
|
+
issues = sum(1 for line in output.split("\n") if line.strip())
|
|
211
211
|
print(f" Lint: {issues} issues")
|
|
212
212
|
|
|
213
213
|
# Check for uncommitted changes
|
|
214
214
|
checks_total += 1
|
|
215
215
|
success, output = _run_command(["git", "status", "--porcelain"])
|
|
216
216
|
if success:
|
|
217
|
-
changes =
|
|
217
|
+
changes = sum(1 for line in output.split("\n") if line.strip())
|
|
218
218
|
if changes == 0:
|
|
219
219
|
checks_passed += 1
|
|
220
220
|
print(" Git: Clean")
|
|
@@ -312,7 +312,7 @@ def _run_security_only(project_root: str = ".", verbose: bool = False) -> int:
|
|
|
312
312
|
if not success or not output.strip():
|
|
313
313
|
print(" PASS - No obvious hardcoded secrets")
|
|
314
314
|
else:
|
|
315
|
-
lines =
|
|
315
|
+
lines = sum(1 for line in output.split("\n") if line.strip())
|
|
316
316
|
issues.append(f"Secrets: {lines} potential hardcoded secrets")
|
|
317
317
|
print(f" WARN - {lines} potential hardcoded values found")
|
|
318
318
|
|
|
@@ -322,7 +322,7 @@ def _run_security_only(project_root: str = ".", verbose: bool = False) -> int:
|
|
|
322
322
|
if not output.strip():
|
|
323
323
|
print(" PASS - No sensitive files tracked")
|
|
324
324
|
else:
|
|
325
|
-
files =
|
|
325
|
+
files = sum(1 for line in output.split("\n") if line.strip())
|
|
326
326
|
issues.append(f"Files: {files} sensitive files in git")
|
|
327
327
|
print(f" WARN - {files} sensitive files tracked in git")
|
|
328
328
|
|
|
@@ -427,10 +427,10 @@ def ship_workflow(
|
|
|
427
427
|
print("4. Checking git status...")
|
|
428
428
|
success, output = _run_command(["git", "status", "--porcelain"])
|
|
429
429
|
if success:
|
|
430
|
-
staged =
|
|
431
|
-
|
|
430
|
+
staged = sum(
|
|
431
|
+
1 for line in output.split("\n") if line.startswith(("A ", "M ", "D ", "R "))
|
|
432
432
|
)
|
|
433
|
-
unstaged =
|
|
433
|
+
unstaged = sum(1 for line in output.split("\n") if line.startswith((" M", " D", "??")))
|
|
434
434
|
if staged > 0:
|
|
435
435
|
print(f" INFO - {staged} staged, {unstaged} unstaged")
|
|
436
436
|
elif unstaged > 0:
|
|
@@ -523,7 +523,7 @@ def fix_all_workflow(project_root: str = ".", dry_run: bool = False, verbose: bo
|
|
|
523
523
|
print(f" Fixed {fixed} issues")
|
|
524
524
|
else:
|
|
525
525
|
# Some issues couldn't be auto-fixed
|
|
526
|
-
unfixable =
|
|
526
|
+
unfixable = sum(1 for line in output.split("\n") if "error" in line.lower())
|
|
527
527
|
print(f" {unfixable} issues require manual fix")
|
|
528
528
|
if verbose:
|
|
529
529
|
print(output)
|
empathy_os/workflows/__init__.py
CHANGED
|
@@ -80,11 +80,9 @@ from .base import (
|
|
|
80
80
|
get_workflow_stats,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
# Builder pattern for workflow construction
|
|
84
|
-
from .builder import WorkflowBuilder, workflow_builder
|
|
85
|
-
|
|
86
83
|
# Config is small and frequently needed
|
|
87
84
|
from .config import DEFAULT_MODELS, ModelConfig, WorkflowConfig, create_example_config, get_model
|
|
85
|
+
from .step_config import WorkflowStepConfig, steps_from_tier_map, validate_step_config
|
|
88
86
|
|
|
89
87
|
# Routing strategies (small, frequently needed for builder pattern)
|
|
90
88
|
from .routing import (
|
|
@@ -94,7 +92,9 @@ from .routing import (
|
|
|
94
92
|
RoutingContext,
|
|
95
93
|
TierRoutingStrategy,
|
|
96
94
|
)
|
|
97
|
-
|
|
95
|
+
|
|
96
|
+
# Builder pattern for workflow construction
|
|
97
|
+
from .builder import WorkflowBuilder, workflow_builder
|
|
98
98
|
|
|
99
99
|
# Lazy import mapping for workflow classes
|
|
100
100
|
_LAZY_WORKFLOW_IMPORTS: dict[str, tuple[str, str]] = {
|
empathy_os/workflows/base.py
CHANGED
|
@@ -17,7 +17,6 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
|
-
import sys
|
|
21
20
|
import time
|
|
22
21
|
import uuid
|
|
23
22
|
from abc import ABC, abstractmethod
|
|
@@ -58,12 +57,7 @@ from empathy_os.models import ModelTier as UnifiedModelTier
|
|
|
58
57
|
from .caching import CachedResponse, CachingMixin
|
|
59
58
|
|
|
60
59
|
# Import progress tracking
|
|
61
|
-
from .progress import
|
|
62
|
-
RICH_AVAILABLE,
|
|
63
|
-
ProgressCallback,
|
|
64
|
-
ProgressTracker,
|
|
65
|
-
RichProgressReporter,
|
|
66
|
-
)
|
|
60
|
+
from .progress import ProgressCallback, ProgressTracker
|
|
67
61
|
from .telemetry_mixin import TelemetryMixin
|
|
68
62
|
|
|
69
63
|
# Import telemetry tracking
|
|
@@ -550,7 +544,6 @@ class BaseWorkflow(CachingMixin, TelemetryMixin, ABC):
|
|
|
550
544
|
enable_tier_tracking: bool = True,
|
|
551
545
|
enable_tier_fallback: bool = False,
|
|
552
546
|
routing_strategy: TierRoutingStrategy | None = None,
|
|
553
|
-
enable_rich_progress: bool = False,
|
|
554
547
|
):
|
|
555
548
|
"""Initialize workflow with optional cost tracker, provider, and config.
|
|
556
549
|
|
|
@@ -576,11 +569,6 @@ class BaseWorkflow(CachingMixin, TelemetryMixin, ABC):
|
|
|
576
569
|
When provided, overrides static tier_map for stage tier decisions.
|
|
577
570
|
Strategies: CostOptimizedRouting, PerformanceOptimizedRouting,
|
|
578
571
|
BalancedRouting, HybridRouting.
|
|
579
|
-
enable_rich_progress: Whether to enable Rich-based live progress display
|
|
580
|
-
(default False). When enabled and output is a TTY, shows live
|
|
581
|
-
progress bars with spinners. Default is False because most users
|
|
582
|
-
run workflows from IDEs (VSCode, etc.) where TTY is not available.
|
|
583
|
-
The console reporter works reliably in all environments.
|
|
584
572
|
|
|
585
573
|
"""
|
|
586
574
|
from .config import WorkflowConfig
|
|
@@ -591,8 +579,6 @@ class BaseWorkflow(CachingMixin, TelemetryMixin, ABC):
|
|
|
591
579
|
# Progress tracking
|
|
592
580
|
self._progress_callback = progress_callback
|
|
593
581
|
self._progress_tracker: ProgressTracker | None = None
|
|
594
|
-
self._enable_rich_progress = enable_rich_progress
|
|
595
|
-
self._rich_reporter: RichProgressReporter | None = None
|
|
596
582
|
|
|
597
583
|
# New: LLMExecutor support
|
|
598
584
|
self._executor = executor
|
|
@@ -1069,39 +1055,15 @@ class BaseWorkflow(CachingMixin, TelemetryMixin, ABC):
|
|
|
1069
1055
|
current_data = kwargs
|
|
1070
1056
|
error = None
|
|
1071
1057
|
|
|
1072
|
-
# Initialize progress tracker
|
|
1073
|
-
# Always show progress by default (IDE-friendly console output)
|
|
1074
|
-
# Rich live display only when explicitly enabled AND in TTY
|
|
1075
|
-
from .progress import ConsoleProgressReporter
|
|
1076
|
-
|
|
1077
|
-
self._progress_tracker = ProgressTracker(
|
|
1078
|
-
workflow_name=self.name,
|
|
1079
|
-
workflow_id=self._run_id,
|
|
1080
|
-
stage_names=self.stages,
|
|
1081
|
-
)
|
|
1082
|
-
|
|
1083
|
-
# Add user's callback if provided
|
|
1058
|
+
# Initialize progress tracker if callback provided
|
|
1084
1059
|
if self._progress_callback:
|
|
1060
|
+
self._progress_tracker = ProgressTracker(
|
|
1061
|
+
workflow_name=self.name,
|
|
1062
|
+
workflow_id=self._run_id,
|
|
1063
|
+
stage_names=self.stages,
|
|
1064
|
+
)
|
|
1085
1065
|
self._progress_tracker.add_callback(self._progress_callback)
|
|
1086
|
-
|
|
1087
|
-
# Rich progress: only when explicitly enabled AND in a TTY
|
|
1088
|
-
if self._enable_rich_progress and RICH_AVAILABLE and sys.stdout.isatty():
|
|
1089
|
-
try:
|
|
1090
|
-
self._rich_reporter = RichProgressReporter(self.name, self.stages)
|
|
1091
|
-
self._progress_tracker.add_callback(self._rich_reporter.report)
|
|
1092
|
-
self._rich_reporter.start()
|
|
1093
|
-
except Exception as e:
|
|
1094
|
-
# Fall back to console reporter
|
|
1095
|
-
logger.debug(f"Rich progress unavailable: {e}")
|
|
1096
|
-
self._rich_reporter = None
|
|
1097
|
-
console_reporter = ConsoleProgressReporter(verbose=False)
|
|
1098
|
-
self._progress_tracker.add_callback(console_reporter.report)
|
|
1099
|
-
else:
|
|
1100
|
-
# Default: use console reporter (works in IDEs, terminals, everywhere)
|
|
1101
|
-
console_reporter = ConsoleProgressReporter(verbose=False)
|
|
1102
|
-
self._progress_tracker.add_callback(console_reporter.report)
|
|
1103
|
-
|
|
1104
|
-
self._progress_tracker.start_workflow()
|
|
1066
|
+
self._progress_tracker.start_workflow()
|
|
1105
1067
|
|
|
1106
1068
|
try:
|
|
1107
1069
|
# Tier fallback mode: try CHEAP → CAPABLE → PREMIUM with validation
|
|
@@ -1431,14 +1393,6 @@ class BaseWorkflow(CachingMixin, TelemetryMixin, ABC):
|
|
|
1431
1393
|
if self._progress_tracker and error is None:
|
|
1432
1394
|
self._progress_tracker.complete_workflow()
|
|
1433
1395
|
|
|
1434
|
-
# Stop Rich progress display if active
|
|
1435
|
-
if self._rich_reporter:
|
|
1436
|
-
try:
|
|
1437
|
-
self._rich_reporter.stop()
|
|
1438
|
-
except Exception:
|
|
1439
|
-
pass # Best effort cleanup
|
|
1440
|
-
self._rich_reporter = None
|
|
1441
|
-
|
|
1442
1396
|
# Save to workflow history for dashboard
|
|
1443
1397
|
try:
|
|
1444
1398
|
_save_workflow_run(self.name, provider_str, result)
|
|
@@ -695,7 +695,7 @@ class BugPredictionWorkflow(BaseWorkflow):
|
|
|
695
695
|
{
|
|
696
696
|
"correlations": correlations,
|
|
697
697
|
"correlation_count": len(correlations),
|
|
698
|
-
"high_confidence_count":
|
|
698
|
+
"high_confidence_count": sum(1 for c in correlations if c["confidence"] > 0.6),
|
|
699
699
|
**input_data,
|
|
700
700
|
},
|
|
701
701
|
input_tokens,
|
|
@@ -759,7 +759,7 @@ class BugPredictionWorkflow(BaseWorkflow):
|
|
|
759
759
|
{
|
|
760
760
|
"predictions": predictions[:20], # Top 20 risky files
|
|
761
761
|
"overall_risk_score": round(self._risk_score, 2),
|
|
762
|
-
"high_risk_files":
|
|
762
|
+
"high_risk_files": sum(1 for p in predictions if float(p["risk_score"]) > 0.7),
|
|
763
763
|
**input_data,
|
|
764
764
|
},
|
|
765
765
|
input_tokens,
|
empathy_os/workflows/history.py
CHANGED
|
@@ -459,6 +459,10 @@ class WorkflowHistoryStore:
|
|
|
459
459
|
Returns:
|
|
460
460
|
Number of runs deleted
|
|
461
461
|
"""
|
|
462
|
+
cutoff = datetime.now().replace(
|
|
463
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
464
|
+
).isoformat()
|
|
465
|
+
|
|
462
466
|
cursor = self.conn.cursor()
|
|
463
467
|
|
|
464
468
|
# Get run IDs to delete
|
|
@@ -476,14 +480,12 @@ class WorkflowHistoryStore:
|
|
|
476
480
|
return 0
|
|
477
481
|
|
|
478
482
|
# Delete stages for these runs
|
|
479
|
-
# Security Note: f-string builds placeholder list only ("?, ?, ?")
|
|
480
|
-
# Actual data (run_ids) passed as parameters - SQL injection safe
|
|
481
483
|
placeholders = ",".join("?" * len(run_ids))
|
|
482
484
|
cursor.execute(
|
|
483
485
|
f"DELETE FROM workflow_stages WHERE run_id IN ({placeholders})", run_ids
|
|
484
486
|
)
|
|
485
487
|
|
|
486
|
-
# Delete runs
|
|
488
|
+
# Delete runs
|
|
487
489
|
cursor.execute(
|
|
488
490
|
f"DELETE FROM workflow_runs WHERE run_id IN ({placeholders})", run_ids
|
|
489
491
|
)
|
|
@@ -269,10 +269,10 @@ class PerformanceAuditWorkflow(BaseWorkflow):
|
|
|
269
269
|
# Analyze each file
|
|
270
270
|
analysis: list[dict] = []
|
|
271
271
|
for file_path, file_findings in by_file.items():
|
|
272
|
-
# Calculate file complexity score
|
|
273
|
-
high_count =
|
|
274
|
-
medium_count =
|
|
275
|
-
low_count =
|
|
272
|
+
# Calculate file complexity score (generator expressions for memory efficiency)
|
|
273
|
+
high_count = sum(1 for f in file_findings if f["impact"] == "high")
|
|
274
|
+
medium_count = sum(1 for f in file_findings if f["impact"] == "medium")
|
|
275
|
+
low_count = sum(1 for f in file_findings if f["impact"] == "low")
|
|
276
276
|
|
|
277
277
|
complexity_score = high_count * 10 + medium_count * 5 + low_count * 1
|
|
278
278
|
|
empathy_os/workflows/progress.py
CHANGED
|
@@ -11,35 +11,12 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import json
|
|
14
|
-
import
|
|
15
|
-
import sys
|
|
16
|
-
import uuid
|
|
17
|
-
from collections.abc import Callable, Coroutine, Generator
|
|
18
|
-
from contextlib import contextmanager
|
|
14
|
+
from collections.abc import Callable, Coroutine
|
|
19
15
|
from dataclasses import dataclass, field
|
|
20
16
|
from datetime import datetime
|
|
21
17
|
from enum import Enum
|
|
22
18
|
from typing import Any, Protocol
|
|
23
19
|
|
|
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
|
-
|
|
43
20
|
|
|
44
21
|
class ProgressStatus(Enum):
|
|
45
22
|
"""Status of a workflow or stage."""
|
|
@@ -384,9 +361,9 @@ class ProgressTracker:
|
|
|
384
361
|
for callback in self._callbacks:
|
|
385
362
|
try:
|
|
386
363
|
callback(update)
|
|
387
|
-
except Exception
|
|
388
|
-
#
|
|
389
|
-
|
|
364
|
+
except Exception as e:
|
|
365
|
+
# Log but don't fail on callback errors
|
|
366
|
+
print(f"Progress callback error: {e}")
|
|
390
367
|
|
|
391
368
|
# Call async callbacks
|
|
392
369
|
for async_callback in self._async_callbacks:
|
|
@@ -410,111 +387,39 @@ class ProgressReporter(Protocol):
|
|
|
410
387
|
|
|
411
388
|
|
|
412
389
|
class ConsoleProgressReporter:
|
|
413
|
-
"""
|
|
390
|
+
"""Simple console-based progress reporter for CLI usage."""
|
|
414
391
|
|
|
415
|
-
|
|
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
|
-
"""
|
|
392
|
+
def __init__(self, verbose: bool = False):
|
|
431
393
|
self.verbose = verbose
|
|
432
|
-
self.show_tokens = show_tokens
|
|
433
|
-
self._start_time: datetime | None = None
|
|
434
|
-
self._stage_times: dict[str, int] = {}
|
|
435
394
|
|
|
436
395
|
def report(self, update: ProgressUpdate) -> None:
|
|
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}%"
|
|
396
|
+
"""Print progress to console."""
|
|
397
|
+
percent = f"{update.percent_complete:.0f}%"
|
|
447
398
|
cost = f"${update.cost_so_far:.4f}"
|
|
448
|
-
|
|
449
|
-
# Status icons that work in most environments
|
|
450
399
|
status_icon = {
|
|
451
400
|
ProgressStatus.PENDING: "○",
|
|
452
|
-
ProgressStatus.RUNNING: "
|
|
453
|
-
ProgressStatus.COMPLETED: "
|
|
401
|
+
ProgressStatus.RUNNING: "◐",
|
|
402
|
+
ProgressStatus.COMPLETED: "●",
|
|
454
403
|
ProgressStatus.FAILED: "✗",
|
|
455
|
-
ProgressStatus.SKIPPED: "
|
|
456
|
-
ProgressStatus.FALLBACK: "
|
|
404
|
+
ProgressStatus.SKIPPED: "◌",
|
|
405
|
+
ProgressStatus.FALLBACK: "↩",
|
|
457
406
|
ProgressStatus.RETRYING: "↻",
|
|
458
407
|
}.get(update.status, "?")
|
|
459
408
|
|
|
460
409
|
# Get current tier from running stage
|
|
461
410
|
tier_info = ""
|
|
462
|
-
model_info = ""
|
|
463
411
|
if update.current_stage and update.stages:
|
|
464
412
|
for stage in update.stages:
|
|
465
|
-
if stage.name == update.current_stage:
|
|
466
|
-
|
|
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
|
|
413
|
+
if stage.name == update.current_stage and stage.status == ProgressStatus.RUNNING:
|
|
414
|
+
tier_info = f" [{stage.tier.upper()}]"
|
|
473
415
|
break
|
|
474
416
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if self.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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)
|
|
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}")
|
|
518
423
|
|
|
519
424
|
async def report_async(self, update: ProgressUpdate) -> None:
|
|
520
425
|
"""Async version just calls sync."""
|
|
@@ -558,6 +463,8 @@ def create_progress_tracker(
|
|
|
558
463
|
Configured ProgressTracker instance
|
|
559
464
|
|
|
560
465
|
"""
|
|
466
|
+
import uuid
|
|
467
|
+
|
|
561
468
|
tracker = ProgressTracker(
|
|
562
469
|
workflow_name=workflow_name,
|
|
563
470
|
workflow_id=uuid.uuid4().hex[:12],
|
|
@@ -568,212 +475,3 @@ def create_progress_tracker(
|
|
|
568
475
|
tracker.add_callback(reporter.report)
|
|
569
476
|
|
|
570
477
|
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()
|
empathy_os/workflows/routing.py
CHANGED
|
@@ -136,12 +136,7 @@ class BalancedRouting(TierRoutingStrategy):
|
|
|
136
136
|
|
|
137
137
|
Args:
|
|
138
138
|
total_budget: Total budget in USD for this workflow execution
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
ValueError: If total_budget is not positive
|
|
142
139
|
"""
|
|
143
|
-
if total_budget <= 0:
|
|
144
|
-
raise ValueError("total_budget must be positive")
|
|
145
140
|
self.total_budget = total_budget
|
|
146
141
|
|
|
147
142
|
def route(self, context: RoutingContext) -> ModelTier:
|