kailash 0.2.2__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +40 -39
  3. kailash/api/auth.py +26 -32
  4. kailash/api/custom_nodes.py +29 -29
  5. kailash/api/custom_nodes_secure.py +35 -35
  6. kailash/api/database.py +17 -17
  7. kailash/api/gateway.py +19 -19
  8. kailash/api/mcp_integration.py +24 -23
  9. kailash/api/studio.py +45 -45
  10. kailash/api/workflow_api.py +8 -8
  11. kailash/cli/commands.py +5 -8
  12. kailash/manifest.py +42 -42
  13. kailash/mcp/__init__.py +1 -1
  14. kailash/mcp/ai_registry_server.py +20 -20
  15. kailash/mcp/client.py +9 -11
  16. kailash/mcp/client_new.py +10 -10
  17. kailash/mcp/server.py +1 -2
  18. kailash/mcp/server_enhanced.py +449 -0
  19. kailash/mcp/servers/ai_registry.py +6 -6
  20. kailash/mcp/utils/__init__.py +31 -0
  21. kailash/mcp/utils/cache.py +267 -0
  22. kailash/mcp/utils/config.py +263 -0
  23. kailash/mcp/utils/formatters.py +293 -0
  24. kailash/mcp/utils/metrics.py +418 -0
  25. kailash/nodes/ai/agents.py +9 -9
  26. kailash/nodes/ai/ai_providers.py +33 -34
  27. kailash/nodes/ai/embedding_generator.py +31 -32
  28. kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
  29. kailash/nodes/ai/iterative_llm_agent.py +48 -48
  30. kailash/nodes/ai/llm_agent.py +32 -33
  31. kailash/nodes/ai/models.py +13 -13
  32. kailash/nodes/ai/self_organizing.py +44 -44
  33. kailash/nodes/api/__init__.py +5 -0
  34. kailash/nodes/api/auth.py +11 -11
  35. kailash/nodes/api/graphql.py +13 -13
  36. kailash/nodes/api/http.py +19 -19
  37. kailash/nodes/api/monitoring.py +463 -0
  38. kailash/nodes/api/rate_limiting.py +9 -13
  39. kailash/nodes/api/rest.py +29 -29
  40. kailash/nodes/api/security.py +819 -0
  41. kailash/nodes/base.py +24 -26
  42. kailash/nodes/base_async.py +7 -7
  43. kailash/nodes/base_cycle_aware.py +12 -12
  44. kailash/nodes/base_with_acl.py +5 -5
  45. kailash/nodes/code/python.py +56 -55
  46. kailash/nodes/data/__init__.py +6 -0
  47. kailash/nodes/data/directory.py +6 -6
  48. kailash/nodes/data/event_generation.py +297 -0
  49. kailash/nodes/data/file_discovery.py +598 -0
  50. kailash/nodes/data/readers.py +8 -8
  51. kailash/nodes/data/retrieval.py +10 -10
  52. kailash/nodes/data/sharepoint_graph.py +17 -17
  53. kailash/nodes/data/sources.py +5 -5
  54. kailash/nodes/data/sql.py +13 -13
  55. kailash/nodes/data/streaming.py +25 -25
  56. kailash/nodes/data/vector_db.py +22 -22
  57. kailash/nodes/data/writers.py +7 -7
  58. kailash/nodes/logic/async_operations.py +17 -17
  59. kailash/nodes/logic/convergence.py +11 -11
  60. kailash/nodes/logic/loop.py +4 -4
  61. kailash/nodes/logic/operations.py +11 -11
  62. kailash/nodes/logic/workflow.py +8 -9
  63. kailash/nodes/mixins/mcp.py +17 -17
  64. kailash/nodes/mixins.py +8 -10
  65. kailash/nodes/transform/chunkers.py +3 -3
  66. kailash/nodes/transform/formatters.py +7 -7
  67. kailash/nodes/transform/processors.py +11 -11
  68. kailash/runtime/access_controlled.py +18 -18
  69. kailash/runtime/async_local.py +18 -20
  70. kailash/runtime/docker.py +24 -26
  71. kailash/runtime/local.py +55 -31
  72. kailash/runtime/parallel.py +25 -25
  73. kailash/runtime/parallel_cyclic.py +29 -29
  74. kailash/runtime/runner.py +6 -6
  75. kailash/runtime/testing.py +22 -22
  76. kailash/sdk_exceptions.py +0 -58
  77. kailash/security.py +14 -26
  78. kailash/tracking/manager.py +38 -38
  79. kailash/tracking/metrics_collector.py +15 -14
  80. kailash/tracking/models.py +53 -53
  81. kailash/tracking/storage/base.py +7 -17
  82. kailash/tracking/storage/database.py +22 -23
  83. kailash/tracking/storage/filesystem.py +38 -40
  84. kailash/utils/export.py +21 -21
  85. kailash/utils/templates.py +8 -9
  86. kailash/visualization/api.py +30 -34
  87. kailash/visualization/dashboard.py +17 -17
  88. kailash/visualization/performance.py +32 -19
  89. kailash/visualization/reports.py +30 -28
  90. kailash/workflow/builder.py +8 -8
  91. kailash/workflow/convergence.py +13 -12
  92. kailash/workflow/cycle_analyzer.py +38 -33
  93. kailash/workflow/cycle_builder.py +12 -12
  94. kailash/workflow/cycle_config.py +16 -15
  95. kailash/workflow/cycle_debugger.py +40 -40
  96. kailash/workflow/cycle_exceptions.py +29 -29
  97. kailash/workflow/cycle_profiler.py +21 -21
  98. kailash/workflow/cycle_state.py +20 -22
  99. kailash/workflow/cyclic_runner.py +45 -45
  100. kailash/workflow/graph.py +57 -45
  101. kailash/workflow/mermaid_visualizer.py +9 -11
  102. kailash/workflow/migration.py +22 -22
  103. kailash/workflow/mock_registry.py +6 -6
  104. kailash/workflow/runner.py +9 -9
  105. kailash/workflow/safety.py +12 -13
  106. kailash/workflow/state.py +8 -11
  107. kailash/workflow/templates.py +19 -19
  108. kailash/workflow/validation.py +14 -14
  109. kailash/workflow/visualization.py +32 -24
  110. kailash-0.3.1.dist-info/METADATA +476 -0
  111. kailash-0.3.1.dist-info/RECORD +136 -0
  112. kailash-0.2.2.dist-info/METADATA +0 -121
  113. kailash-0.2.2.dist-info/RECORD +0 -126
  114. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/WHEEL +0 -0
  115. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/licenses/LICENSE +0 -0
  117. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/top_level.txt +0 -0
