empathy-framework 3.7.0__py3-none-any.whl → 3.8.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.
- coach_wizards/code_reviewer_README.md +60 -0
- coach_wizards/code_reviewer_wizard.py +180 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +148 -11
- empathy_framework-3.8.0.dist-info/RECORD +333 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +5 -1
- empathy_healthcare_plugin/monitors/__init__.py +9 -0
- empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
- empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
- empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
- empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
- empathy_llm_toolkit/agent_factory/__init__.py +53 -0
- empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
- empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
- empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
- empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
- empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
- empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
- empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
- empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
- empathy_llm_toolkit/agent_factory/base.py +305 -0
- empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
- empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
- empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
- empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
- empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
- empathy_llm_toolkit/agent_factory/decorators.py +286 -0
- empathy_llm_toolkit/agent_factory/factory.py +558 -0
- empathy_llm_toolkit/agent_factory/framework.py +192 -0
- empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
- empathy_llm_toolkit/agent_factory/resilient.py +320 -0
- empathy_llm_toolkit/cli/__init__.py +8 -0
- empathy_llm_toolkit/cli/sync_claude.py +487 -0
- empathy_llm_toolkit/code_health.py +150 -3
- empathy_llm_toolkit/config/__init__.py +29 -0
- empathy_llm_toolkit/config/unified.py +295 -0
- empathy_llm_toolkit/routing/__init__.py +32 -0
- empathy_llm_toolkit/routing/model_router.py +362 -0
- empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
- empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
- empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
- empathy_llm_toolkit/security/README.md +262 -0
- empathy_llm_toolkit/security/__init__.py +62 -0
- empathy_llm_toolkit/security/audit_logger.py +929 -0
- empathy_llm_toolkit/security/audit_logger_example.py +152 -0
- empathy_llm_toolkit/security/pii_scrubber.py +640 -0
- empathy_llm_toolkit/security/secrets_detector.py +678 -0
- empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
- empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
- empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
- empathy_llm_toolkit/wizards/__init__.py +38 -0
- empathy_llm_toolkit/wizards/base_wizard.py +364 -0
- empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
- empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
- empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
- empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
- empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
- empathy_os/__init__.py +52 -52
- empathy_os/adaptive/__init__.py +13 -0
- empathy_os/adaptive/task_complexity.py +127 -0
- empathy_os/cache/__init__.py +117 -0
- empathy_os/cache/base.py +166 -0
- empathy_os/cache/dependency_manager.py +253 -0
- empathy_os/cache/hash_only.py +248 -0
- empathy_os/cache/hybrid.py +390 -0
- empathy_os/cache/storage.py +282 -0
- empathy_os/cli.py +118 -8
- empathy_os/cli_unified.py +121 -1
- empathy_os/config/__init__.py +63 -0
- empathy_os/config/xml_config.py +239 -0
- empathy_os/config.py +2 -1
- empathy_os/dashboard/__init__.py +15 -0
- empathy_os/dashboard/server.py +743 -0
- empathy_os/memory/__init__.py +195 -0
- empathy_os/memory/claude_memory.py +466 -0
- empathy_os/memory/config.py +224 -0
- empathy_os/memory/control_panel.py +1298 -0
- empathy_os/memory/edges.py +179 -0
- empathy_os/memory/graph.py +567 -0
- empathy_os/memory/long_term.py +1194 -0
- empathy_os/memory/nodes.py +179 -0
- empathy_os/memory/redis_bootstrap.py +540 -0
- empathy_os/memory/security/__init__.py +31 -0
- empathy_os/memory/security/audit_logger.py +930 -0
- empathy_os/memory/security/pii_scrubber.py +640 -0
- empathy_os/memory/security/secrets_detector.py +678 -0
- empathy_os/memory/short_term.py +2119 -0
- empathy_os/memory/storage/__init__.py +15 -0
- empathy_os/memory/summary_index.py +583 -0
- empathy_os/memory/unified.py +619 -0
- empathy_os/metrics/__init__.py +12 -0
- empathy_os/metrics/prompt_metrics.py +190 -0
- empathy_os/models/__init__.py +136 -0
- empathy_os/models/__main__.py +13 -0
- empathy_os/models/cli.py +655 -0
- empathy_os/models/empathy_executor.py +354 -0
- empathy_os/models/executor.py +252 -0
- empathy_os/models/fallback.py +671 -0
- empathy_os/models/provider_config.py +563 -0
- empathy_os/models/registry.py +382 -0
- empathy_os/models/tasks.py +302 -0
- empathy_os/models/telemetry.py +548 -0
- empathy_os/models/token_estimator.py +378 -0
- empathy_os/models/validation.py +274 -0
- empathy_os/monitoring/__init__.py +52 -0
- empathy_os/monitoring/alerts.py +23 -0
- empathy_os/monitoring/alerts_cli.py +268 -0
- empathy_os/monitoring/multi_backend.py +271 -0
- empathy_os/monitoring/otel_backend.py +363 -0
- empathy_os/optimization/__init__.py +19 -0
- empathy_os/optimization/context_optimizer.py +272 -0
- empathy_os/plugins/__init__.py +28 -0
- empathy_os/plugins/base.py +361 -0
- empathy_os/plugins/registry.py +268 -0
- empathy_os/project_index/__init__.py +30 -0
- empathy_os/project_index/cli.py +335 -0
- empathy_os/project_index/crew_integration.py +430 -0
- empathy_os/project_index/index.py +425 -0
- empathy_os/project_index/models.py +501 -0
- empathy_os/project_index/reports.py +473 -0
- empathy_os/project_index/scanner.py +538 -0
- empathy_os/prompts/__init__.py +61 -0
- empathy_os/prompts/config.py +77 -0
- empathy_os/prompts/context.py +177 -0
- empathy_os/prompts/parser.py +285 -0
- empathy_os/prompts/registry.py +313 -0
- empathy_os/prompts/templates.py +208 -0
- empathy_os/resilience/__init__.py +56 -0
- empathy_os/resilience/circuit_breaker.py +256 -0
- empathy_os/resilience/fallback.py +179 -0
- empathy_os/resilience/health.py +300 -0
- empathy_os/resilience/retry.py +209 -0
- empathy_os/resilience/timeout.py +135 -0
- empathy_os/routing/__init__.py +43 -0
- empathy_os/routing/chain_executor.py +433 -0
- empathy_os/routing/classifier.py +217 -0
- empathy_os/routing/smart_router.py +234 -0
- empathy_os/routing/wizard_registry.py +307 -0
- empathy_os/trust/__init__.py +28 -0
- empathy_os/trust/circuit_breaker.py +579 -0
- empathy_os/validation/__init__.py +19 -0
- empathy_os/validation/xml_validator.py +281 -0
- empathy_os/wizard_factory_cli.py +170 -0
- empathy_os/workflows/__init__.py +360 -0
- empathy_os/workflows/base.py +1660 -0
- empathy_os/workflows/bug_predict.py +962 -0
- empathy_os/workflows/code_review.py +960 -0
- empathy_os/workflows/code_review_adapters.py +310 -0
- empathy_os/workflows/code_review_pipeline.py +720 -0
- empathy_os/workflows/config.py +600 -0
- empathy_os/workflows/dependency_check.py +648 -0
- empathy_os/workflows/document_gen.py +1069 -0
- empathy_os/workflows/documentation_orchestrator.py +1205 -0
- empathy_os/workflows/health_check.py +679 -0
- empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
- empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
- empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
- empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
- empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
- empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
- empathy_os/workflows/manage_documentation.py +804 -0
- empathy_os/workflows/new_sample_workflow1.py +146 -0
- empathy_os/workflows/new_sample_workflow1_README.md +150 -0
- empathy_os/workflows/perf_audit.py +687 -0
- empathy_os/workflows/pr_review.py +748 -0
- empathy_os/workflows/progress.py +445 -0
- empathy_os/workflows/progress_server.py +322 -0
- empathy_os/workflows/refactor_plan.py +693 -0
- empathy_os/workflows/release_prep.py +808 -0
- empathy_os/workflows/research_synthesis.py +404 -0
- empathy_os/workflows/secure_release.py +585 -0
- empathy_os/workflows/security_adapters.py +297 -0
- empathy_os/workflows/security_audit.py +1046 -0
- empathy_os/workflows/step_config.py +234 -0
- empathy_os/workflows/test5.py +125 -0
- empathy_os/workflows/test5_README.md +158 -0
- empathy_os/workflows/test_gen.py +1855 -0
- empathy_os/workflows/test_lifecycle.py +526 -0
- empathy_os/workflows/test_maintenance.py +626 -0
- empathy_os/workflows/test_maintenance_cli.py +590 -0
- empathy_os/workflows/test_maintenance_crew.py +821 -0
- empathy_os/workflows/xml_enhanced_crew.py +285 -0
- empathy_software_plugin/cli/__init__.py +120 -0
- empathy_software_plugin/cli/inspect.py +362 -0
- empathy_software_plugin/cli.py +3 -1
- empathy_software_plugin/wizards/__init__.py +42 -0
- empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
- empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
- empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
- empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
- empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
- empathy_software_plugin/wizards/base_wizard.py +288 -0
- empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
- empathy_software_plugin/wizards/code_review_wizard.py +606 -0
- empathy_software_plugin/wizards/debugging/__init__.py +50 -0
- empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
- empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
- empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
- empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
- empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
- empathy_software_plugin/wizards/debugging/verification.py +369 -0
- empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
- empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
- empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
- empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
- empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
- empathy_software_plugin/wizards/performance/__init__.py +9 -0
- empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
- empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
- empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
- empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
- empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
- empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
- empathy_software_plugin/wizards/security/__init__.py +32 -0
- empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
- empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
- empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
- empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
- empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
- empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
- empathy_software_plugin/wizards/testing/__init__.py +27 -0
- empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
- empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
- empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
- empathy_software_plugin/wizards/testing_wizard.py +274 -0
- hot_reload/README.md +473 -0
- hot_reload/__init__.py +62 -0
- hot_reload/config.py +84 -0
- hot_reload/integration.py +228 -0
- hot_reload/reloader.py +298 -0
- hot_reload/watcher.py +179 -0
- hot_reload/websocket.py +176 -0
- scaffolding/README.md +589 -0
- scaffolding/__init__.py +35 -0
- scaffolding/__main__.py +14 -0
- scaffolding/cli.py +240 -0
- test_generator/__init__.py +38 -0
- test_generator/__main__.py +14 -0
- test_generator/cli.py +226 -0
- test_generator/generator.py +325 -0
- test_generator/risk_analyzer.py +216 -0
- workflow_patterns/__init__.py +33 -0
- workflow_patterns/behavior.py +249 -0
- workflow_patterns/core.py +76 -0
- workflow_patterns/output.py +99 -0
- workflow_patterns/registry.py +255 -0
- workflow_patterns/structural.py +288 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
- agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
- agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
- agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
- agents/compliance_anticipation_agent.py +0 -1422
- agents/compliance_db.py +0 -339
- agents/epic_integration_wizard.py +0 -530
- agents/notifications.py +0 -291
- agents/trust_building_behaviors.py +0 -872
- empathy_framework-3.7.0.dist-info/RECORD +0 -105
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
- /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Progress Tracking System
|
|
2
|
+
|
|
3
|
+
Real-time progress tracking for workflow execution with WebSocket support.
|
|
4
|
+
Enables live UI updates during workflow runs.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
7
|
+
Licensed under Fair Source 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
from collections.abc import Callable, Coroutine
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Protocol
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProgressStatus(Enum):
|
|
22
|
+
"""Status of a workflow or stage."""
|
|
23
|
+
|
|
24
|
+
PENDING = "pending"
|
|
25
|
+
RUNNING = "running"
|
|
26
|
+
COMPLETED = "completed"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
SKIPPED = "skipped"
|
|
29
|
+
FALLBACK = "fallback" # Using fallback model
|
|
30
|
+
RETRYING = "retrying" # Retrying after error
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class StageProgress:
|
|
35
|
+
"""Progress information for a single stage."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
status: ProgressStatus
|
|
39
|
+
tier: str = "capable"
|
|
40
|
+
model: str = ""
|
|
41
|
+
started_at: datetime | None = None
|
|
42
|
+
completed_at: datetime | None = None
|
|
43
|
+
duration_ms: int = 0
|
|
44
|
+
cost: float = 0.0
|
|
45
|
+
tokens_in: int = 0
|
|
46
|
+
tokens_out: int = 0
|
|
47
|
+
error: str | None = None
|
|
48
|
+
fallback_info: str | None = None
|
|
49
|
+
retry_count: int = 0
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
"""Convert to dictionary for JSON serialization."""
|
|
53
|
+
return {
|
|
54
|
+
"name": self.name,
|
|
55
|
+
"status": self.status.value,
|
|
56
|
+
"tier": self.tier,
|
|
57
|
+
"model": self.model,
|
|
58
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
59
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
60
|
+
"duration_ms": self.duration_ms,
|
|
61
|
+
"cost": self.cost,
|
|
62
|
+
"tokens_in": self.tokens_in,
|
|
63
|
+
"tokens_out": self.tokens_out,
|
|
64
|
+
"error": self.error,
|
|
65
|
+
"fallback_info": self.fallback_info,
|
|
66
|
+
"retry_count": self.retry_count,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ProgressUpdate:
|
|
72
|
+
"""A progress update to be broadcast."""
|
|
73
|
+
|
|
74
|
+
workflow: str
|
|
75
|
+
workflow_id: str
|
|
76
|
+
current_stage: str
|
|
77
|
+
stage_index: int
|
|
78
|
+
total_stages: int
|
|
79
|
+
status: ProgressStatus
|
|
80
|
+
message: str
|
|
81
|
+
cost_so_far: float = 0.0
|
|
82
|
+
tokens_so_far: int = 0
|
|
83
|
+
percent_complete: float = 0.0
|
|
84
|
+
estimated_remaining_ms: int | None = None
|
|
85
|
+
stages: list[StageProgress] = field(default_factory=list)
|
|
86
|
+
fallback_info: str | None = None
|
|
87
|
+
error: str | None = None
|
|
88
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
"""Convert to dictionary for JSON serialization."""
|
|
92
|
+
return {
|
|
93
|
+
"workflow": self.workflow,
|
|
94
|
+
"workflow_id": self.workflow_id,
|
|
95
|
+
"current_stage": self.current_stage,
|
|
96
|
+
"stage_index": self.stage_index,
|
|
97
|
+
"total_stages": self.total_stages,
|
|
98
|
+
"status": self.status.value,
|
|
99
|
+
"message": self.message,
|
|
100
|
+
"cost_so_far": self.cost_so_far,
|
|
101
|
+
"tokens_so_far": self.tokens_so_far,
|
|
102
|
+
"percent_complete": self.percent_complete,
|
|
103
|
+
"estimated_remaining_ms": self.estimated_remaining_ms,
|
|
104
|
+
"stages": [s.to_dict() for s in self.stages],
|
|
105
|
+
"fallback_info": self.fallback_info,
|
|
106
|
+
"error": self.error,
|
|
107
|
+
"timestamp": self.timestamp.isoformat(),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def to_json(self) -> str:
|
|
111
|
+
"""Convert to JSON string."""
|
|
112
|
+
return json.dumps(self.to_dict())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Type for progress callbacks
|
|
116
|
+
ProgressCallback = Callable[[ProgressUpdate], None]
|
|
117
|
+
AsyncProgressCallback = Callable[[ProgressUpdate], Coroutine[Any, Any, None]]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ProgressTracker:
|
|
121
|
+
"""Tracks and broadcasts workflow progress.
|
|
122
|
+
|
|
123
|
+
Maintains state for all stages and emits updates to registered callbacks.
|
|
124
|
+
Supports both sync and async callbacks for flexibility.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
workflow_name: str,
|
|
130
|
+
workflow_id: str,
|
|
131
|
+
stage_names: list[str],
|
|
132
|
+
):
|
|
133
|
+
self.workflow = workflow_name
|
|
134
|
+
self.workflow_id = workflow_id
|
|
135
|
+
self.stage_names = stage_names
|
|
136
|
+
self.current_index = 0
|
|
137
|
+
self.cost_accumulated = 0.0
|
|
138
|
+
self.tokens_accumulated = 0
|
|
139
|
+
self._started_at = datetime.now()
|
|
140
|
+
self._stage_start_times: dict[str, datetime] = {}
|
|
141
|
+
self._stage_durations: list[int] = []
|
|
142
|
+
|
|
143
|
+
# Initialize stages
|
|
144
|
+
self.stages: list[StageProgress] = [
|
|
145
|
+
StageProgress(name=name, status=ProgressStatus.PENDING) for name in stage_names
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# Callbacks
|
|
149
|
+
self._callbacks: list[ProgressCallback] = []
|
|
150
|
+
self._async_callbacks: list[AsyncProgressCallback] = []
|
|
151
|
+
|
|
152
|
+
def add_callback(self, callback: ProgressCallback) -> None:
|
|
153
|
+
"""Add a synchronous progress callback."""
|
|
154
|
+
self._callbacks.append(callback)
|
|
155
|
+
|
|
156
|
+
def add_async_callback(self, callback: AsyncProgressCallback) -> None:
|
|
157
|
+
"""Add an asynchronous progress callback."""
|
|
158
|
+
self._async_callbacks.append(callback)
|
|
159
|
+
|
|
160
|
+
def remove_callback(self, callback: ProgressCallback) -> None:
|
|
161
|
+
"""Remove a synchronous callback."""
|
|
162
|
+
if callback in self._callbacks:
|
|
163
|
+
self._callbacks.remove(callback)
|
|
164
|
+
|
|
165
|
+
def start_workflow(self) -> None:
|
|
166
|
+
"""Mark workflow as started."""
|
|
167
|
+
self._started_at = datetime.now()
|
|
168
|
+
self._emit(ProgressStatus.RUNNING, f"Starting {self.workflow}...")
|
|
169
|
+
|
|
170
|
+
def start_stage(self, stage_name: str, tier: str = "capable", model: str = "") -> None:
|
|
171
|
+
"""Mark a stage as started."""
|
|
172
|
+
stage = self._get_stage(stage_name)
|
|
173
|
+
if stage:
|
|
174
|
+
stage.status = ProgressStatus.RUNNING
|
|
175
|
+
stage.started_at = datetime.now()
|
|
176
|
+
stage.tier = tier
|
|
177
|
+
stage.model = model
|
|
178
|
+
self._stage_start_times[stage_name] = stage.started_at
|
|
179
|
+
self.current_index = self.stage_names.index(stage_name)
|
|
180
|
+
|
|
181
|
+
self._emit(ProgressStatus.RUNNING, f"Running {stage_name}...")
|
|
182
|
+
|
|
183
|
+
def complete_stage(
|
|
184
|
+
self,
|
|
185
|
+
stage_name: str,
|
|
186
|
+
cost: float = 0.0,
|
|
187
|
+
tokens_in: int = 0,
|
|
188
|
+
tokens_out: int = 0,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Mark a stage as completed."""
|
|
191
|
+
stage = self._get_stage(stage_name)
|
|
192
|
+
if stage:
|
|
193
|
+
stage.status = ProgressStatus.COMPLETED
|
|
194
|
+
stage.completed_at = datetime.now()
|
|
195
|
+
stage.cost = cost
|
|
196
|
+
stage.tokens_in = tokens_in
|
|
197
|
+
stage.tokens_out = tokens_out
|
|
198
|
+
|
|
199
|
+
if stage.started_at:
|
|
200
|
+
duration_ms = int((stage.completed_at - stage.started_at).total_seconds() * 1000)
|
|
201
|
+
stage.duration_ms = duration_ms
|
|
202
|
+
self._stage_durations.append(duration_ms)
|
|
203
|
+
|
|
204
|
+
self.cost_accumulated += cost
|
|
205
|
+
self.tokens_accumulated += tokens_in + tokens_out
|
|
206
|
+
self.current_index = self.stage_names.index(stage_name) + 1
|
|
207
|
+
|
|
208
|
+
self._emit(ProgressStatus.COMPLETED, f"Completed {stage_name}")
|
|
209
|
+
|
|
210
|
+
def fail_stage(self, stage_name: str, error: str) -> None:
|
|
211
|
+
"""Mark a stage as failed."""
|
|
212
|
+
stage = self._get_stage(stage_name)
|
|
213
|
+
if stage:
|
|
214
|
+
stage.status = ProgressStatus.FAILED
|
|
215
|
+
stage.completed_at = datetime.now()
|
|
216
|
+
stage.error = error
|
|
217
|
+
|
|
218
|
+
if stage.started_at:
|
|
219
|
+
stage.duration_ms = int(
|
|
220
|
+
(stage.completed_at - stage.started_at).total_seconds() * 1000,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
self._emit(ProgressStatus.FAILED, f"Failed: {stage_name}", error=error)
|
|
224
|
+
|
|
225
|
+
def skip_stage(self, stage_name: str, reason: str = "") -> None:
|
|
226
|
+
"""Mark a stage as skipped."""
|
|
227
|
+
stage = self._get_stage(stage_name)
|
|
228
|
+
if stage:
|
|
229
|
+
stage.status = ProgressStatus.SKIPPED
|
|
230
|
+
|
|
231
|
+
message = f"Skipped {stage_name}"
|
|
232
|
+
if reason:
|
|
233
|
+
message += f": {reason}"
|
|
234
|
+
self._emit(ProgressStatus.SKIPPED, message)
|
|
235
|
+
|
|
236
|
+
def fallback_occurred(
|
|
237
|
+
self,
|
|
238
|
+
stage_name: str,
|
|
239
|
+
original_model: str,
|
|
240
|
+
fallback_model: str,
|
|
241
|
+
reason: str,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Record that a fallback occurred."""
|
|
244
|
+
stage = self._get_stage(stage_name)
|
|
245
|
+
fallback_info = f"{original_model} → {fallback_model} ({reason})"
|
|
246
|
+
|
|
247
|
+
if stage:
|
|
248
|
+
stage.status = ProgressStatus.FALLBACK
|
|
249
|
+
stage.fallback_info = fallback_info
|
|
250
|
+
|
|
251
|
+
self._emit(
|
|
252
|
+
ProgressStatus.FALLBACK,
|
|
253
|
+
f"Falling back from {original_model} to {fallback_model}",
|
|
254
|
+
fallback_info=fallback_info,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def retry_occurred(self, stage_name: str, attempt: int, max_attempts: int) -> None:
|
|
258
|
+
"""Record that a retry is occurring."""
|
|
259
|
+
stage = self._get_stage(stage_name)
|
|
260
|
+
if stage:
|
|
261
|
+
stage.status = ProgressStatus.RETRYING
|
|
262
|
+
stage.retry_count = attempt
|
|
263
|
+
|
|
264
|
+
self._emit(
|
|
265
|
+
ProgressStatus.RETRYING,
|
|
266
|
+
f"Retrying {stage_name} (attempt {attempt}/{max_attempts})",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def complete_workflow(self) -> None:
|
|
270
|
+
"""Mark workflow as completed."""
|
|
271
|
+
self._emit(
|
|
272
|
+
ProgressStatus.COMPLETED,
|
|
273
|
+
f"Workflow {self.workflow} completed",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def fail_workflow(self, error: str) -> None:
|
|
277
|
+
"""Mark workflow as failed."""
|
|
278
|
+
self._emit(
|
|
279
|
+
ProgressStatus.FAILED,
|
|
280
|
+
f"Workflow {self.workflow} failed",
|
|
281
|
+
error=error,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _get_stage(self, stage_name: str) -> StageProgress | None:
|
|
285
|
+
"""Get stage by name."""
|
|
286
|
+
for stage in self.stages:
|
|
287
|
+
if stage.name == stage_name:
|
|
288
|
+
return stage
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def _calculate_percent_complete(self) -> float:
|
|
292
|
+
"""Calculate completion percentage."""
|
|
293
|
+
completed = sum(1 for s in self.stages if s.status == ProgressStatus.COMPLETED)
|
|
294
|
+
return (completed / len(self.stages)) * 100 if self.stages else 0.0
|
|
295
|
+
|
|
296
|
+
def _estimate_remaining_ms(self) -> int | None:
|
|
297
|
+
"""Estimate remaining time based on average stage duration."""
|
|
298
|
+
if not self._stage_durations:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
avg_duration = sum(self._stage_durations) / len(self._stage_durations)
|
|
302
|
+
remaining_stages = len(self.stages) - self.current_index
|
|
303
|
+
return int(avg_duration * remaining_stages)
|
|
304
|
+
|
|
305
|
+
def _emit(
|
|
306
|
+
self,
|
|
307
|
+
status: ProgressStatus,
|
|
308
|
+
message: str,
|
|
309
|
+
fallback_info: str | None = None,
|
|
310
|
+
error: str | None = None,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Emit a progress update to all callbacks."""
|
|
313
|
+
current_stage = (
|
|
314
|
+
self.stage_names[min(self.current_index, len(self.stage_names) - 1)]
|
|
315
|
+
if self.stage_names
|
|
316
|
+
else ""
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
update = ProgressUpdate(
|
|
320
|
+
workflow=self.workflow,
|
|
321
|
+
workflow_id=self.workflow_id,
|
|
322
|
+
current_stage=current_stage,
|
|
323
|
+
stage_index=self.current_index,
|
|
324
|
+
total_stages=len(self.stages),
|
|
325
|
+
status=status,
|
|
326
|
+
message=message,
|
|
327
|
+
cost_so_far=self.cost_accumulated,
|
|
328
|
+
tokens_so_far=self.tokens_accumulated,
|
|
329
|
+
percent_complete=self._calculate_percent_complete(),
|
|
330
|
+
estimated_remaining_ms=self._estimate_remaining_ms(),
|
|
331
|
+
stages=list(self.stages),
|
|
332
|
+
fallback_info=fallback_info,
|
|
333
|
+
error=error,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Call sync callbacks
|
|
337
|
+
for callback in self._callbacks:
|
|
338
|
+
try:
|
|
339
|
+
callback(update)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
# Log but don't fail on callback errors
|
|
342
|
+
print(f"Progress callback error: {e}")
|
|
343
|
+
|
|
344
|
+
# Call async callbacks
|
|
345
|
+
for async_callback in self._async_callbacks:
|
|
346
|
+
try:
|
|
347
|
+
asyncio.create_task(async_callback(update))
|
|
348
|
+
except RuntimeError:
|
|
349
|
+
# No event loop running, skip async callbacks
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class ProgressReporter(Protocol):
|
|
354
|
+
"""Protocol for progress reporting implementations."""
|
|
355
|
+
|
|
356
|
+
def report(self, update: ProgressUpdate) -> None:
|
|
357
|
+
"""Report a progress update."""
|
|
358
|
+
...
|
|
359
|
+
|
|
360
|
+
async def report_async(self, update: ProgressUpdate) -> None:
|
|
361
|
+
"""Report a progress update asynchronously."""
|
|
362
|
+
...
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ConsoleProgressReporter:
|
|
366
|
+
"""Simple console-based progress reporter for CLI usage."""
|
|
367
|
+
|
|
368
|
+
def __init__(self, verbose: bool = False):
|
|
369
|
+
self.verbose = verbose
|
|
370
|
+
|
|
371
|
+
def report(self, update: ProgressUpdate) -> None:
|
|
372
|
+
"""Print progress to console."""
|
|
373
|
+
percent = f"{update.percent_complete:.0f}%"
|
|
374
|
+
cost = f"${update.cost_so_far:.4f}"
|
|
375
|
+
status_icon = {
|
|
376
|
+
ProgressStatus.PENDING: "○",
|
|
377
|
+
ProgressStatus.RUNNING: "◐",
|
|
378
|
+
ProgressStatus.COMPLETED: "●",
|
|
379
|
+
ProgressStatus.FAILED: "✗",
|
|
380
|
+
ProgressStatus.SKIPPED: "◌",
|
|
381
|
+
ProgressStatus.FALLBACK: "↩",
|
|
382
|
+
ProgressStatus.RETRYING: "↻",
|
|
383
|
+
}.get(update.status, "?")
|
|
384
|
+
|
|
385
|
+
print(f"[{percent}] {status_icon} {update.message} ({cost})")
|
|
386
|
+
|
|
387
|
+
if self.verbose and update.fallback_info:
|
|
388
|
+
print(f" Fallback: {update.fallback_info}")
|
|
389
|
+
if self.verbose and update.error:
|
|
390
|
+
print(f" Error: {update.error}")
|
|
391
|
+
|
|
392
|
+
async def report_async(self, update: ProgressUpdate) -> None:
|
|
393
|
+
"""Async version just calls sync."""
|
|
394
|
+
self.report(update)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class JsonLinesProgressReporter:
|
|
398
|
+
"""JSON Lines progress reporter for machine parsing."""
|
|
399
|
+
|
|
400
|
+
def __init__(self, output_file: str | None = None):
|
|
401
|
+
self.output_file = output_file
|
|
402
|
+
|
|
403
|
+
def report(self, update: ProgressUpdate) -> None:
|
|
404
|
+
"""Output progress as JSON line."""
|
|
405
|
+
json_line = update.to_json()
|
|
406
|
+
|
|
407
|
+
if self.output_file:
|
|
408
|
+
with open(self.output_file, "a") as f:
|
|
409
|
+
f.write(json_line + "\n")
|
|
410
|
+
else:
|
|
411
|
+
print(json_line)
|
|
412
|
+
|
|
413
|
+
async def report_async(self, update: ProgressUpdate) -> None:
|
|
414
|
+
"""Async version just calls sync."""
|
|
415
|
+
self.report(update)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def create_progress_tracker(
|
|
419
|
+
workflow_name: str,
|
|
420
|
+
stage_names: list[str],
|
|
421
|
+
reporter: ProgressReporter | None = None,
|
|
422
|
+
) -> ProgressTracker:
|
|
423
|
+
"""Factory function to create a progress tracker with optional reporter.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
workflow_name: Name of the workflow
|
|
427
|
+
stage_names: List of stage names in order
|
|
428
|
+
reporter: Optional progress reporter
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Configured ProgressTracker instance
|
|
432
|
+
|
|
433
|
+
"""
|
|
434
|
+
import uuid
|
|
435
|
+
|
|
436
|
+
tracker = ProgressTracker(
|
|
437
|
+
workflow_name=workflow_name,
|
|
438
|
+
workflow_id=uuid.uuid4().hex[:12],
|
|
439
|
+
stage_names=stage_names,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if reporter:
|
|
443
|
+
tracker.add_callback(reporter.report)
|
|
444
|
+
|
|
445
|
+
return tracker
|