kailash 0.6.6__py3-none-any.whl → 0.7.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.
- kailash/__init__.py +35 -5
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/security.py +16 -1
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +466 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +290 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +291 -12
- kailash/workflow/validation.py +144 -8
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/RECORD +63 -25
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.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
|