@@ -27,7 +27,7 @@ import json
27
27
  import logging
28
28
  from datetime import datetime
29
29
  from pathlib import Path
30
- from typing import Any, Dict, List, Optional, Union
30
+ from typing import Any, Optional
31
31
 
32
32
  try:
33
33
  from fastapi import (
@@ -59,8 +59,8 @@ if FASTAPI_AVAILABLE:
59
59
  class RunRequest(BaseModel):
60
60
  """Request model for starting monitoring."""
61
61
 
62
- run_id: Optional[str] = None
63
- config: Optional[Dict[str, Any]] = None
62
+ run_id: str | None = None
63
+ config: dict[str, Any] | None = None
64
64
 
65
65
  class MetricsResponse(BaseModel):
66
66
  """Response model for metrics data."""
@@ -80,12 +80,12 @@ if FASTAPI_AVAILABLE:
80
80
  node_id: str
81
81
  node_type: str
82
82
  status: str
83
- started_at: Optional[datetime]
84
- ended_at: Optional[datetime]
85
- duration: Optional[float]
86
- cpu_usage: Optional[float]
87
- memory_usage_mb: Optional[float]
88
- error_message: Optional[str]
83
+ started_at: datetime | None
84
+ ended_at: datetime | None
85
+ duration: float | None
86
+ cpu_usage: float | None
87
+ memory_usage_mb: float | None
88
+ error_message: str | None
89
89
 
90
90
  class RunResponse(BaseModel):
91
91
  """Response model for run information."""
@@ -93,8 +93,8 @@ if FASTAPI_AVAILABLE:
93
93
  run_id: str
94
94
  workflow_name: str
95
95
  status: str
96
- started_at: Optional[datetime]
97
- ended_at: Optional[datetime]
96
+ started_at: datetime | None
97
+ ended_at: datetime | None
98
98
  total_tasks: int
99
99
  completed_tasks: int
100
100
  failed_tasks: int
@@ -105,7 +105,7 @@ if FASTAPI_AVAILABLE:
105
105
  run_id: str
106
106
  format: str = "html"
107
107
  include_charts: bool = True
108
- compare_runs: Optional[List[str]] = None
108
+ compare_runs: list[str] | None = None
109
109
  detail_level: str = "detailed"
110
110
 
111
111
 
@@ -123,7 +123,7 @@ class DashboardAPIServer:
123
123
  def __init__(
124
124
  self,
125
125
  task_manager: TaskManager,
126
- dashboard_config: Optional[DashboardConfig] = None,
126
+ dashboard_config: DashboardConfig | None = None,
127
127
  ):
128
128
  """Initialize API server.
129
129
 
@@ -145,8 +145,8 @@ class DashboardAPIServer:
145
145
  self.reporter = WorkflowPerformanceReporter(task_manager)
146
146
 
147
147
  # WebSocket connections for real-time updates
148
- self._websocket_connections: List[WebSocket] = []
149
- self._broadcast_task: Optional[asyncio.Task] = None
148
+ self._websocket_connections: list[WebSocket] = []
149
+ self._broadcast_task: asyncio.Task | None = None
150
150
 
151
151
  # Create FastAPI app
152
152
  self.app = FastAPI(
@@ -177,7 +177,7 @@ class DashboardAPIServer:
177
177
  """Health check endpoint."""
178
178
  return {"status": "healthy", "timestamp": datetime.now()}
179
179
 
180
- @self.app.get("/api/v1/runs", response_model=List[RunResponse])
180
+ @self.app.get("/api/v1/runs", response_model=list[RunResponse])
181
181
  async def list_runs(limit: int = 10, offset: int = 0):
182
182
  """Get list of workflow runs."""
183
183
  try:
@@ -243,7 +243,7 @@ class DashboardAPIServer:
243
243
  self.logger.error(f"Failed to get run {run_id}: {e}")
244
244
  raise HTTPException(status_code=500, detail=str(e))
245
245
 
246
- @self.app.get("/api/v1/runs/{run_id}/tasks", response_model=List[TaskResponse])
246
+ @self.app.get("/api/v1/runs/{run_id}/tasks", response_model=list[TaskResponse])
247
247
  async def get_run_tasks(run_id: str):
248
248
  """Get tasks for a specific run."""
249
249
  try:
@@ -352,7 +352,7 @@ class DashboardAPIServer:
352
352
  self.logger.error(f"Failed to get current metrics: {e}")
353
353
  raise HTTPException(status_code=500, detail=str(e))
354
354
 
355
- @self.app.get("/api/v1/metrics/history", response_model=List[MetricsResponse])
355
+ @self.app.get("/api/v1/metrics/history", response_model=list[MetricsResponse])
356
356
  async def get_metrics_history(minutes: int = 30):
357
357
  """Get metrics history for specified time period."""
358
358
  try:
@@ -475,7 +475,7 @@ class DashboardAPIServer:
475
475
  run_id: str,
476
476
  output_path: Path,
477
477
  report_format: ReportFormat,
478
- compare_runs: Optional[List[str]] = None,
478
+ compare_runs: list[str] | None = None,
479
479
  ):
480
480
  """Generate report in background task."""
481
481
  try:
@@ -564,7 +564,7 @@ class SimpleDashboardAPI:
564
564
  def __init__(
565
565
  self,
566
566
  task_manager: TaskManager,
567
- dashboard_config: Optional[DashboardConfig] = None,
567
+ dashboard_config: DashboardConfig | None = None,
568
568
  ):
569
569
  """Initialize simple API interface.
570
570
 
@@ -578,7 +578,7 @@ class SimpleDashboardAPI:
578
578
  self.reporter = WorkflowPerformanceReporter(task_manager)
579
579
  self.logger = logger
580
580
 
581
- def get_runs(self, limit: int = 10, offset: int = 0) -> List[Dict[str, Any]]:
581
+ def get_runs(self, limit: int = 10, offset: int = 0) -> list[dict[str, Any]]:
582
582
  """Get list of workflow runs."""
583
583
  all_runs = self.task_manager.list_runs()
584
584
  runs = all_runs[offset : offset + limit]
@@ -604,7 +604,7 @@ class SimpleDashboardAPI:
604
604
 
605
605
  return result
606
606
 
607
- def get_run_details(self, run_id: str) -> Optional[Dict[str, Any]]:
607
+ def get_run_details(self, run_id: str) -> dict[str, Any] | None:
608
608
  """Get details for a specific run."""
609
609
  run = self.task_manager.get_run(run_id)
610
610
  if not run:
@@ -641,17 +641,17 @@ class SimpleDashboardAPI:
641
641
  ],
