empathy-framework 3.2.3__py3-none-any.whl → 3.8.2__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/__init__.py +11 -12
- coach_wizards/accessibility_wizard.py +12 -12
- coach_wizards/api_wizard.py +12 -12
- coach_wizards/base_wizard.py +26 -20
- coach_wizards/cicd_wizard.py +15 -13
- coach_wizards/code_reviewer_README.md +60 -0
- coach_wizards/code_reviewer_wizard.py +180 -0
- coach_wizards/compliance_wizard.py +12 -12
- coach_wizards/database_wizard.py +12 -12
- coach_wizards/debugging_wizard.py +12 -12
- coach_wizards/documentation_wizard.py +12 -12
- coach_wizards/generate_wizards.py +1 -2
- coach_wizards/localization_wizard.py +101 -19
- coach_wizards/migration_wizard.py +12 -12
- coach_wizards/monitoring_wizard.py +12 -12
- coach_wizards/observability_wizard.py +12 -12
- coach_wizards/performance_wizard.py +12 -12
- coach_wizards/prompt_engineering_wizard.py +22 -25
- coach_wizards/refactoring_wizard.py +12 -12
- coach_wizards/scaling_wizard.py +12 -12
- coach_wizards/security_wizard.py +12 -12
- coach_wizards/testing_wizard.py +12 -12
- {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/METADATA +513 -58
- empathy_framework-3.8.2.dist-info/RECORD +333 -0
- empathy_framework-3.8.2.dist-info/entry_points.txt +22 -0
- {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/top_level.txt +5 -1
- empathy_healthcare_plugin/__init__.py +1 -2
- 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/__init__.py +7 -7
- 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/claude_memory.py +14 -15
- empathy_llm_toolkit/cli/__init__.py +8 -0
- empathy_llm_toolkit/cli/sync_claude.py +487 -0
- empathy_llm_toolkit/code_health.py +177 -22
- empathy_llm_toolkit/config/__init__.py +29 -0
- empathy_llm_toolkit/config/unified.py +295 -0
- empathy_llm_toolkit/contextual_patterns.py +11 -12
- empathy_llm_toolkit/core.py +51 -49
- empathy_llm_toolkit/git_pattern_extractor.py +16 -12
- empathy_llm_toolkit/levels.py +6 -13
- empathy_llm_toolkit/pattern_confidence.py +14 -18
- empathy_llm_toolkit/pattern_resolver.py +10 -12
- empathy_llm_toolkit/pattern_summary.py +13 -11
- empathy_llm_toolkit/providers.py +194 -28
- 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/session_status.py +18 -20
- empathy_llm_toolkit/state.py +20 -21
- 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 +76 -77
- empathy_os/adaptive/__init__.py +13 -0
- empathy_os/adaptive/task_complexity.py +127 -0
- empathy_os/{monitoring.py → agent_monitoring.py} +27 -27
- 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 +515 -109
- empathy_os/cli_unified.py +189 -42
- empathy_os/config/__init__.py +63 -0
- empathy_os/config/xml_config.py +239 -0
- empathy_os/config.py +87 -36
- empathy_os/coordination.py +48 -54
- empathy_os/core.py +90 -99
- empathy_os/cost_tracker.py +20 -23
- empathy_os/dashboard/__init__.py +15 -0
- empathy_os/dashboard/server.py +743 -0
- empathy_os/discovery.py +9 -11
- empathy_os/emergence.py +20 -21
- empathy_os/exceptions.py +18 -30
- empathy_os/feedback_loops.py +27 -30
- empathy_os/levels.py +31 -34
- empathy_os/leverage_points.py +27 -28
- empathy_os/logging_config.py +11 -12
- 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/pattern_library.py +29 -28
- empathy_os/persistence.py +30 -34
- empathy_os/platform_utils.py +261 -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/redis_config.py +144 -58
- empathy_os/redis_memory.py +53 -56
- 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/templates.py +12 -11
- empathy_os/trust/__init__.py +28 -0
- empathy_os/trust/circuit_breaker.py +579 -0
- empathy_os/trust_building.py +44 -36
- 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.py → workflow_commands.py} +123 -31
- 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/__init__.py +1 -2
- empathy_software_plugin/cli/__init__.py +120 -0
- empathy_software_plugin/cli/inspect.py +362 -0
- empathy_software_plugin/cli.py +35 -26
- empathy_software_plugin/plugin.py +4 -8
- 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 -1427
- agents/epic_integration_wizard.py +0 -541
- agents/trust_building_behaviors.py +0 -891
- empathy_framework-3.2.3.dist-info/RECORD +0 -104
- empathy_framework-3.2.3.dist-info/entry_points.txt +0 -7
- empathy_llm_toolkit/htmlcov/status.json +0 -1
- empathy_llm_toolkit/security/htmlcov/status.json +0 -1
- {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/WHEEL +0 -0
- {empathy_framework-3.2.3.dist-info → empathy_framework-3.8.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Integration example for hot-reload with wizard API.
|
|
2
|
+
|
|
3
|
+
Shows how to integrate hot-reload into the existing wizard_api.py.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
12
|
+
|
|
13
|
+
from .config import get_hot_reload_config
|
|
14
|
+
from .reloader import WizardReloader
|
|
15
|
+
from .watcher import WizardFileWatcher
|
|
16
|
+
from .websocket import create_notification_callback, get_notification_manager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HotReloadIntegration:
|
|
22
|
+
"""Integrates hot-reload with wizard API.
|
|
23
|
+
|
|
24
|
+
Example usage in wizard_api.py:
|
|
25
|
+
|
|
26
|
+
from hot_reload.integration import HotReloadIntegration
|
|
27
|
+
|
|
28
|
+
# Create FastAPI app
|
|
29
|
+
app = FastAPI()
|
|
30
|
+
|
|
31
|
+
# Initialize hot-reload (if enabled)
|
|
32
|
+
hot_reload = HotReloadIntegration(app, register_wizard)
|
|
33
|
+
|
|
34
|
+
@app.on_event("startup")
|
|
35
|
+
async def startup_event():
|
|
36
|
+
init_wizards() # Initialize wizards
|
|
37
|
+
hot_reload.start() # Start hot-reload watcher
|
|
38
|
+
|
|
39
|
+
@app.on_event("shutdown")
|
|
40
|
+
async def shutdown_event():
|
|
41
|
+
hot_reload.stop()
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
app: FastAPI,
|
|
48
|
+
register_callback: callable,
|
|
49
|
+
):
|
|
50
|
+
"""Initialize hot-reload integration.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
app: FastAPI application instance
|
|
54
|
+
register_callback: Function to register wizard (wizard_id, wizard_class) -> bool
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
self.app = app
|
|
58
|
+
self.register_callback = register_callback
|
|
59
|
+
self.config = get_hot_reload_config()
|
|
60
|
+
|
|
61
|
+
# Initialize components
|
|
62
|
+
self.notification_callback = create_notification_callback()
|
|
63
|
+
self.reloader = WizardReloader(
|
|
64
|
+
register_callback=self._register_wizard_wrapper,
|
|
65
|
+
notification_callback=self.notification_callback,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.watcher: WizardFileWatcher | None = None
|
|
69
|
+
|
|
70
|
+
# Add WebSocket endpoint to app
|
|
71
|
+
if self.config.enabled:
|
|
72
|
+
self._setup_websocket_endpoint()
|
|
73
|
+
|
|
74
|
+
def _register_wizard_wrapper(self, wizard_id: str, wizard_class: type) -> bool:
|
|
75
|
+
"""Wrapper for register callback that handles errors.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
wizard_id: Wizard identifier
|
|
79
|
+
wizard_class: Wizard class to register
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if registration succeeded
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
return self.register_callback(wizard_id, wizard_class)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Error registering wizard {wizard_id}: {e}")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _setup_websocket_endpoint(self) -> None:
|
|
92
|
+
"""Add WebSocket endpoint to FastAPI app."""
|
|
93
|
+
|
|
94
|
+
@self.app.websocket(self.config.websocket_path)
|
|
95
|
+
async def hot_reload_websocket(websocket: WebSocket):
|
|
96
|
+
"""WebSocket endpoint for hot-reload notifications."""
|
|
97
|
+
manager = get_notification_manager()
|
|
98
|
+
|
|
99
|
+
await manager.connect(websocket)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Keep connection alive
|
|
103
|
+
while True:
|
|
104
|
+
# Receive ping messages
|
|
105
|
+
await websocket.receive_text()
|
|
106
|
+
|
|
107
|
+
except WebSocketDisconnect:
|
|
108
|
+
await manager.disconnect(websocket)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"WebSocket error: {e}")
|
|
111
|
+
await manager.disconnect(websocket)
|
|
112
|
+
|
|
113
|
+
logger.info(f"WebSocket endpoint added: {self.config.websocket_path}")
|
|
114
|
+
|
|
115
|
+
def start(self) -> None:
|
|
116
|
+
"""Start hot-reload watcher."""
|
|
117
|
+
if not self.config.enabled:
|
|
118
|
+
logger.info("Hot-reload disabled (set HOT_RELOAD_ENABLED=true to enable)")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if not self.config.watch_dirs:
|
|
122
|
+
logger.warning("No wizard directories found to watch")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if self.watcher and self.watcher.is_running():
|
|
126
|
+
logger.warning("Hot-reload already started")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Create watcher
|
|
130
|
+
self.watcher = WizardFileWatcher(
|
|
131
|
+
wizard_dirs=self.config.watch_dirs,
|
|
132
|
+
reload_callback=self._on_file_change,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Start watching
|
|
136
|
+
self.watcher.start()
|
|
137
|
+
|
|
138
|
+
logger.info(f"🔥 Hot-reload started - watching {len(self.config.watch_dirs)} directories")
|
|
139
|
+
|
|
140
|
+
def stop(self) -> None:
|
|
141
|
+
"""Stop hot-reload watcher."""
|
|
142
|
+
if self.watcher:
|
|
143
|
+
self.watcher.stop()
|
|
144
|
+
self.watcher = None
|
|
145
|
+
logger.info("Hot-reload stopped")
|
|
146
|
+
|
|
147
|
+
def _on_file_change(self, wizard_id: str, file_path: str) -> None:
|
|
148
|
+
"""Handle file change event.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
wizard_id: ID of wizard that changed
|
|
152
|
+
file_path: Path to changed file
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
logger.info(f"File change detected: {wizard_id} ({file_path})")
|
|
156
|
+
|
|
157
|
+
# Reload wizard
|
|
158
|
+
result = self.reloader.reload_wizard(wizard_id, file_path)
|
|
159
|
+
|
|
160
|
+
if result.success:
|
|
161
|
+
logger.info(f"✓ {result.message}")
|
|
162
|
+
else:
|
|
163
|
+
logger.error(f"✗ Reload failed: {result.error}")
|
|
164
|
+
|
|
165
|
+
def get_status(self) -> dict:
|
|
166
|
+
"""Get hot-reload status.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Status dictionary
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
return {
|
|
173
|
+
"enabled": self.config.enabled,
|
|
174
|
+
"running": self.watcher.is_running() if self.watcher else False,
|
|
175
|
+
"watch_dirs": [str(d) for d in self.config.watch_dirs],
|
|
176
|
+
"reload_count": self.reloader.get_reload_count(),
|
|
177
|
+
"websocket_connections": get_notification_manager().get_connection_count(),
|
|
178
|
+
"websocket_path": self.config.websocket_path,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Example usage in wizard_api.py:
|
|
183
|
+
"""
|
|
184
|
+
from fastapi import FastAPI
|
|
185
|
+
from hot_reload.integration import HotReloadIntegration
|
|
186
|
+
|
|
187
|
+
app = FastAPI(title="Empathy Wizard API")
|
|
188
|
+
|
|
189
|
+
# Global hot-reload instance
|
|
190
|
+
hot_reload = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def register_wizard(wizard_id: str, wizard_class: type, *args, **kwargs) -> bool:
|
|
194
|
+
'''Register wizard with WIZARDS dict'''
|
|
195
|
+
try:
|
|
196
|
+
WIZARDS[wizard_id] = wizard_class(*args, **kwargs)
|
|
197
|
+
logger.info(f"✓ Registered wizard: {wizard_id}")
|
|
198
|
+
return True
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(f"Failed to register {wizard_id}: {e}")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.on_event("startup")
|
|
205
|
+
async def startup_event():
|
|
206
|
+
global hot_reload
|
|
207
|
+
|
|
208
|
+
# Initialize wizards
|
|
209
|
+
init_wizards()
|
|
210
|
+
|
|
211
|
+
# Start hot-reload
|
|
212
|
+
hot_reload = HotReloadIntegration(app, register_wizard)
|
|
213
|
+
hot_reload.start()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.on_event("shutdown")
|
|
217
|
+
async def shutdown_event():
|
|
218
|
+
if hot_reload:
|
|
219
|
+
hot_reload.stop()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.get("/api/hot-reload/status")
|
|
223
|
+
async def get_hot_reload_status():
|
|
224
|
+
'''Get hot-reload status'''
|
|
225
|
+
if not hot_reload:
|
|
226
|
+
return {"enabled": False}
|
|
227
|
+
return hot_reload.get_status()
|
|
228
|
+
"""
|
hot_reload/reloader.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Dynamic wizard reloader for hot-reload.
|
|
2
|
+
|
|
3
|
+
Handles reloading wizard modules without server restart.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReloadResult:
|
|
20
|
+
"""Result of a wizard reload operation."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
success: bool,
|
|
25
|
+
wizard_id: str,
|
|
26
|
+
message: str,
|
|
27
|
+
error: str | None = None,
|
|
28
|
+
):
|
|
29
|
+
"""Initialize reload result.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
success: Whether reload succeeded
|
|
33
|
+
wizard_id: ID of wizard that was reloaded
|
|
34
|
+
message: Status message
|
|
35
|
+
error: Error message if failed
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
self.success = success
|
|
39
|
+
self.wizard_id = wizard_id
|
|
40
|
+
self.message = message
|
|
41
|
+
self.error = error
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Convert to dictionary."""
|
|
45
|
+
return {
|
|
46
|
+
"success": self.success,
|
|
47
|
+
"wizard_id": self.wizard_id,
|
|
48
|
+
"message": self.message,
|
|
49
|
+
"error": self.error,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WizardReloader:
|
|
54
|
+
"""Handles dynamic reloading of wizard modules.
|
|
55
|
+
|
|
56
|
+
Supports hot-reload of wizards without server restart by:
|
|
57
|
+
1. Unloading old module from sys.modules
|
|
58
|
+
2. Reloading module with importlib
|
|
59
|
+
3. Re-registering wizard with wizard API
|
|
60
|
+
4. Notifying clients via callback
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
register_callback: Callable[[str, type], bool],
|
|
66
|
+
notification_callback: Callable[[dict], None] | None = None,
|
|
67
|
+
):
|
|
68
|
+
"""Initialize reloader.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
register_callback: Function to register wizard (wizard_id, wizard_class) -> success
|
|
72
|
+
notification_callback: Optional function to notify clients of reload events
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
self.register_callback = register_callback
|
|
76
|
+
self.notification_callback = notification_callback
|
|
77
|
+
self._reload_count = 0
|
|
78
|
+
|
|
79
|
+
def reload_wizard(self, wizard_id: str, file_path: str) -> ReloadResult:
|
|
80
|
+
"""Reload a wizard module.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
wizard_id: Wizard identifier
|
|
84
|
+
file_path: Path to wizard file
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ReloadResult with outcome
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
logger.info(f"Attempting to reload wizard: {wizard_id} from {file_path}")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Get module name from file path
|
|
94
|
+
module_name = self._get_module_name(file_path)
|
|
95
|
+
if not module_name:
|
|
96
|
+
error_msg = f"Could not determine module name from {file_path}"
|
|
97
|
+
logger.error(error_msg)
|
|
98
|
+
return ReloadResult(
|
|
99
|
+
success=False,
|
|
100
|
+
wizard_id=wizard_id,
|
|
101
|
+
message="Failed to reload",
|
|
102
|
+
error=error_msg,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Unload old module
|
|
106
|
+
self._unload_module(module_name)
|
|
107
|
+
|
|
108
|
+
# Reload module
|
|
109
|
+
try:
|
|
110
|
+
module = importlib.import_module(module_name)
|
|
111
|
+
except ImportError as e:
|
|
112
|
+
error_msg = f"Failed to import module {module_name}: {e}"
|
|
113
|
+
logger.error(error_msg)
|
|
114
|
+
self._notify_reload_failed(wizard_id, error_msg)
|
|
115
|
+
return ReloadResult(
|
|
116
|
+
success=False,
|
|
117
|
+
wizard_id=wizard_id,
|
|
118
|
+
message="Import failed",
|
|
119
|
+
error=error_msg,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Find wizard class in module
|
|
123
|
+
wizard_class = self._find_wizard_class(module)
|
|
124
|
+
if not wizard_class:
|
|
125
|
+
error_msg = f"No wizard class found in {module_name}"
|
|
126
|
+
logger.error(error_msg)
|
|
127
|
+
self._notify_reload_failed(wizard_id, error_msg)
|
|
128
|
+
return ReloadResult(
|
|
129
|
+
success=False,
|
|
130
|
+
wizard_id=wizard_id,
|
|
131
|
+
message="No wizard class found",
|
|
132
|
+
error=error_msg,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Re-register wizard
|
|
136
|
+
success = self.register_callback(wizard_id, wizard_class)
|
|
137
|
+
|
|
138
|
+
if success:
|
|
139
|
+
self._reload_count += 1
|
|
140
|
+
logger.info(
|
|
141
|
+
f"✓ Successfully reloaded {wizard_id} ({self._reload_count} total reloads)"
|
|
142
|
+
)
|
|
143
|
+
self._notify_reload_success(wizard_id)
|
|
144
|
+
|
|
145
|
+
return ReloadResult(
|
|
146
|
+
success=True,
|
|
147
|
+
wizard_id=wizard_id,
|
|
148
|
+
message=f"Reloaded successfully (reload #{self._reload_count})",
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
error_msg = "Registration failed"
|
|
152
|
+
logger.error(f"Failed to re-register {wizard_id}")
|
|
153
|
+
self._notify_reload_failed(wizard_id, error_msg)
|
|
154
|
+
return ReloadResult(
|
|
155
|
+
success=False,
|
|
156
|
+
wizard_id=wizard_id,
|
|
157
|
+
message="Registration failed",
|
|
158
|
+
error=error_msg,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
error_msg = f"Unexpected error reloading {wizard_id}: {e}"
|
|
163
|
+
logger.exception(error_msg)
|
|
164
|
+
self._notify_reload_failed(wizard_id, str(e))
|
|
165
|
+
return ReloadResult(
|
|
166
|
+
success=False,
|
|
167
|
+
wizard_id=wizard_id,
|
|
168
|
+
message="Unexpected error",
|
|
169
|
+
error=str(e),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _get_module_name(self, file_path: str) -> str | None:
|
|
173
|
+
"""Get Python module name from file path.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
file_path: Path to Python file
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Module name or None if cannot determine
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
path = Path(file_path).resolve()
|
|
184
|
+
|
|
185
|
+
# Remove .py extension
|
|
186
|
+
if not path.suffix == ".py":
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# Get parts relative to project root
|
|
190
|
+
# Try to find common patterns: wizards/, coach_wizards/, empathy_software_plugin/wizards/
|
|
191
|
+
parts = path.parts
|
|
192
|
+
|
|
193
|
+
# Find wizard directory in path
|
|
194
|
+
wizard_dir_indices = [i for i, part in enumerate(parts) if "wizard" in part.lower()]
|
|
195
|
+
|
|
196
|
+
if not wizard_dir_indices:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Take from first wizard directory
|
|
200
|
+
start_idx = wizard_dir_indices[0]
|
|
201
|
+
|
|
202
|
+
# Build module name
|
|
203
|
+
module_parts = list(parts[start_idx:])
|
|
204
|
+
module_parts[-1] = module_parts[-1].replace(".py", "")
|
|
205
|
+
|
|
206
|
+
module_name = ".".join(module_parts)
|
|
207
|
+
return module_name
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error getting module name from {file_path}: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _unload_module(self, module_name: str) -> None:
|
|
214
|
+
"""Unload module from sys.modules.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
module_name: Name of module to unload
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
# Unload exact module
|
|
221
|
+
if module_name in sys.modules:
|
|
222
|
+
del sys.modules[module_name]
|
|
223
|
+
logger.debug(f"Unloaded module: {module_name}")
|
|
224
|
+
|
|
225
|
+
# Also unload any submodules
|
|
226
|
+
submodules = [name for name in sys.modules.keys() if name.startswith(f"{module_name}.")]
|
|
227
|
+
for submodule in submodules:
|
|
228
|
+
del sys.modules[submodule]
|
|
229
|
+
logger.debug(f"Unloaded submodule: {submodule}")
|
|
230
|
+
|
|
231
|
+
def _find_wizard_class(self, module: Any) -> type | None:
|
|
232
|
+
"""Find wizard class in module.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
module: Python module
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Wizard class or None if not found
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
# Look for classes ending with "Wizard"
|
|
242
|
+
for name in dir(module):
|
|
243
|
+
if name.endswith("Wizard") and not name.startswith("_"):
|
|
244
|
+
attr = getattr(module, name)
|
|
245
|
+
if isinstance(attr, type):
|
|
246
|
+
return attr
|
|
247
|
+
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def _notify_reload_success(self, wizard_id: str) -> None:
|
|
251
|
+
"""Notify clients of successful reload.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
wizard_id: ID of reloaded wizard
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
if self.notification_callback:
|
|
258
|
+
try:
|
|
259
|
+
self.notification_callback(
|
|
260
|
+
{
|
|
261
|
+
"event": "wizard_reloaded",
|
|
262
|
+
"wizard_id": wizard_id,
|
|
263
|
+
"success": True,
|
|
264
|
+
"reload_count": self._reload_count,
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Error sending reload notification: {e}")
|
|
269
|
+
|
|
270
|
+
def _notify_reload_failed(self, wizard_id: str, error: str) -> None:
|
|
271
|
+
"""Notify clients of failed reload.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
wizard_id: ID of wizard that failed to reload
|
|
275
|
+
error: Error message
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
if self.notification_callback:
|
|
279
|
+
try:
|
|
280
|
+
self.notification_callback(
|
|
281
|
+
{
|
|
282
|
+
"event": "wizard_reload_failed",
|
|
283
|
+
"wizard_id": wizard_id,
|
|
284
|
+
"success": False,
|
|
285
|
+
"error": error,
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Error sending failure notification: {e}")
|
|
290
|
+
|
|
291
|
+
def get_reload_count(self) -> int:
|
|
292
|
+
"""Get total number of successful reloads.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Reload count
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
return self._reload_count
|
hot_reload/watcher.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""File system watcher for wizard hot-reload.
|
|
2
|
+
|
|
3
|
+
Monitors wizard directories for changes and triggers reloads.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
14
|
+
from watchdog.observers import Observer
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WizardFileHandler(FileSystemEventHandler):
|
|
20
|
+
"""Handles file system events for wizard files."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, reload_callback: Callable[[str], None]):
|
|
23
|
+
"""Initialize handler.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
reload_callback: Function to call when wizard file changes
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.reload_callback = reload_callback
|
|
31
|
+
self._processing = set() # Prevent duplicate events
|
|
32
|
+
|
|
33
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
34
|
+
"""Handle file modification events.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
event: File system event
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
if event.is_directory:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
file_path = event.src_path
|
|
44
|
+
|
|
45
|
+
# Only process Python files
|
|
46
|
+
if not file_path.endswith(".py"):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Skip __pycache__ and test files
|
|
50
|
+
if "__pycache__" in file_path or "test_" in file_path:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Prevent duplicate processing
|
|
54
|
+
if file_path in self._processing:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
self._processing.add(file_path)
|
|
59
|
+
|
|
60
|
+
wizard_id = self._extract_wizard_id(file_path)
|
|
61
|
+
if wizard_id:
|
|
62
|
+
logger.info(f"Detected change in {wizard_id} ({file_path})")
|
|
63
|
+
self.reload_callback(wizard_id, file_path)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error processing file change {file_path}: {e}")
|
|
67
|
+
finally:
|
|
68
|
+
self._processing.discard(file_path)
|
|
69
|
+
|
|
70
|
+
def _extract_wizard_id(self, file_path: str) -> str | None:
|
|
71
|
+
"""Extract wizard ID from file path.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
file_path: Path to wizard file
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Wizard ID or None if cannot extract
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
path = Path(file_path)
|
|
81
|
+
|
|
82
|
+
# Get filename without extension
|
|
83
|
+
filename = path.stem
|
|
84
|
+
|
|
85
|
+
# Remove common suffixes
|
|
86
|
+
wizard_id = filename.replace("_wizard", "").replace("wizard_", "")
|
|
87
|
+
|
|
88
|
+
# Convert to wizard ID format (snake_case)
|
|
89
|
+
wizard_id = wizard_id.lower()
|
|
90
|
+
|
|
91
|
+
return wizard_id if wizard_id else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WizardFileWatcher:
|
|
95
|
+
"""Watches wizard directories for file changes.
|
|
96
|
+
|
|
97
|
+
Monitors specified directories and triggers reload callbacks
|
|
98
|
+
when wizard files are modified.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, wizard_dirs: list[Path], reload_callback: Callable[[str, str], None]):
|
|
102
|
+
"""Initialize watcher.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
wizard_dirs: List of directories to watch
|
|
106
|
+
reload_callback: Function to call on file changes (wizard_id, file_path)
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
self.wizard_dirs = [Path(d) for d in wizard_dirs]
|
|
110
|
+
self.reload_callback = reload_callback
|
|
111
|
+
self.observer = Observer()
|
|
112
|
+
self.event_handler = WizardFileHandler(reload_callback)
|
|
113
|
+
self._running = False
|
|
114
|
+
|
|
115
|
+
def start(self) -> None:
|
|
116
|
+
"""Start watching wizard directories."""
|
|
117
|
+
if self._running:
|
|
118
|
+
logger.warning("Watcher already running")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
valid_dirs = []
|
|
122
|
+
for directory in self.wizard_dirs:
|
|
123
|
+
if not directory.exists():
|
|
124
|
+
logger.warning(f"Directory does not exist: {directory}")
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
if not directory.is_dir():
|
|
128
|
+
logger.warning(f"Not a directory: {directory}")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Schedule watching
|
|
132
|
+
self.observer.schedule(
|
|
133
|
+
self.event_handler,
|
|
134
|
+
str(directory),
|
|
135
|
+
recursive=True,
|
|
136
|
+
)
|
|
137
|
+
valid_dirs.append(directory)
|
|
138
|
+
logger.info(f"Watching directory: {directory}")
|
|
139
|
+
|
|
140
|
+
if not valid_dirs:
|
|
141
|
+
logger.error("No valid directories to watch")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
self.observer.start()
|
|
145
|
+
self._running = True
|
|
146
|
+
|
|
147
|
+
logger.info(
|
|
148
|
+
f"Hot-reload enabled for {len(valid_dirs)} "
|
|
149
|
+
f"{'directory' if len(valid_dirs) == 1 else 'directories'}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def stop(self) -> None:
|
|
153
|
+
"""Stop watching wizard directories."""
|
|
154
|
+
if not self._running:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
self.observer.stop()
|
|
158
|
+
self.observer.join(timeout=5.0)
|
|
159
|
+
self._running = False
|
|
160
|
+
|
|
161
|
+
logger.info("Hot-reload watcher stopped")
|
|
162
|
+
|
|
163
|
+
def is_running(self) -> bool:
|
|
164
|
+
"""Check if watcher is running.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if watching, False otherwise
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
return self._running
|
|
171
|
+
|
|
172
|
+
def __enter__(self):
|
|
173
|
+
"""Context manager entry."""
|
|
174
|
+
self.start()
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
178
|
+
"""Context manager exit."""
|
|
179
|
+
self.stop()
|