kailash 0.6.6__py3-none-any.whl → 0.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.
Files changed (82) hide show
  1. kailash/__init__.py +35 -5
  2. kailash/access_control.py +64 -46
  3. kailash/adapters/__init__.py +5 -0
  4. kailash/adapters/mcp_platform_adapter.py +273 -0
  5. kailash/api/workflow_api.py +34 -3
  6. kailash/channels/__init__.py +21 -0
  7. kailash/channels/api_channel.py +409 -0
  8. kailash/channels/base.py +271 -0
  9. kailash/channels/cli_channel.py +661 -0
  10. kailash/channels/event_router.py +496 -0
  11. kailash/channels/mcp_channel.py +648 -0
  12. kailash/channels/session.py +423 -0
  13. kailash/mcp_server/discovery.py +57 -18
  14. kailash/middleware/communication/api_gateway.py +23 -3
  15. kailash/middleware/communication/realtime.py +83 -0
  16. kailash/middleware/core/agent_ui.py +1 -1
  17. kailash/middleware/gateway/storage_backends.py +393 -0
  18. kailash/middleware/mcp/enhanced_server.py +22 -16
  19. kailash/nexus/__init__.py +21 -0
  20. kailash/nexus/cli/__init__.py +5 -0
  21. kailash/nexus/cli/__main__.py +6 -0
  22. kailash/nexus/cli/main.py +176 -0
  23. kailash/nexus/factory.py +413 -0
  24. kailash/nexus/gateway.py +545 -0
  25. kailash/nodes/__init__.py +8 -5
  26. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  27. kailash/nodes/ai/llm_agent.py +29 -9
  28. kailash/nodes/api/__init__.py +2 -2
  29. kailash/nodes/api/monitoring.py +1 -1
  30. kailash/nodes/base.py +29 -5
  31. kailash/nodes/base_async.py +54 -14
  32. kailash/nodes/code/async_python.py +1 -1
  33. kailash/nodes/code/python.py +50 -6
  34. kailash/nodes/data/async_sql.py +90 -0
  35. kailash/nodes/data/bulk_operations.py +939 -0
  36. kailash/nodes/data/query_builder.py +373 -0
  37. kailash/nodes/data/query_cache.py +512 -0
  38. kailash/nodes/monitoring/__init__.py +10 -0
  39. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  40. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  41. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  42. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  43. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  44. kailash/nodes/security/behavior_analysis.py +414 -0
  45. kailash/nodes/system/__init__.py +17 -0
  46. kailash/nodes/system/command_parser.py +820 -0
  47. kailash/nodes/transaction/__init__.py +48 -0
  48. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  49. kailash/nodes/transaction/saga_coordinator.py +652 -0
  50. kailash/nodes/transaction/saga_state_storage.py +411 -0
  51. kailash/nodes/transaction/saga_step.py +467 -0
  52. kailash/nodes/transaction/transaction_context.py +756 -0
  53. kailash/nodes/transaction/two_phase_commit.py +978 -0
  54. kailash/nodes/transform/processors.py +17 -1
  55. kailash/nodes/validation/__init__.py +21 -0
  56. kailash/nodes/validation/test_executor.py +532 -0
  57. kailash/nodes/validation/validation_nodes.py +447 -0
  58. kailash/resources/factory.py +1 -1
  59. kailash/runtime/access_controlled.py +9 -7
  60. kailash/runtime/async_local.py +84 -21
  61. kailash/runtime/local.py +21 -2
  62. kailash/runtime/parameter_injector.py +187 -31
  63. kailash/runtime/runner.py +6 -4
  64. kailash/runtime/testing.py +1 -1
  65. kailash/security.py +22 -3
  66. kailash/servers/__init__.py +32 -0
  67. kailash/servers/durable_workflow_server.py +430 -0
  68. kailash/servers/enterprise_workflow_server.py +522 -0
  69. kailash/servers/gateway.py +183 -0
  70. kailash/servers/workflow_server.py +293 -0
  71. kailash/utils/data_validation.py +192 -0
  72. kailash/workflow/builder.py +382 -15
  73. kailash/workflow/cyclic_runner.py +102 -10
  74. kailash/workflow/validation.py +144 -8
  75. kailash/workflow/visualization.py +99 -27
  76. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
  77. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
  78. kailash/workflow/builder_improvements.py +0 -207
  79. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
  80. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
  81. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
  82. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,820 @@
1
+ """Command parsing nodes for CLI channel integration."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+ import shlex
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Any, Callable, Dict, List, Optional, Union
10
+
11
+ from ..base import Node, NodeParameter
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CommandType(Enum):
17
+ """Types of commands that can be parsed."""
18
+
19
+ WORKFLOW = "workflow"
20
+ SYSTEM = "system"
21
+ HELP = "help"
22
+ ADMIN = "admin"
23
+ CUSTOM = "custom"
24
+
25
+
26
+ @dataclass
27
+ class ParsedCommand:
28
+ """Represents a parsed command."""
29
+
30
+ command_type: CommandType
31
+ command_name: str
32
+ arguments: Dict[str, Any]
33
+ subcommand: Optional[str] = None
34
+ flags: List[str] = None
35
+ raw_command: str = ""
36
+ error: Optional[str] = None
37
+
38
+ def __post_init__(self):
39
+ if self.flags is None:
40
+ self.flags = []
41
+
42
+
43
+ class CommandParserNode(Node):
44
+ """Node for parsing CLI commands into structured data.
45
+
46
+ This node takes raw command-line input and parses it into a structured
47
+ format that can be used by other nodes in the workflow.
48
+ """
49
+
50
+ def __init__(self):
51
+ """Initialize command parser node."""
52
+ super().__init__()
53
+
54
+ def get_parameters(self) -> Dict[str, NodeParameter]:
55
+ """Define the parameters this node accepts.
56
+
57
+ Returns:
58
+ Dictionary of parameter definitions
59
+ """
60
+ return {
61
+ "command_input": NodeParameter(
62
+ name="command_input",
63
+ type=str,
64
+ required=True,
65
+ description="Raw command line input to parse",
66
+ ),
67
+ "command_definitions": NodeParameter(
68
+ name="command_definitions",
69
+ type=dict,
70
+ required=False,
71
+ default={},
72
+ description="Dictionary defining available commands and their arguments",
73
+ ),
74
+ "allow_unknown_commands": NodeParameter(
75
+ name="allow_unknown_commands",
76
+ type=bool,
77
+ required=False,
78
+ default=True,
79
+ description="Whether to allow parsing of unknown commands",
80
+ ),
81
+ "default_command_type": NodeParameter(
82
+ name="default_command_type",
83
+ type=str,
84
+ required=False,
85
+ default="workflow",
86
+ description="Default command type for unknown commands",
87
+ ),
88
+ }
89
+
90
+ def execute(self, **kwargs) -> Dict[str, Any]:
91
+ """Execute command parsing.
92
+
93
+ Returns:
94
+ Dictionary containing parsed command data
95
+ """
96
+ # Get parameters
97
+ command_input = kwargs.get("command_input", "")
98
+ command_definitions = kwargs.get("command_definitions", {})
99
+ allow_unknown = kwargs.get("allow_unknown_commands", True)
100
+ default_type = kwargs.get("default_command_type", "workflow")
101
+
102
+ try:
103
+ # Parse the command
104
+ parsed_command = self._parse_command(
105
+ command_input, command_definitions, allow_unknown, default_type
106
+ )
107
+
108
+ return {
109
+ "parsed_command": parsed_command,
110
+ "success": parsed_command.error is None,
111
+ "command_type": parsed_command.command_type.value,
112
+ "command_name": parsed_command.command_name,
113
+ "arguments": parsed_command.arguments,
114
+ "subcommand": parsed_command.subcommand,
115
+ "flags": parsed_command.flags,
116
+ "error": parsed_command.error,
117
+ }
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error parsing command: {e}")
121
+
122
+ error_command = ParsedCommand(
123
+ command_type=CommandType.SYSTEM,
124
+ command_name="error",
125
+ arguments={},
126
+ raw_command=command_input,
127
+ error=str(e),
128
+ )
129
+
130
+ return {
131
+ "parsed_command": error_command,
132
+ "success": False,
133
+ "command_type": "system",
134
+ "command_name": "error",
135
+ "arguments": {},
136
+ "subcommand": None,
137
+ "flags": [],
138
+ "error": str(e),
139
+ }
140
+
141
+ def _parse_command(
142
+ self,
143
+ command_input: str,
144
+ command_definitions: Dict[str, Any],
145
+ allow_unknown: bool,
146
+ default_type: str,
147
+ ) -> ParsedCommand:
148
+ """Parse a command string into structured data.
149
+
150
+ Args:
151
+ command_input: Raw command string
152
+ command_definitions: Dictionary of known command definitions
153
+ allow_unknown: Whether to allow unknown commands
154
+ default_type: Default command type for unknown commands
155
+
156
+ Returns:
157
+ ParsedCommand instance
158
+ """
159
+ # Tokenize the command
160
+ try:
161
+ tokens = shlex.split(command_input.strip())
162
+ except ValueError as e:
163
+ return ParsedCommand(
164
+ command_type=CommandType.SYSTEM,
165
+ command_name="parse_error",
166
+ arguments={},
167
+ raw_command=command_input,
168
+ error=f"Failed to tokenize command: {e}",
169
+ )
170
+
171
+ if not tokens:
172
+ return ParsedCommand(
173
+ command_type=CommandType.HELP,
174
+ command_name="help",
175
+ arguments={},
176
+ raw_command=command_input,
177
+ )
178
+
179
+ # Extract command name
180
+ command_name = tokens[0]
181
+ remaining_tokens = tokens[1:] if len(tokens) > 1 else []
182
+
183
+ # Check if it's a known command
184
+ if command_name in command_definitions:
185
+ return self._parse_known_command(
186
+ command_name,
187
+ remaining_tokens,
188
+ command_definitions[command_name],
189
+ command_input,
190
+ )
191
+
192
+ # Handle special system commands
193
+ if command_name in ["help", "exit", "quit", "status", "version"]:
194
+ return self._parse_system_command(
195
+ command_name, remaining_tokens, command_input
196
+ )
197
+
198
+ # Handle unknown commands
199
+ if allow_unknown:
200
+ return self._parse_unknown_command(
201
+ command_name, remaining_tokens, default_type, command_input
202
+ )
203
+ else:
204
+ return ParsedCommand(
205
+ command_type=CommandType.SYSTEM,
206
+ command_name="unknown_command",
207
+ arguments={"requested_command": command_name},
208
+ raw_command=command_input,
209
+ error=f"Unknown command: {command_name}",
210
+ )
211
+
212
+ def _parse_known_command(
213
+ self,
214
+ command_name: str,
215
+ tokens: List[str],
216
+ definition: Dict[str, Any],
217
+ raw_command: str,
218
+ ) -> ParsedCommand:
219
+ """Parse a known command using its definition.
220
+
221
+ Args:
222
+ command_name: Name of the command
223
+ tokens: Remaining tokens after command name
224
+ definition: Command definition dictionary
225
+ raw_command: Original command string
226
+
227
+ Returns:
228
+ ParsedCommand instance
229
+ """
230
+ command_type = CommandType(definition.get("type", "custom"))
231
+
232
+ # Create argument parser based on definition
233
+ parser = argparse.ArgumentParser(
234
+ prog=command_name,
235
+ description=definition.get("description", ""),
236
+ add_help=False, # We'll handle help ourselves
237
+ )
238
+
239
+ # Add arguments from definition
240
+ arguments_def = definition.get("arguments", {})
241
+ for arg_name, arg_config in arguments_def.items():
242
+ arg_flags = arg_config.get("flags", [f"--{arg_name}"])
243
+ arg_type = arg_config.get("type", str)
244
+ arg_required = arg_config.get("required", False)
245
+ arg_help = arg_config.get("help", "")
246
+ arg_default = arg_config.get("default")
247
+ arg_action = arg_config.get("action")
248
+
249
+ # Filter flags to only include those starting with '-'
250
+ valid_flags = [flag for flag in arg_flags if flag.startswith("-")]
251
+ if not valid_flags:
252
+ valid_flags = [f"--{arg_name}"] # Fallback
253
+
254
+ kwargs = {"help": arg_help}
255
+
256
+ if arg_action:
257
+ kwargs["action"] = arg_action
258
+ else:
259
+ kwargs["type"] = arg_type
260
+ kwargs["required"] = arg_required
261
+ if arg_default is not None:
262
+ kwargs["default"] = arg_default
263
+
264
+ parser.add_argument(*valid_flags, **kwargs)
265
+
266
+ # Add subcommands if defined
267
+ subcommands_def = definition.get("subcommands", {})
268
+ subparser = None
269
+ if subcommands_def:
270
+ subparser = parser.add_subparsers(dest="subcommand")
271
+ for sub_name, sub_config in subcommands_def.items():
272
+ sub_p = subparser.add_parser(sub_name, help=sub_config.get("help", ""))
273
+ # Add subcommand arguments
274
+ for arg_name, arg_config in sub_config.get("arguments", {}).items():
275
+ arg_flags = arg_config.get("flags", [f"--{arg_name}"])
276
+ sub_p.add_argument(
277
+ *arg_flags,
278
+ **{k: v for k, v in arg_config.items() if k != "flags"},
279
+ )
280
+
281
+ try:
282
+ # Parse the arguments
283
+ parsed_args = parser.parse_args(tokens)
284
+
285
+ # Convert to dictionary
286
+ args_dict = vars(parsed_args)
287
+ subcommand = args_dict.pop("subcommand", None)
288
+
289
+ # Extract flags (boolean arguments that were set)
290
+ flags = [
291
+ arg
292
+ for arg, value in args_dict.items()
293
+ if isinstance(value, bool) and value
294
+ ]
295
+
296
+ return ParsedCommand(
297
+ command_type=command_type,
298
+ command_name=command_name,
299
+ arguments=args_dict,
300
+ subcommand=subcommand,
301
+ flags=flags,
302
+ raw_command=raw_command,
303
+ )
304
+
305
+ except SystemExit:
306
+ # argparse calls sys.exit on error, we catch this
307
+ return ParsedCommand(
308
+ command_type=command_type,
309
+ command_name=command_name,
310
+ arguments={},
311
+ raw_command=raw_command,
312
+ error="Invalid command arguments",
313
+ )
314
+
315
+ def _parse_system_command(
316
+ self, command_name: str, tokens: List[str], raw_command: str
317
+ ) -> ParsedCommand:
318
+ """Parse a system command.
319
+
320
+ Args:
321
+ command_name: System command name
322
+ tokens: Remaining tokens
323
+ raw_command: Original command string
324
+
325
+ Returns:
326
+ ParsedCommand instance
327
+ """
328
+ arguments = {}
329
+
330
+ if command_name == "help":
331
+ if tokens:
332
+ arguments["topic"] = tokens[0]
333
+ elif command_name in ["exit", "quit"]:
334
+ arguments["force"] = "--force" in tokens
335
+ elif command_name == "status":
336
+ arguments["verbose"] = "--verbose" in tokens or "-v" in tokens
337
+
338
+ flags = [token for token in tokens if token.startswith("-")]
339
+
340
+ return ParsedCommand(
341
+ command_type=CommandType.SYSTEM,
342
+ command_name=command_name,
343
+ arguments=arguments,
344
+ flags=flags,
345
+ raw_command=raw_command,
346
+ )
347
+
348
+ def _parse_unknown_command(
349
+ self, command_name: str, tokens: List[str], default_type: str, raw_command: str
350
+ ) -> ParsedCommand:
351
+ """Parse an unknown command with basic argument extraction.
352
+
353
+ Args:
354
+ command_name: Unknown command name
355
+ tokens: Remaining tokens
356
+ default_type: Default command type to assign
357
+ raw_command: Original command string
358
+
359
+ Returns:
360
+ ParsedCommand instance
361
+ """
362
+ arguments = {}
363
+ flags = []
364
+ positional_args = []
365
+
366
+ i = 0
367
+ while i < len(tokens):
368
+ token = tokens[i]
369
+
370
+ if token.startswith("--"):
371
+ # Long flag
372
+ if "=" in token:
373
+ key, value = token[2:].split("=", 1)
374
+ arguments[key] = value
375
+ else:
376
+ key = token[2:]
377
+ if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
378
+ arguments[key] = tokens[i + 1]
379
+ i += 1
380
+ else:
381
+ flags.append(key)
382
+ elif token.startswith("-") and len(token) > 1:
383
+ # Short flag(s)
384
+ flag_chars = token[1:]
385
+ for char in flag_chars:
386
+ flags.append(char)
387
+ else:
388
+ # Positional argument
389
+ positional_args.append(token)
390
+
391
+ i += 1
392
+
393
+ # Add positional arguments
394
+ if positional_args:
395
+ arguments["args"] = positional_args
396
+
397
+ try:
398
+ command_type = CommandType(default_type)
399
+ except ValueError:
400
+ command_type = CommandType.CUSTOM
401
+
402
+ return ParsedCommand(
403
+ command_type=command_type,
404
+ command_name=command_name,
405
+ arguments=arguments,
406
+ flags=flags,
407
+ raw_command=raw_command,
408
+ )
409
+
410
+
411
+ class InteractiveShellNode(Node):
412
+ """Node for handling interactive shell sessions.
413
+
414
+ This node manages persistent shell sessions with command history,
415
+ tab completion, and session state.
416
+ """
417
+
418
+ def __init__(self):
419
+ """Initialize interactive shell node."""
420
+ super().__init__()
421
+
422
+ def get_parameters(self) -> Dict[str, NodeParameter]:
423
+ """Define the parameters this node accepts.
424
+
425
+ Returns:
426
+ Dictionary of parameter definitions
427
+ """
428
+ return {
429
+ "session_id": NodeParameter(
430
+ name="session_id",
431
+ type=str,
432
+ required=True,
433
+ description="Unique session identifier",
434
+ ),
435
+ "command_input": NodeParameter(
436
+ name="command_input",
437
+ type=str,
438
+ required=True,
439
+ description="Command input from user",
440
+ ),
441
+ "session_state": NodeParameter(
442
+ name="session_state",
443
+ type=dict,
444
+ required=False,
445
+ default={},
446
+ description="Current session state",
447
+ ),
448
+ "prompt_template": NodeParameter(
449
+ name="prompt_template",
450
+ type=str,
451
+ required=False,
452
+ default="kailash> ",
453
+ description="Shell prompt template",
454
+ ),
455
+ "max_history": NodeParameter(
456
+ name="max_history",
457
+ type=int,
458
+ required=False,
459
+ default=1000,
460
+ description="Maximum number of commands to keep in history",
461
+ ),
462
+ }
463
+
464
+ def execute(self, **kwargs) -> Dict[str, Any]:
465
+ """Execute interactive shell processing.
466
+
467
+ Returns:
468
+ Dictionary containing shell session data
469
+ """
470
+ # Get parameters
471
+ session_id = kwargs.get("session_id", "")
472
+ command_input = kwargs.get("command_input", "")
473
+ session_state = kwargs.get("session_state", {})
474
+ prompt_template = kwargs.get("prompt_template", "kailash> ")
475
+ max_history = kwargs.get("max_history", 1000)
476
+
477
+ try:
478
+ # Initialize session state if needed
479
+ if "history" not in session_state:
480
+ session_state["history"] = []
481
+ if "environment" not in session_state:
482
+ session_state["environment"] = {}
483
+ if "working_directory" not in session_state:
484
+ session_state["working_directory"] = "/"
485
+ if "last_command_time" not in session_state:
486
+ session_state["last_command_time"] = None
487
+
488
+ # Process special shell commands BEFORE adding to history
489
+ shell_result = self._process_shell_commands(command_input, session_state)
490
+
491
+ # Add command to history (but not if it's a history command to avoid including itself)
492
+ if command_input.strip() and command_input.strip() != "history":
493
+ import time
494
+
495
+ current_time = time.time()
496
+ session_state["history"].append(
497
+ {"command": command_input, "timestamp": current_time}
498
+ )
499
+
500
+ # Maintain history size
501
+ if len(session_state["history"]) > max_history:
502
+ session_state["history"] = session_state["history"][-max_history:]
503
+
504
+ session_state["last_command_time"] = current_time
505
+
506
+ # Generate prompt
507
+ prompt = self._generate_prompt(prompt_template, session_state)
508
+
509
+ return {
510
+ "session_id": session_id,
511
+ "session_state": session_state,
512
+ "prompt": prompt,
513
+ "command_input": command_input,
514
+ "shell_result": shell_result,
515
+ "history_count": len(session_state["history"]),
516
+ "success": True,
517
+ }
518
+
519
+ except Exception as e:
520
+ logger.error(f"Error in interactive shell: {e}")
521
+ return {
522
+ "session_id": session_id,
523
+ "session_state": session_state,
524
+ "prompt": prompt_template,
525
+ "command_input": command_input,
526
+ "shell_result": {"error": str(e)},
527
+ "history_count": len(session_state.get("history", [])),
528
+ "success": False,
529
+ "error": str(e),
530
+ }
531
+
532
+ def _generate_prompt(self, template: str, session_state: Dict[str, Any]) -> str:
533
+ """Generate shell prompt based on template and session state.
534
+
535
+ Args:
536
+ template: Prompt template string
537
+ session_state: Current session state
538
+
539
+ Returns:
540
+ Generated prompt string
541
+ """
542
+ # Simple template substitution
543
+ prompt = template
544
+
545
+ # Replace common placeholders
546
+ prompt = prompt.replace("{cwd}", session_state.get("working_directory", "/"))
547
+ prompt = prompt.replace(
548
+ "{history_count}", str(len(session_state.get("history", [])))
549
+ )
550
+
551
+ # Add environment variables if needed
552
+ for env_key, env_value in session_state.get("environment", {}).items():
553
+ prompt = prompt.replace(f"{{{env_key}}}", str(env_value))
554
+
555
+ return prompt
556
+
557
+ def _process_shell_commands(
558
+ self, command_input: str, session_state: Dict[str, Any]
559
+ ) -> Dict[str, Any]:
560
+ """Process shell-specific commands.
561
+
562
+ Args:
563
+ command_input: Command input string
564
+ session_state: Current session state
565
+
566
+ Returns:
567
+ Shell command result
568
+ """
569
+ command = command_input.strip()
570
+
571
+ if command == "history":
572
+ return {
573
+ "type": "history",
574
+ "data": session_state.get("history", [])[-20:], # Last 20 commands
575
+ }
576
+ elif command.startswith("cd "):
577
+ # Change directory
578
+ new_dir = command[3:].strip()
579
+ if new_dir:
580
+ session_state["working_directory"] = new_dir
581
+ return {"type": "directory_change", "data": {"new_directory": new_dir}}
582
+ elif command.startswith("set "):
583
+ # Set environment variable
584
+ try:
585
+ var_assignment = command[4:].strip()
586
+ if "=" in var_assignment:
587
+ key, value = var_assignment.split("=", 1)
588
+ session_state["environment"][key.strip()] = value.strip()
589
+ return {
590
+ "type": "environment_set",
591
+ "data": {"key": key.strip(), "value": value.strip()},
592
+ }
593
+ except Exception as e:
594
+ return {
595
+ "type": "error",
596
+ "data": {"message": f"Invalid set command: {e}"},
597
+ }
598
+ elif command == "env":
599
+ return {"type": "environment", "data": session_state.get("environment", {})}
600
+ elif command == "pwd":
601
+ return {
602
+ "type": "directory",
603
+ "data": {
604
+ "current_directory": session_state.get("working_directory", "/")
605
+ },
606
+ }
607
+
608
+ # Not a shell command, pass through
609
+ return {"type": "passthrough", "data": {"command": command}}
610
+
611
+
612
+ class CommandRouterNode(Node):
613
+ """Node for routing parsed commands to appropriate handlers.
614
+
615
+ This node takes parsed commands and routes them to the correct
616
+ workflow or handler based on command type and configuration.
617
+ """
618
+
619
+ def __init__(self):
620
+ """Initialize command router node."""
621
+ super().__init__()
622
+
623
+ def get_parameters(self) -> Dict[str, NodeParameter]:
624
+ """Define the parameters this node accepts.
625
+
626
+ Returns:
627
+ Dictionary of parameter definitions
628
+ """
629
+ return {
630
+ "parsed_command": NodeParameter(
631
+ name="parsed_command",
632
+ type=dict,
633
+ required=True,
634
+ description="Parsed command data from CommandParserNode",
635
+ ),
636
+ "routing_config": NodeParameter(
637
+ name="routing_config",
638
+ type=dict,
639
+ required=True,
640
+ description="Configuration for command routing",
641
+ ),
642
+ "default_handler": NodeParameter(
643
+ name="default_handler",
644
+ type=str,
645
+ required=False,
646
+ default="help",
647
+ description="Default handler for unmatched commands",
648
+ ),
649
+ }
650
+
651
+ def execute(self, **kwargs) -> Dict[str, Any]:
652
+ """Execute command routing.
653
+
654
+ Returns:
655
+ Dictionary containing routing decision and target information
656
+ """
657
+ # Get parameters
658
+ parsed_command = kwargs.get("parsed_command", {})
659
+ routing_config = kwargs.get("routing_config", {})
660
+ default_handler = kwargs.get("default_handler", "help")
661
+
662
+ try:
663
+ # Extract command info
664
+ if isinstance(parsed_command, ParsedCommand):
665
+ command_type = parsed_command.command_type.value
666
+ command_name = parsed_command.command_name
667
+ arguments = parsed_command.arguments
668
+ subcommand = parsed_command.subcommand
669
+ else:
670
+ # Handle dictionary input
671
+ command_type = parsed_command.get("command_type", "custom")
672
+ command_name = parsed_command.get("command_name", "unknown")
673
+ arguments = parsed_command.get("arguments", {})
674
+ subcommand = parsed_command.get("subcommand")
675
+
676
+ # Find routing target
677
+ routing_target = self._find_routing_target(
678
+ command_type, command_name, subcommand, routing_config, default_handler
679
+ )
680
+
681
+ # Prepare execution parameters
682
+ execution_params = self._prepare_execution_params(
683
+ parsed_command, arguments, routing_target
684
+ )
685
+
686
+ return {
687
+ "routing_target": routing_target,
688
+ "execution_params": execution_params,
689
+ "command_type": command_type,
690
+ "command_name": command_name,
691
+ "subcommand": subcommand,
692
+ "success": True,
693
+ }
694
+
695
+ except Exception as e:
696
+ logger.error(f"Error routing command: {e}")
697
+ return {
698
+ "routing_target": {"type": "error", "handler": "error"},
699
+ "execution_params": {"error": str(e)},
700
+ "command_type": "system",
701
+ "command_name": "error",
702
+ "subcommand": None,
703
+ "success": False,
704
+ "error": str(e),
705
+ }
706
+
707
+ def _find_routing_target(
708
+ self,
709
+ command_type: str,
710
+ command_name: str,
711
+ subcommand: Optional[str],
712
+ routing_config: Dict[str, Any],
713
+ default_handler: str,
714
+ ) -> Dict[str, Any]:
715
+ """Find the appropriate routing target for a command.
716
+
717
+ Args:
718
+ command_type: Type of command
719
+ command_name: Name of command
720
+ subcommand: Optional subcommand
721
+ routing_config: Routing configuration
722
+ default_handler: Default handler name
723
+
724
+ Returns:
725
+ Routing target information
726
+ """
727
+ # Check for exact command match first
728
+ command_key = f"{command_name}"
729
+ if subcommand:
730
+ command_key += f":{subcommand}"
731
+
732
+ if command_key in routing_config:
733
+ return routing_config[command_key]
734
+
735
+ # Check for command name match without subcommand
736
+ if command_name in routing_config:
737
+ return routing_config[command_name]
738
+
739
+ # Check for command type routing
740
+ type_key = f"type:{command_type}"
741
+ if type_key in routing_config:
742
+ return routing_config[type_key]
743
+
744
+ # Check for pattern matching
745
+ for pattern, target in routing_config.items():
746
+ if pattern.startswith("pattern:"):
747
+ pattern_str = pattern[8:] # Remove "pattern:" prefix
748
+ if self._match_pattern(command_name, pattern_str):
749
+ return target
750
+
751
+ # Return default handler
752
+ return {
753
+ "type": "handler",
754
+ "handler": default_handler,
755
+ "description": f"Default handler for {command_name}",
756
+ }
757
+
758
+ def _match_pattern(self, command_name: str, pattern: str) -> bool:
759
+ """Check if command name matches a pattern.
760
+
761
+ Args:
762
+ command_name: Command name to test
763
+ pattern: Pattern to match against
764
+
765
+ Returns:
766
+ True if pattern matches
767
+ """
768
+ # Simple wildcard matching for now
769
+ if "*" in pattern:
770
+ import fnmatch
771
+
772
+ return fnmatch.fnmatch(command_name, pattern)
773
+
774
+ return command_name == pattern
775
+
776
+ def _prepare_execution_params(
777
+ self,
778
+ parsed_command: Union[ParsedCommand, Dict[str, Any]],
779
+ arguments: Dict[str, Any],
780
+ routing_target: Dict[str, Any],
781
+ ) -> Dict[str, Any]:
782
+ """Prepare parameters for execution.
783
+
784
+ Args:
785
+ parsed_command: Original parsed command
786
+ arguments: Command arguments
787
+ routing_target: Routing target information
788
+
789
+ Returns:
790
+ Execution parameters dictionary
791
+ """
792
+ # Base execution parameters
793
+ exec_params = {"command_arguments": arguments, "routing_info": routing_target}
794
+
795
+ # Add command data
796
+ if isinstance(parsed_command, ParsedCommand):
797
+ exec_params["parsed_command"] = {
798
+ "command_type": parsed_command.command_type.value,
799
+ "command_name": parsed_command.command_name,
800
+ "arguments": parsed_command.arguments,
801
+ "subcommand": parsed_command.subcommand,
802
+ "flags": parsed_command.flags,
803
+ "raw_command": parsed_command.raw_command,
804
+ "error": parsed_command.error,
805
+ }
806
+ else:
807
+ exec_params["parsed_command"] = parsed_command
808
+
809
+ # Add target-specific parameters
810
+ if routing_target.get("type") == "workflow":
811
+ exec_params["workflow_name"] = routing_target.get("workflow")
812
+ exec_params["workflow_inputs"] = arguments
813
+ elif routing_target.get("type") == "handler":
814
+ exec_params["handler_name"] = routing_target.get("handler")
815
+
816
+ # Add any additional parameters from routing target
817
+ additional_params = routing_target.get("parameters", {})
818
+ exec_params.update(additional_params)
819
+
820
+ return exec_params