642
642
  }
643
643
 
644
- def start_monitoring(self, run_id: Optional[str] = None) -> Dict[str, Any]:
644
+ def start_monitoring(self, run_id: str | None = None) -> dict[str, Any]:
645
645
  """Start real-time monitoring."""
646
646
  self.dashboard.start_monitoring(run_id)
647
647
  return {"status": "started", "run_id": run_id}
648
648
 
649
- def stop_monitoring(self) -> Dict[str, Any]:
649
+ def stop_monitoring(self) -> dict[str, Any]:
650
650
  """Stop real-time monitoring."""
651
651
  self.dashboard.stop_monitoring()
652
652
  return {"status": "stopped"}
653
653
 
654
- def get_current_metrics(self) -> Optional[Dict[str, Any]]:
654
+ def get_current_metrics(self) -> dict[str, Any] | None:
655
655
  """Get current live metrics."""
656
656
  metrics = self.dashboard.get_current_metrics()
657
657
  if not metrics:
@@ -668,7 +668,7 @@ class SimpleDashboardAPI:
668
668
  "avg_task_duration": metrics.avg_task_duration,
669
669
  }
670
670
 
671
- def get_metrics_history(self, minutes: int = 30) -> List[Dict[str, Any]]:
671
+ def get_metrics_history(self, minutes: int = 30) -> list[dict[str, Any]]:
672
672
  """Get metrics history."""
673
673
  history = self.dashboard.get_metrics_history(minutes=minutes)
674
674
 
@@ -690,8 +690,8 @@ class SimpleDashboardAPI:
690
690
  self,
691
691
  run_id: str,
692
692
  format: str = "html",
693
- output_path: Optional[Union[str, Path]] = None,
694
- compare_runs: Optional[List[str]] = None,
693
+ output_path: str | Path | None = None,
694
+ compare_runs: list[str] | None = None,
695
695
  ) -> Path:
696
696
  """Generate performance report."""
697
697
  try:
@@ -708,9 +708,7 @@ class SimpleDashboardAPI:
708
708
  compare_runs=compare_runs,
709
709
  )
710
710
 
711
- def generate_dashboard(
712
- self, output_path: Optional[Union[str, Path]] = None
713
- ) -> Path:
711
+ def generate_dashboard(self, output_path: str | Path | None = None) -> Path:
714
712
  """Generate live dashboard HTML."""
715
713
  if output_path is None:
716
714
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -718,9 +716,7 @@ class SimpleDashboardAPI:
718
716
 
719
717
  return self.dashboard.generate_live_report(output_path, include_charts=True)
720
718
 
721
- def export_metrics_json(
722
- self, output_path: Optional[Union[str, Path]] = None
723
- ) -> Path:
719
+ def export_metrics_json(self, output_path: str | Path | None = None) -> Path:
724
720
  """Export current metrics as JSON."""
725
721
  if output_path is None:
726
722
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -27,7 +27,7 @@ import time
27
27
  from dataclasses import dataclass, field
28
28
  from datetime import datetime, timedelta
29
29
  from pathlib import Path
30
- from typing import Any, Dict, List, Optional, Union
30
+ from typing import Any
31
31
 
32
32
  import numpy as np
33
33
 
@@ -100,7 +100,7 @@ class RealTimeDashboard:
100
100
  """
101
101
 
102
102
  def __init__(
103
- self, task_manager: TaskManager, config: Optional[DashboardConfig] = None
103
+ self, task_manager: TaskManager, config: DashboardConfig | None = None
104
104
  ):
105
105
  """Initialize real-time dashboard.
106
106
 
@@ -114,17 +114,17 @@ class RealTimeDashboard:
114
114
 
115
115
  # Live monitoring state
116
116
  self._monitoring = False
117
- self._monitor_thread: Optional[threading.Thread] = None
118
- self._metrics_history: List[LiveMetrics] = []
119
- self._current_run_id: Optional[str] = None
117
+ self._monitor_thread: threading.Thread | None = None
118
+ self._metrics_history: list[LiveMetrics] = []
119
+ self._current_run_id: str | None = None
120
120
 
121
121
  # Event callbacks
122
- self._status_callbacks: List[callable] = []
123
- self._metrics_callbacks: List[callable] = []
122
+ self._status_callbacks: list[callable] = []
123
+ self._metrics_callbacks: list[callable] = []
124
124
 
125
125
  self.logger = logger
126
126
 
127
- def start_monitoring(self, run_id: Optional[str] = None):
127
+ def start_monitoring(self, run_id: str | None = None):
128
128
  """Start real-time monitoring for a workflow run.
129
129
 
130
130
  Args:
@@ -297,11 +297,11 @@ class RealTimeDashboard:
297
297
  """
298
298
  self._status_callbacks.append(callback)
299
299
 
300
- def get_current_metrics(self) -> Optional[LiveMetrics]:
300
+ def get_current_metrics(self) -> LiveMetrics | None:
301
301
  """Get the most recent metrics."""
302
302
  return self._metrics_history[-1] if self._metrics_history else None
303
303
 
304
- def get_metrics_history(self, minutes: Optional[int] = None) -> List[LiveMetrics]:
304
+ def get_metrics_history(self, minutes: int | None = None) -> list[LiveMetrics]:
305
305
  """Get metrics history for specified time period.
306
306
 
307
307
  Args:
@@ -317,7 +317,7 @@ class RealTimeDashboard:
317
317
  return [m for m in self._metrics_history if m.timestamp >= cutoff]
318
318
 
319
319
  def generate_live_report(
320
- self, output_path: Union[str, Path], include_charts: bool = True
320
+ self, output_path: str | Path, include_charts: bool = True
321
321
  ) -> Path:
322
322
  """Generate comprehensive live dashboard report.
323
323
 
@@ -396,7 +396,7 @@ class RealTimeDashboard:
396
396
 
397
397
  return html_template
398
398
 
399
- def _generate_status_section(self, metrics: Optional[LiveMetrics]) -> str:
399
+ def _generate_status_section(self, metrics: LiveMetrics | None) -> str:
400
400
  """Generate status overview section."""
401
401
  if not metrics:
402
402
  return """
@@ -442,7 +442,7 @@ class RealTimeDashboard:
442
442
  </section>
443
443
  """
444
444
 
445
- def _generate_live_metrics_section(self, history: List[LiveMetrics]) -> str:
445
+ def _generate_live_metrics_section(self, history: list[LiveMetrics]) -> str:
446
446
  """Generate live metrics charts section."""
447
447
  if not history:
448
448
  return """
@@ -860,7 +860,7 @@ class DashboardExporter:
860
860
  self.dashboard = dashboard
861
861
  self.logger = logger
862
862
 
863
- def export_metrics_json(self, output_path: Union[str, Path]) -> Path:
863
+ def export_metrics_json(self, output_path: str | Path) -> Path:
864
864
  """Export current metrics as JSON.
865
865
 
866
866
  Args:
@@ -895,7 +895,7 @@ class DashboardExporter:
895
895
  self.logger.info(f"Exported metrics to: {output_path}")
896
896
  return output_path
897
897
 
898
- def _metrics_to_dict(self, metrics: LiveMetrics) -> Dict[str, Any]:
898
+ def _metrics_to_dict(self, metrics: LiveMetrics) -> dict[str, Any]:
899
899
  """Convert LiveMetrics to dictionary."""
900
900
  return {
901
901
  "timestamp": metrics.timestamp.isoformat(),
@@ -909,8 +909,8 @@ class DashboardExporter:
909
909
  }
910
910
 
911
911
  def create_dashboard_snapshot(
912
- self, output_dir: Union[str, Path], include_static_charts: bool = True
913
- ) -> Dict[str, Path]:
912
+ self, output_dir: str | Path, include_static_charts: bool = True
913
+ ) -> dict[str, Path]:
914
914
  """Create complete dashboard snapshot with all assets.
915
915
 
916
916
  Args:
@@ -23,7 +23,7 @@ Downstream Consumers:
23
23
 
24
24
  import logging
25
25
  from pathlib import Path
26
- from typing import Any, Dict, List, Optional
26
+ from typing import Any
27
27
 
28
28
  import matplotlib.pyplot as plt
29
29
  import numpy as np
@@ -51,8 +51,8 @@ class PerformanceVisualizer:
51
51
  self.logger = logger
52
52
 
53
53
  def create_run_performance_summary(
54
- self, run_id: str, output_dir: Optional[Path] = None
55
- ) -> Dict[str, Path]:
54
+ self, run_id: str, output_dir: Path | None = None
55
+ ) -> dict[str, Path]:
56
56
  """Create comprehensive performance summary for a workflow run.
57
57
 
58
58
  Args:
@@ -63,8 +63,12 @@ class PerformanceVisualizer:
63
63
  Dictionary mapping chart names to file paths
64
64
  """
65
65
  if output_dir is None:
66
- # Use relative path that works from project root or create in current directory
67
- output_dir = Path.cwd() / "outputs" / "performance"
66
+ # Use centralized output directory
67
+ # Get project root and use data/outputs/visualizations/performance
68
+ project_root = Path(__file__).parent.parent.parent.parent
69
+ output_dir = (
70
+ project_root / "data" / "outputs" / "visualizations" / "performance"
71
+ )
68
72
  output_dir.mkdir(parents=True, exist_ok=True)
69
73
 
70
74
  # Get run data
@@ -108,7 +112,7 @@ class PerformanceVisualizer:
108
112
  return outputs
109
113
 
110
114
  def _create_execution_timeline(
111
- self, tasks: List[TaskRun], output_path: Path
115
+ self, tasks: list[TaskRun], output_path: Path
112
116
  ) -> Path:
113
117
  """Create Gantt-style execution timeline."""
114
118
  fig, ax = plt.subplots(figsize=(12, max(6, len(tasks) * 0.5)))
@@ -206,7 +210,7 @@ class PerformanceVisualizer:
206
210
  return output_path
207
211
 
208
212
  def _create_resource_usage_chart(
209
- self, tasks: List[TaskRun], output_path: Path
213
+ self, tasks: list[TaskRun], output_path: Path
210
214
  ) -> Path:
211
215
  """Create resource usage comparison chart."""
212
216
  fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12))
@@ -254,7 +258,7 @@ class PerformanceVisualizer:
254
258
  ax1.grid(True, axis="y", alpha=0.3)
255
259
 
256
260
  # Add value labels
257
- for bar, value in zip(bars1, cpu_usage):
261
+ for bar, value in zip(bars1, cpu_usage, strict=False):
258
262
  if value > 0:
259
263
  ax1.text(
260
264
  bar.get_x() + bar.get_width() / 2,
@@ -291,7 +295,7 @@ class PerformanceVisualizer:
291
295
  ax3.grid(True, axis="y", alpha=0.3)
292
296
 
293
297
  # Add value labels
294
- for bar, value in zip(bars3, durations):
298
+ for bar, value in zip(bars3, durations, strict=False):
295
299
  if value > 0:
296
300
  ax3.text(
297
301
  bar.get_x() + bar.get_width() / 2,
@@ -310,7 +314,7 @@ class PerformanceVisualizer:
310
314
  return output_path
311
315
 
312
316
  def _create_node_performance_comparison(
313
- self, tasks: List[TaskRun], output_path: Path
317
+ self, tasks: list[TaskRun], output_path: Path
314
318
  ) -> Path:
315
319
  """Create performance comparison radar chart."""
316
320
  # Group tasks by node type
@@ -381,7 +385,9 @@ class PerformanceVisualizer:
381
385
 
382
386
  colors = plt.cm.tab10(np.linspace(0, 1, len(avg_metrics)))
383
387
 
384
- for (node_type, metrics), color in zip(avg_metrics.items(), colors):
388
+ for (node_type, metrics), color in zip(
389
+ avg_metrics.items(), colors, strict=False
390
+ ):
385
391
  values = list(metrics.values())
386
392
 
387
393
  # Normalize values to 0-100 scale for better visualization
@@ -400,7 +406,7 @@ class PerformanceVisualizer:
400
406
  }
401
407
 
402
408
  normalized_values = []
403
- for cat, val in zip(categories, values):
409
+ for cat, val in zip(categories, values, strict=False):
404
410
  normalized_values.append((val / max_vals[cat]) * 100)
405
411
 
406
412
  normalized_values += normalized_values[:1]
@@ -431,7 +437,7 @@ class PerformanceVisualizer:
431
437
 
432
438
  return output_path
433
439
 
434
- def _create_io_analysis(self, tasks: List[TaskRun], output_path: Path) -> Path:
440
+ def _create_io_analysis(self, tasks: list[TaskRun], output_path: Path) -> Path:
435
441
  """Create I/O operations analysis chart."""
436
442
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
437
443
 
@@ -539,7 +545,7 @@ class PerformanceVisualizer:
539
545
  return output_path
540
546
 
541
547
  def _create_performance_heatmap(
542
- self, tasks: List[TaskRun], output_path: Path
548
+ self, tasks: list[TaskRun], output_path: Path
543
549
  ) -> Path:
544
550
  """Create performance metrics heatmap."""
545
551
  # Prepare data matrix
@@ -632,7 +638,7 @@ class PerformanceVisualizer:
632
638
  return output_path
633
639
 
634
640
  def _create_performance_report(
635
- self, run: Any, tasks: List[TaskRun], output_path: Path
641
+ self, run: Any, tasks: list[TaskRun], output_path: Path
636
642
  ) -> Path:
637
643
  """Create markdown performance report."""
638
644
  lines = []
@@ -714,12 +720,19 @@ class PerformanceVisualizer:
714
720
 
715
721
  return output_path
716
722
 
717
- def compare_runs(
718
- self, run_ids: List[str], output_path: Optional[Path] = None
719
- ) -> Path:
723
+ def compare_runs(self, run_ids: list[str], output_path: Path | None = None) -> Path:
720
724
  """Compare performance across multiple runs."""
721
725
  if output_path is None:
722
- output_path = Path.cwd() / "outputs" / "performance" / "comparison.png"
726
+ # Use centralized output directory
727
+ project_root = Path(__file__).parent.parent.parent.parent
728
+ output_path = (
729
+ project_root
730
+ / "data"
731
+ / "outputs"
732
+ / "visualizations"
733
+ / "performance"
734
+ / "comparison.png"
735
+ )
723
736
  output_path.parent.mkdir(parents=True, exist_ok=True)
724
737
 
725
738
  fig, axes = plt.subplots(2, 2, figsize=(15, 12))
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
27
27
  from datetime import datetime
28
28
  from enum import Enum
29
29
  from pathlib import Path
30
- from typing import Any, Dict, List, Optional, Union
30
+ from typing import Any
31
31
 
32
32
  import numpy as np
33
33
 
@@ -86,7 +86,7 @@ class PerformanceInsight:
86
86
  title: str
87
87
  description: str
88
88
  recommendation: str
89
- metrics: Dict[str, Any] = field(default_factory=dict)
89
+ metrics: dict[str, Any] = field(default_factory=dict)
90
90
 
91
91
 
92
92
  @dataclass
@@ -134,9 +134,7 @@ class WorkflowPerformanceReporter:
134
134
  report = reporter.generate_report(run_id, output_path="report.html")
135
135
  """
136
136
 
137
- def __init__(
138
- self, task_manager: TaskManager, config: Optional[ReportConfig] = None
139
- ):
137
+ def __init__(self, task_manager: TaskManager, config: ReportConfig | None = None):
140
138
  """Initialize performance reporter.
141
139
 
142
140
  Args:
@@ -151,9 +149,9 @@ class WorkflowPerformanceReporter:
151
149
  def generate_report(
152
150
  self,
153
151
  run_id: str,
154
- output_path: Optional[Union[str, Path]] = None,
152
+ output_path: str | Path | None = None,
155
153
  format: ReportFormat = ReportFormat.HTML,
156
- compare_runs: Optional[List[str]] = None,
154
+ compare_runs: list[str] | None = None,
157
155
  ) -> Path:
158
156
  """Generate comprehensive performance report.
159
157
 
@@ -168,9 +166,13 @@ class WorkflowPerformanceReporter:
168
166
  """
169
167
  if output_path is None:
170
168
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
169
+ # Use centralized output directory
170
+ project_root = Path(__file__).parent.parent.parent.parent
171
171
  output_path = (
172
- Path.cwd()
172
+ project_root
173
+ / "data"
173
174
  / "outputs"
175
+ / "reports"
174
176
  / f"workflow_report_{run_id[:8]}_{timestamp}.{format.value}"
175
177
  )
176
178
 
@@ -207,7 +209,7 @@ class WorkflowPerformanceReporter:
207
209
  self.logger.info(f"Generated {format.value.upper()} report: {output_path}")
208
210
  return output_path
209
211
 
210
- def _analyze_workflow_run(self, run_id: str) -> Dict[str, Any]:
212
+ def _analyze_workflow_run(self, run_id: str) -> dict[str, Any]:
211
213
  """Perform detailed analysis of a workflow run.
212
214
 
213
215
  Args:
@@ -260,7 +262,7 @@ class WorkflowPerformanceReporter:
260
262
  }
261
263
 
262
264
  def _calculate_workflow_summary(
263
- self, run: Any, tasks: List[TaskRun]
265
+ self, run: Any, tasks: list[TaskRun]
264
266
  ) -> WorkflowSummary:
265
267
  """Calculate summary statistics for the workflow run."""
266
268
  summary = WorkflowSummary(
@@ -336,7 +338,7 @@ class WorkflowPerformanceReporter:
336
338
 
337
339
  return summary
338
340
 
339
- def _analyze_task_performance(self, tasks: List[TaskRun]) -> Dict[str, Any]:
341
+ def _analyze_task_performance(self, tasks: list[TaskRun]) -> dict[str, Any]:
340
342
  """Analyze performance patterns across tasks."""
341
343
  analysis = {
342
344
  "by_node_type": {},
@@ -399,7 +401,7 @@ class WorkflowPerformanceReporter:
399
401
 
400
402
  return analysis
401
403
 
402
- def _identify_bottlenecks(self, tasks: List[TaskRun]) -> List[Dict[str, Any]]:
404
+ def _identify_bottlenecks(self, tasks: list[TaskRun]) -> list[dict[str, Any]]:
403
405
  """Identify performance bottlenecks in the workflow."""
404
406
  bottlenecks = []
405
407
 
@@ -493,7 +495,7 @@ class WorkflowPerformanceReporter:
493
495
 
494
496
  return sorted(bottlenecks, key=lambda x: x["value"], reverse=True)
495
497
 
496
- def _analyze_resource_utilization(self, tasks: List[TaskRun]) -> Dict[str, Any]:
498
+ def _analyze_resource_utilization(self, tasks: list[TaskRun]) -> dict[str, Any]:
497
499
  """Analyze overall resource utilization patterns."""
498
500
  analysis = {
499
501
  "cpu_distribution": {},
@@ -577,7 +579,7 @@ class WorkflowPerformanceReporter:
577
579
 
578
580
  return analysis
579
581
 
580
- def _analyze_errors(self, tasks: List[TaskRun]) -> Dict[str, Any]:
582
+ def _analyze_errors(self, tasks: list[TaskRun]) -> dict[str, Any]:
581
583
  """Analyze error patterns and failure modes."""
582
584
  analysis = {
583
585
  "error_summary": {},
@@ -630,7 +632,7 @@ class WorkflowPerformanceReporter:
630
632
 
631
633
  return analysis
632
634
 
633
- def _generate_insights(self, analysis: Dict[str, Any]) -> List[PerformanceInsight]:
635
+ def _generate_insights(self, analysis: dict[str, Any]) -> list[PerformanceInsight]:
634
636
  """Generate actionable insights from analysis results."""
635
637
  insights = []
636
638
 
@@ -737,8 +739,8 @@ class WorkflowPerformanceReporter:
737
739
  return insights
738
740
 
739
741
  def _generate_analysis_charts(
740
- self, run_id: str, tasks: List[TaskRun]
741
- ) -> Dict[str, str]:
742
+ self, run_id: str, tasks: list[TaskRun]
743
+ ) -> dict[str, str]:
742
744
  """Generate analysis charts and return file paths."""
743
745
  charts = {}
744
746
 
@@ -751,7 +753,7 @@ class WorkflowPerformanceReporter:
751
753
 
752
754
  return charts
753
755
 
754
- def _compare_runs(self, run_ids: List[str]) -> Dict[str, Any]:
756
+ def _compare_runs(self, run_ids: list[str]) -> dict[str, Any]:
755
757
  """Compare performance across multiple runs."""
756
758
  comparison = {"runs": [], "trends": {}, "relative_performance": {}}
757
759
 
@@ -810,9 +812,9 @@ class WorkflowPerformanceReporter:
810
812
 
811
813
  def _generate_html_report(
812
814
  self,
813
- analysis: Dict[str, Any],
814
- insights: List[PerformanceInsight],
815
- comparison_data: Optional[Dict[str, Any]] = None,
815
+ analysis: dict[str, Any],
816
+ insights: list[PerformanceInsight],
817
+ comparison_data: dict[str, Any] | None = None,
816
818
  ) -> str:
817
819
  """Generate HTML report content."""
818
820
  run_info = analysis["run_info"]
@@ -978,9 +980,9 @@ class WorkflowPerformanceReporter:
978
980
 
979
981
  def _generate_markdown_report(
980
982
  self,
981
- analysis: Dict[str, Any],
982
- insights: List[PerformanceInsight],
983
- comparison_data: Optional[Dict[str, Any]] = None,
983
+ analysis: dict[str, Any],
984
+ insights: list[PerformanceInsight],
985
+ comparison_data: dict[str, Any] | None = None,
984
986
  ) -> str:
985
987
  """Generate Markdown report content."""
986
988
  run_info = analysis["run_info"]
@@ -1094,9 +1096,9 @@ class WorkflowPerformanceReporter:
1094
1096
 
1095
1097
  def _generate_json_report(
1096
1098
  self,
1097
- analysis: Dict[str, Any],
1098
- insights: List[PerformanceInsight],
1099
- comparison_data: Optional[Dict[str, Any]] = None,
1099
+ analysis: dict[str, Any],
1100
+ insights: list[PerformanceInsight],
1101
+ comparison_data: dict[str, Any] | None = None,
1100
1102
  ) -> str:
1101
1103
  """Generate JSON report content."""
1102
1104
  report_data = {
@@ -1140,7 +1142,7 @@ class WorkflowPerformanceReporter:
1140
1142
 
1141
1143
  return json.dumps(report_data, indent=2, default=str)
1142
1144
 
1143
- def _generate_comparison_html(self, comparison_data: Dict[str, Any]) -> str:
1145
+ def _generate_comparison_html(self, comparison_data: dict[str, Any]) -> str:
1144
1146
  """Generate HTML for run comparison section."""
1145
1147
  runs = comparison_data.get("runs", [])
1146
1148
  trends = comparison_data.get("trends", {})