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.
Files changed (38) hide show
  1. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/METADATA +1 -1
  2. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/RECORD +27 -38
  3. empathy_os/cache/hash_only.py +3 -6
  4. empathy_os/cache/hybrid.py +3 -6
  5. empathy_os/cli_legacy.py +1 -27
  6. empathy_os/cli_unified.py +0 -25
  7. empathy_os/memory/__init__.py +5 -19
  8. empathy_os/memory/short_term.py +132 -10
  9. empathy_os/memory/types.py +4 -0
  10. empathy_os/models/registry.py +4 -4
  11. empathy_os/project_index/scanner.py +3 -2
  12. empathy_os/socratic/ab_testing.py +1 -1
  13. empathy_os/workflow_commands.py +9 -9
  14. empathy_os/workflows/__init__.py +4 -4
  15. empathy_os/workflows/base.py +8 -54
  16. empathy_os/workflows/bug_predict.py +2 -2
  17. empathy_os/workflows/history.py +5 -3
  18. empathy_os/workflows/perf_audit.py +4 -4
  19. empathy_os/workflows/progress.py +22 -324
  20. empathy_os/workflows/routing.py +0 -5
  21. empathy_os/workflows/security_audit.py +0 -189
  22. empathy_os/workflows/security_audit_phase3.py +26 -2
  23. empathy_os/workflows/test_gen.py +7 -7
  24. empathy_os/vscode_bridge 2.py +0 -173
  25. empathy_os/workflows/output.py +0 -410
  26. empathy_os/workflows/progressive/README 2.md +0 -454
  27. empathy_os/workflows/progressive/__init__ 2.py +0 -92
  28. empathy_os/workflows/progressive/cli 2.py +0 -242
  29. empathy_os/workflows/progressive/core 2.py +0 -488
  30. empathy_os/workflows/progressive/orchestrator 2.py +0 -701
  31. empathy_os/workflows/progressive/reports 2.py +0 -528
  32. empathy_os/workflows/progressive/telemetry 2.py +0 -280
  33. empathy_os/workflows/progressive/test_gen 2.py +0 -514
  34. empathy_os/workflows/progressive/workflow 2.py +0 -628
  35. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/WHEEL +0 -0
  36. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/entry_points.txt +0 -0
  37. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/licenses/LICENSE +0 -0
  38. {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/top_level.txt +0 -0
@@ -137,7 +137,7 @@ def morning_workflow(
137
137
  print("-" * 40)
138
138
 
139
139
  total_bugs = len(patterns.get("debugging", []))
140
- resolved_bugs = len([p for p in patterns.get("debugging", []) if p.get("status") == "resolved"])
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 = len([line for line in output.split("\n") if line.strip()])
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 = len([line for line in output.split("\n") if line.strip()])
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 = len([line for line in output.split("\n") if line.strip()])
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 = len([line for line in output.split("\n") if line.strip()])
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 = len(
431
- [line for line in output.split("\n") if line.startswith(("A ", "M ", "D ", "R "))],
430
+ staged = sum(
431
+ 1 for line in output.split("\n") if line.startswith(("A ", "M ", "D ", "R "))
432
432
  )
433
- unstaged = len([line for line in output.split("\n") if line.startswith((" M", " D", "??"))])
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 = len([line for line in output.split("\n") if "error" in line.lower()])
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)
@@ -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
- from .step_config import WorkflowStepConfig, steps_from_tier_map, validate_step_config
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]] = {
@@ -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": len([c for c in correlations if c["confidence"] > 0.6]),
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": len([p for p in predictions if float(p["risk_score"]) > 0.7]),
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,
@@ -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 (same safe parameterization pattern)
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 = len([f for f in file_findings if f["impact"] == "high"])
274
- medium_count = len([f for f in file_findings if f["impact"] == "medium"])
275
- low_count = len([f for f in file_findings if f["impact"] == "low"])
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
 
@@ -11,35 +11,12 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  import json
14
- import logging
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: # noqa: BLE001
388
- # INTENTIONAL: Callbacks are optional - never fail workflow on callback error
389
- logger.warning("Progress callback error", exc_info=True)
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
- """Console-based progress reporter optimized for IDE environments.
390
+ """Simple console-based progress reporter for CLI usage."""
414
391
 
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
- """
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
- 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
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
- # 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)
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()
@@ -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: