fastworkflow 2.15.5__py3-none-any.whl → 2.17.13__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.
- fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
- fastworkflow/chat_session.py +379 -206
- fastworkflow/cli.py +80 -165
- fastworkflow/command_context_model.py +73 -7
- fastworkflow/command_executor.py +14 -5
- fastworkflow/command_metadata_api.py +106 -6
- fastworkflow/examples/fastworkflow.env +2 -1
- fastworkflow/examples/fastworkflow.passwords.env +2 -1
- fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
- fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
- fastworkflow/intent_clarification_agent.py +131 -0
- fastworkflow/mcp_server.py +3 -3
- fastworkflow/run/__main__.py +33 -40
- fastworkflow/run_fastapi_mcp/README.md +373 -0
- fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
- fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
- fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
- fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
- fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
- fastworkflow/run_fastapi_mcp/utils.py +517 -0
- fastworkflow/train/__main__.py +1 -1
- fastworkflow/utils/chat_adapter.py +99 -0
- fastworkflow/utils/python_utils.py +4 -4
- fastworkflow/utils/react.py +258 -0
- fastworkflow/utils/signatures.py +338 -139
- fastworkflow/workflow.py +1 -5
- fastworkflow/workflow_agent.py +185 -133
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
- fastworkflow/run_agent/__main__.py +0 -294
- fastworkflow/run_agent/agent_module.py +0 -194
- /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/entry_points.txt +0 -0
fastworkflow/cli.py
CHANGED
|
@@ -12,7 +12,6 @@ import time
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
import importlib.resources
|
|
14
14
|
from rich import print as rprint
|
|
15
|
-
from rich.console import Console
|
|
16
15
|
from rich.live import Live
|
|
17
16
|
from rich.spinner import Spinner
|
|
18
17
|
|
|
@@ -175,72 +174,6 @@ def fetch_example(args):
|
|
|
175
174
|
traceback.print_exc(file=sys.stderr)
|
|
176
175
|
sys.exit(1)
|
|
177
176
|
|
|
178
|
-
def train_example(args):
|
|
179
|
-
# sourcery skip: extract-duplicate-method, extract-method, remove-redundant-fstring, split-or-ifs
|
|
180
|
-
"""Train an existing example workflow."""
|
|
181
|
-
# Create a spinner for the initial check
|
|
182
|
-
spinner = Spinner("dots", text="[bold green]Preparing to train example...[/bold green]")
|
|
183
|
-
|
|
184
|
-
with Live(spinner, refresh_per_second=10):
|
|
185
|
-
# Check if example exists in the local examples directory
|
|
186
|
-
local_examples_dir = Path("./examples")
|
|
187
|
-
workflow_path = local_examples_dir / args.name
|
|
188
|
-
|
|
189
|
-
if workflow_path.is_dir():
|
|
190
|
-
# Get the appropriate env files for this example workflow
|
|
191
|
-
env_file_path, passwords_file_path = find_default_env_files(local_examples_dir)
|
|
192
|
-
|
|
193
|
-
# Check if the files exist
|
|
194
|
-
env_file = Path(env_file_path)
|
|
195
|
-
passwords_file = Path(passwords_file_path)
|
|
196
|
-
|
|
197
|
-
# Create args object for train_main
|
|
198
|
-
train_args = argparse.Namespace(
|
|
199
|
-
workflow_folderpath=str(workflow_path),
|
|
200
|
-
env_file_path=str(env_file),
|
|
201
|
-
passwords_file_path=str(passwords_file)
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# After the spinner, handle any errors or proceed with training
|
|
205
|
-
if not workflow_path.is_dir():
|
|
206
|
-
rprint(f"[bold red]Error:[/bold red] Example '{args.name}' not found in '{local_examples_dir}'.")
|
|
207
|
-
rprint(f"Use 'fastworkflow examples fetch {args.name}' to fetch the example first.")
|
|
208
|
-
rprint("Or use 'fastworkflow examples list' to see available examples.")
|
|
209
|
-
sys.exit(1)
|
|
210
|
-
|
|
211
|
-
if not env_file.exists() or not passwords_file.exists():
|
|
212
|
-
rprint(f"[bold red]Error:[/bold red] Required environment files not found:")
|
|
213
|
-
if not env_file.exists():
|
|
214
|
-
rprint(f" - {env_file} (not found)")
|
|
215
|
-
if not passwords_file.exists():
|
|
216
|
-
rprint(f" - {passwords_file} (not found)")
|
|
217
|
-
rprint("\nPlease run the following command to fetch the example and its environment files:")
|
|
218
|
-
rprint(f" fastworkflow examples fetch {args.name}")
|
|
219
|
-
rprint("\nAfter fetching, edit the passwords file to add your API keys:")
|
|
220
|
-
rprint(f" {local_examples_dir}/fastworkflow.passwords.env")
|
|
221
|
-
sys.exit(1)
|
|
222
|
-
|
|
223
|
-
rprint(f"[bold green]Training example[/bold green] '{args.name}' in '{workflow_path}'...")
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
# Lazy import train_main only when actually training
|
|
227
|
-
from .train.__main__ import train_main as _train_main
|
|
228
|
-
# Call train_main directly instead of using subprocess
|
|
229
|
-
result = _train_main(train_args)
|
|
230
|
-
|
|
231
|
-
if result is None or result == 0:
|
|
232
|
-
rprint(f"\n✅ Successfully trained example '{args.name}'.")
|
|
233
|
-
rprint(f"You can now run it with:\nfastworkflow examples run {args.name}")
|
|
234
|
-
else:
|
|
235
|
-
rprint(f"\n❌ Training failed.")
|
|
236
|
-
sys.exit(1)
|
|
237
|
-
|
|
238
|
-
except Exception as e:
|
|
239
|
-
rprint(f"[bold red]An unexpected error occurred during training:[/bold red] {e}")
|
|
240
|
-
import traceback
|
|
241
|
-
traceback.print_exc(file=sys.stderr)
|
|
242
|
-
sys.exit(1)
|
|
243
|
-
|
|
244
177
|
def find_default_env_files(workflow_path):
|
|
245
178
|
"""Find the appropriate default env files based on context.
|
|
246
179
|
|
|
@@ -338,14 +271,34 @@ def add_run_parser(subparsers):
|
|
|
338
271
|
parser_run.add_argument("--startup_action", help="Optional startup action", default="")
|
|
339
272
|
parser_run.add_argument("--keep_alive", help="Optional keep_alive", default=True)
|
|
340
273
|
parser_run.add_argument("--project_folderpath", help="Optional path to project folder containing application code", default=None)
|
|
341
|
-
parser_run.add_argument(
|
|
342
|
-
"--run_as_agent",
|
|
343
|
-
help="Run in agent mode (uses DSPy for tool selection)",
|
|
344
|
-
action="store_true",
|
|
345
|
-
default=False,
|
|
346
|
-
)
|
|
347
274
|
parser_run.set_defaults(func=lambda args: run_with_defaults(args))
|
|
348
275
|
|
|
276
|
+
def add_run_fastapi_mcp_parser(subparsers):
|
|
277
|
+
"""Add subparser for the 'run_fastapi_mcp' command."""
|
|
278
|
+
parser_run_fastapi_mcp = subparsers.add_parser("run_fastapi_mcp", help="Run a workflow as a FastAPI server with MCP support.")
|
|
279
|
+
parser_run_fastapi_mcp.add_argument("workflow_path", help="Path to the workflow folder")
|
|
280
|
+
|
|
281
|
+
# Default env files will be determined at runtime based on the workflow path
|
|
282
|
+
parser_run_fastapi_mcp.add_argument(
|
|
283
|
+
"env_file_path",
|
|
284
|
+
nargs='?',
|
|
285
|
+
default=None,
|
|
286
|
+
help="Path to the environment file (default: .env in current directory, or bundled env file for examples)",
|
|
287
|
+
)
|
|
288
|
+
parser_run_fastapi_mcp.add_argument(
|
|
289
|
+
"passwords_file_path",
|
|
290
|
+
nargs='?',
|
|
291
|
+
default=None,
|
|
292
|
+
help="Path to the passwords file (default: passwords.env in current directory, or bundled env file for examples)",
|
|
293
|
+
)
|
|
294
|
+
parser_run_fastapi_mcp.add_argument("--context", help="Optional context (JSON string)", default=None)
|
|
295
|
+
parser_run_fastapi_mcp.add_argument("--startup_command", help="Optional startup command", default=None)
|
|
296
|
+
parser_run_fastapi_mcp.add_argument("--startup_action", help="Optional startup action (JSON string)", default=None)
|
|
297
|
+
parser_run_fastapi_mcp.add_argument("--project_folderpath", help="Optional path to project folder containing application code", default=None)
|
|
298
|
+
parser_run_fastapi_mcp.add_argument("--port", type=int, default=8000, help="Port to run the FastAPI server on (default: 8000)")
|
|
299
|
+
parser_run_fastapi_mcp.add_argument("--host", default="0.0.0.0", help="Host to bind the FastAPI server to (default: 0.0.0.0)")
|
|
300
|
+
parser_run_fastapi_mcp.set_defaults(func=lambda args: run_fastapi_mcp_with_defaults(args))
|
|
301
|
+
|
|
349
302
|
def train_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
350
303
|
"""Wrapper for train_main that sets default env file paths based on context."""
|
|
351
304
|
if args.env_file_path is None or args.passwords_file_path is None:
|
|
@@ -363,7 +316,7 @@ def train_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
|
363
316
|
example_name = os.path.basename(args.workflow_folderpath)
|
|
364
317
|
print("\nThis appears to be an example workflow. Please run:")
|
|
365
318
|
print(f" fastworkflow examples fetch {example_name}")
|
|
366
|
-
print(f" fastworkflow
|
|
319
|
+
print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
367
320
|
else:
|
|
368
321
|
print("\nPlease ensure this file exists with required environment variables.")
|
|
369
322
|
print("You can create a basic .env file in your current directory.")
|
|
@@ -376,7 +329,7 @@ def train_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
|
376
329
|
example_name = os.path.basename(args.workflow_folderpath)
|
|
377
330
|
print("\nThis appears to be an example workflow. Please run:")
|
|
378
331
|
print(f" fastworkflow examples fetch {example_name}")
|
|
379
|
-
print(f" fastworkflow
|
|
332
|
+
print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
380
333
|
else:
|
|
381
334
|
print("\nPlease ensure this file exists with required API keys.")
|
|
382
335
|
print("You can create a basic passwords.env file in your current directory.")
|
|
@@ -403,7 +356,7 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
|
403
356
|
example_name = os.path.basename(args.workflow_path)
|
|
404
357
|
print("\nThis appears to be an example workflow. Please run:")
|
|
405
358
|
print(f" fastworkflow examples fetch {example_name}")
|
|
406
|
-
print(f" fastworkflow
|
|
359
|
+
print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
407
360
|
else:
|
|
408
361
|
print("\nPlease ensure this file exists with required environment variables.")
|
|
409
362
|
print("You can create a basic .env file in your current directory.")
|
|
@@ -416,7 +369,7 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
|
416
369
|
example_name = os.path.basename(args.workflow_path)
|
|
417
370
|
print("\nThis appears to be an example workflow. Please run:")
|
|
418
371
|
print(f" fastworkflow examples fetch {example_name}")
|
|
419
|
-
print(f" fastworkflow
|
|
372
|
+
print(f" fastworkflow train ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
420
373
|
else:
|
|
421
374
|
print("\nPlease ensure this file exists with required API keys.")
|
|
422
375
|
print("You can create a basic passwords.env file in your current directory.")
|
|
@@ -426,85 +379,62 @@ def run_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
|
426
379
|
from .run.__main__ import run_main as _run_main
|
|
427
380
|
return _run_main(args)
|
|
428
381
|
|
|
429
|
-
def
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
382
|
+
def run_fastapi_mcp_with_defaults(args): # sourcery skip: extract-duplicate-method
|
|
383
|
+
"""Wrapper for fastapi mcp server that sets default env file paths based on context."""
|
|
384
|
+
if args.env_file_path is None or args.passwords_file_path is None:
|
|
385
|
+
default_env, default_passwords = find_default_env_files(args.workflow_path)
|
|
386
|
+
if args.env_file_path is None:
|
|
387
|
+
args.env_file_path = default_env
|
|
388
|
+
if args.passwords_file_path is None:
|
|
389
|
+
args.passwords_file_path = default_passwords
|
|
434
390
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
# Check if the example has been trained
|
|
449
|
-
command_info_dir = workflow_path / "___command_info"
|
|
450
|
-
|
|
451
|
-
# After the spinner, handle any errors or proceed with running
|
|
452
|
-
if not workflow_path.is_dir():
|
|
453
|
-
rprint(f"[bold red]Error:[/bold red] Example '{args.name}' not found in '{local_examples_dir}'.")
|
|
454
|
-
rprint(f"Use 'fastworkflow examples fetch {args.name}' to fetch the example first.")
|
|
455
|
-
rprint("Or use 'fastworkflow examples list' to see available examples.")
|
|
391
|
+
# Check if the files exist and provide helpful error messages
|
|
392
|
+
if not os.path.exists(args.env_file_path):
|
|
393
|
+
print(f"Error: Environment file not found at: {args.env_file_path}", file=sys.stderr)
|
|
394
|
+
# Check if this is an example workflow
|
|
395
|
+
if "/examples/" in str(args.workflow_path) or "\\examples\\" in str(args.workflow_path):
|
|
396
|
+
example_name = os.path.basename(args.workflow_path)
|
|
397
|
+
print("\nThis appears to be an example workflow. Please run:")
|
|
398
|
+
print(f" fastworkflow examples fetch {example_name}")
|
|
399
|
+
print(f" fastworkflow run_fastapi_mcp ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
400
|
+
else:
|
|
401
|
+
print("\nPlease ensure this file exists with required environment variables.")
|
|
402
|
+
print("You can create a basic .env file in your current directory.")
|
|
456
403
|
sys.exit(1)
|
|
457
404
|
|
|
458
|
-
if not
|
|
459
|
-
|
|
460
|
-
if
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
rprint(f" fastworkflow examples train {args.name}")
|
|
405
|
+
if not os.path.exists(args.passwords_file_path):
|
|
406
|
+
print(f"Error: Passwords file not found at: {args.passwords_file_path}", file=sys.stderr)
|
|
407
|
+
# Check if this is an example workflow
|
|
408
|
+
if "/examples/" in str(args.workflow_path) or "\\examples\\" in str(args.workflow_path):
|
|
409
|
+
example_name = os.path.basename(args.workflow_path)
|
|
410
|
+
print("\nThis appears to be an example workflow. Please run:")
|
|
411
|
+
print(f" fastworkflow examples fetch {example_name}")
|
|
412
|
+
print(f" fastworkflow run_fastapi_mcp ./examples/{example_name} ./examples/fastworkflow.env ./examples/fastworkflow.passwords.env")
|
|
413
|
+
else:
|
|
414
|
+
print("\nPlease ensure this file exists with required API keys.")
|
|
415
|
+
print("You can create a basic passwords.env file in your current directory.")
|
|
470
416
|
sys.exit(1)
|
|
471
417
|
|
|
472
|
-
#
|
|
473
|
-
if not command_info_dir.exists() or not any(command_info_dir.iterdir()):
|
|
474
|
-
rprint(f"[bold yellow]Warning:[/bold yellow] Example '{args.name}' does not appear to be trained yet.")
|
|
475
|
-
rprint(f"Please train the example first with:")
|
|
476
|
-
rprint(f" fastworkflow examples train {args.name}")
|
|
477
|
-
response = input("Do you want to continue anyway? [y/N] ")
|
|
478
|
-
if response.lower() != 'y':
|
|
479
|
-
rprint("Operation cancelled.")
|
|
480
|
-
sys.exit(0)
|
|
481
|
-
|
|
482
|
-
rprint(f"[bold green]Running example[/bold green] '{args.name}'...")
|
|
483
|
-
|
|
484
|
-
# For interactive applications, we need to use os.execvp to replace the current process
|
|
485
|
-
# This ensures that stdin/stdout/stderr are properly connected for interactive use
|
|
418
|
+
# Use subprocess to run python -m fastworkflow.run_fastapi_mcp with the correct arguments
|
|
486
419
|
cmd = [
|
|
487
|
-
sys.executable,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
str(
|
|
420
|
+
sys.executable, '-m', 'fastworkflow.run_fastapi_mcp',
|
|
421
|
+
'--workflow_path', args.workflow_path,
|
|
422
|
+
'--env_file_path', args.env_file_path,
|
|
423
|
+
'--passwords_file_path', args.passwords_file_path,
|
|
424
|
+
'--port', str(args.port),
|
|
425
|
+
'--host', args.host,
|
|
492
426
|
]
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if
|
|
496
|
-
cmd.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
rprint(f"[bold red]An unexpected error occurred while running the example:[/bold red] {e}")
|
|
505
|
-
import traceback
|
|
506
|
-
traceback.print_exc(file=sys.stderr)
|
|
507
|
-
sys.exit(1)
|
|
427
|
+
if args.context:
|
|
428
|
+
cmd.extend(['--context', args.context])
|
|
429
|
+
if args.startup_command:
|
|
430
|
+
cmd.extend(['--startup_command', args.startup_command])
|
|
431
|
+
if args.startup_action:
|
|
432
|
+
cmd.extend(['--startup_action', args.startup_action])
|
|
433
|
+
if args.project_folderpath:
|
|
434
|
+
cmd.extend(['--project_folderpath', args.project_folderpath])
|
|
435
|
+
|
|
436
|
+
# Run the subprocess
|
|
437
|
+
return subprocess.run(cmd).returncode
|
|
508
438
|
|
|
509
439
|
def main():
|
|
510
440
|
"""Main function for the fastworkflow CLI."""
|
|
@@ -528,27 +458,12 @@ def main():
|
|
|
528
458
|
parser_fetch.add_argument("--force", action="store_true", help="Force overwrite if example already exists")
|
|
529
459
|
parser_fetch.set_defaults(func=fetch_example)
|
|
530
460
|
|
|
531
|
-
# 'examples train' command
|
|
532
|
-
parser_train_example = examples_subparsers.add_parser("train", help="Train a specific example")
|
|
533
|
-
parser_train_example.add_argument("name", help="The name of the example to train")
|
|
534
|
-
parser_train_example.set_defaults(func=train_example)
|
|
535
|
-
|
|
536
|
-
# 'examples run' command
|
|
537
|
-
parser_run_example = examples_subparsers.add_parser("run", help="Run a specific example")
|
|
538
|
-
parser_run_example.add_argument("name", help="The name of the example to run")
|
|
539
|
-
parser_run_example.add_argument(
|
|
540
|
-
"--run_as_agent",
|
|
541
|
-
help="Run the example in agent mode (uses DSPy for tool selection)",
|
|
542
|
-
action="store_true",
|
|
543
|
-
default=False,
|
|
544
|
-
)
|
|
545
|
-
parser_run_example.set_defaults(func=run_example)
|
|
546
|
-
|
|
547
461
|
# Add top-level commands
|
|
548
462
|
add_build_parser(subparsers)
|
|
549
463
|
add_refine_parser(subparsers)
|
|
550
464
|
add_train_parser(subparsers)
|
|
551
465
|
add_run_parser(subparsers)
|
|
466
|
+
add_run_fastapi_mcp_parser(subparsers)
|
|
552
467
|
|
|
553
468
|
try:
|
|
554
469
|
args = parser.parse_args()
|
|
@@ -2,11 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
import os
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Optional, Type
|
|
7
8
|
|
|
8
9
|
import fastworkflow
|
|
9
|
-
from fastworkflow.command_directory import
|
|
10
|
+
from fastworkflow.command_directory import get_cached_command_directory
|
|
10
11
|
from fastworkflow.utils import python_utils
|
|
11
12
|
|
|
12
13
|
"""Utility for loading and traversing the single-file workflow command context model.
|
|
@@ -152,10 +153,10 @@ class CommandContextModel:
|
|
|
152
153
|
hierarchy_model_path = Path(self._workflow_path) / "context_hierarchy_model.json"
|
|
153
154
|
if not hierarchy_model_path.is_file():
|
|
154
155
|
return {}
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
with hierarchy_model_path.open("r") as f:
|
|
157
158
|
hierarchy_data = json.load(f)
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
return hierarchy_data
|
|
160
161
|
|
|
161
162
|
def _resolve_ancestry(self, hierarchy: dict[str, dict[str, list[str]]]) -> None:
|
|
@@ -205,10 +206,10 @@ class CommandContextModel:
|
|
|
205
206
|
all_ancestors.add(parent)
|
|
206
207
|
grandparents = self.get_ancestor_contexts(parent, visiting.copy(), _hierarchy)
|
|
207
208
|
all_ancestors.update(grandparents)
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
final_ancestors = sorted(list(all_ancestors))
|
|
210
211
|
self._resolved_ancestors[context_name] = final_ancestors
|
|
211
|
-
|
|
212
|
+
|
|
212
213
|
visiting.remove(context_name)
|
|
213
214
|
|
|
214
215
|
return final_ancestors
|
|
@@ -228,7 +229,7 @@ class CommandContextModel:
|
|
|
228
229
|
# or by being part of a command inheritance structure. If it's not
|
|
229
230
|
# in _command_contexts, it's an unknown/invalid context.
|
|
230
231
|
raise CommandContextModelValidationError(f"Context '{context_name}' not found in model.")
|
|
231
|
-
|
|
232
|
+
|
|
232
233
|
if visiting is None:
|
|
233
234
|
visiting = set()
|
|
234
235
|
|
|
@@ -294,3 +295,68 @@ class CommandContextModel:
|
|
|
294
295
|
return getattr(module, context_metadata.context_class, None)
|
|
295
296
|
else:
|
|
296
297
|
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------
|
|
301
|
+
# Workflow info helper (module-level)
|
|
302
|
+
# ---------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def get_workflow_info(chat_session_or_path: fastworkflow.ChatSession | str) -> dict[str, Any]:
|
|
305
|
+
"""
|
|
306
|
+
Return workflow-level metadata for the active workflow in the given chat session or workflow path.
|
|
307
|
+
|
|
308
|
+
Shape:
|
|
309
|
+
{
|
|
310
|
+
"workflow_name": str,
|
|
311
|
+
"available_contexts": list[str],
|
|
312
|
+
"context_inheritance_model": dict,
|
|
313
|
+
"context_hierarchy_model": dict
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
Notes:
|
|
317
|
+
- available_contexts are derived from CommandContextModel.load(workflow_path)
|
|
318
|
+
so they reflect filesystem + inheritance.
|
|
319
|
+
- current context and purpose/description are intentionally omitted.
|
|
320
|
+
- Raw JSON for inheritance (from _commands/context_inheritance_model.json)
|
|
321
|
+
and hierarchy (from context_hierarchy_model.json) is returned verbatim when present.
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(chat_session_or_path, str):
|
|
324
|
+
workflow_path_str = chat_session_or_path
|
|
325
|
+
else:
|
|
326
|
+
workflow_path_str = chat_session_or_path.get_active_workflow().folderpath
|
|
327
|
+
workflow_name = os.path.basename(os.path.abspath(workflow_path_str))
|
|
328
|
+
|
|
329
|
+
available_contexts: list[str]
|
|
330
|
+
inheritance_json: dict[str, Any] = {}
|
|
331
|
+
hierarchy_json: dict[str, Any] = {}
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# Derive contexts via consolidated model
|
|
335
|
+
context_model = CommandContextModel.load(workflow_path_str)
|
|
336
|
+
# Do not normalize names beyond keeping what the model exposes
|
|
337
|
+
available_contexts = sorted([key for key in context_model._command_contexts.keys() if key != 'IntentDetection'])
|
|
338
|
+
except Exception:
|
|
339
|
+
available_contexts = ["/"]
|
|
340
|
+
|
|
341
|
+
# Load raw context inheritance model (if present)
|
|
342
|
+
try:
|
|
343
|
+
inheritance_path = Path(workflow_path_str) / "_commands" / "context_inheritance_model.json"
|
|
344
|
+
if inheritance_path.is_file():
|
|
345
|
+
inheritance_json = json.loads(inheritance_path.read_text())
|
|
346
|
+
except Exception:
|
|
347
|
+
inheritance_json = {}
|
|
348
|
+
|
|
349
|
+
# Load raw context hierarchy model (if present)
|
|
350
|
+
try:
|
|
351
|
+
hierarchy_path = Path(workflow_path_str) / "context_hierarchy_model.json"
|
|
352
|
+
if hierarchy_path.is_file():
|
|
353
|
+
hierarchy_json = json.loads(hierarchy_path.read_text())
|
|
354
|
+
except Exception:
|
|
355
|
+
hierarchy_json = {}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"workflow_name": workflow_name,
|
|
359
|
+
"available_contexts": available_contexts,
|
|
360
|
+
"context_inheritance_model": inheritance_json,
|
|
361
|
+
"context_hierarchy_model": hierarchy_json,
|
|
362
|
+
}
|
fastworkflow/command_executor.py
CHANGED
|
@@ -45,16 +45,21 @@ class CommandExecutor(CommandExecutorInterface):
|
|
|
45
45
|
command = command)
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
if command_output.command_handled
|
|
48
|
+
if command_output.command_handled:
|
|
49
|
+
# important to clear the current command mode from the workflow context
|
|
50
|
+
if "is_assistant_mode_command" in chat_session.cme_workflow._context:
|
|
51
|
+
del chat_session.cme_workflow._context["is_assistant_mode_command"]
|
|
52
|
+
return command_output
|
|
53
|
+
elif not command_output.success:
|
|
49
54
|
return command_output
|
|
50
55
|
|
|
51
56
|
command_name = command_output.command_responses[0].artifacts["command_name"]
|
|
52
57
|
input_obj = command_output.command_responses[0].artifacts["cmd_parameters"]
|
|
53
58
|
|
|
54
|
-
workflow =
|
|
59
|
+
workflow = chat_session.get_active_workflow()
|
|
55
60
|
workflow_name = workflow.folderpath.split('/')[-1]
|
|
56
61
|
context = workflow.current_command_context_displayname
|
|
57
|
-
|
|
62
|
+
|
|
58
63
|
command_routing_definition = fastworkflow.RoutingRegistry.get_definition(
|
|
59
64
|
workflow.folderpath
|
|
60
65
|
)
|
|
@@ -77,13 +82,17 @@ class CommandExecutor(CommandExecutorInterface):
|
|
|
77
82
|
command_output = response_generation_object(workflow, command, input_obj)
|
|
78
83
|
else:
|
|
79
84
|
command_output = response_generation_object(workflow, command)
|
|
80
|
-
|
|
85
|
+
|
|
81
86
|
# Set the additional attributes
|
|
82
87
|
command_output.workflow_name = workflow_name
|
|
83
88
|
command_output.context = context
|
|
84
89
|
command_output.command_name = command_name
|
|
85
90
|
command_output.command_parameters = input_obj or None
|
|
86
91
|
|
|
92
|
+
# important to clear the current command mode from the workflow context
|
|
93
|
+
if "is_assistant_mode_command" in chat_session.cme_workflow._context:
|
|
94
|
+
del chat_session.cme_workflow._context["is_assistant_mode_command"]
|
|
95
|
+
|
|
87
96
|
return command_output
|
|
88
97
|
|
|
89
98
|
@classmethod
|
|
@@ -134,7 +143,7 @@ class CommandExecutor(CommandExecutorInterface):
|
|
|
134
143
|
input_obj = command_parameters_class(**action.parameters)
|
|
135
144
|
|
|
136
145
|
input_for_param_extraction = InputForParamExtraction(command=action.command)
|
|
137
|
-
is_valid, error_msg, _ = input_for_param_extraction.validate_parameters(
|
|
146
|
+
is_valid, error_msg, _, _ = input_for_param_extraction.validate_parameters(
|
|
138
147
|
workflow, action.command_name, input_obj
|
|
139
148
|
)
|
|
140
149
|
if not is_valid:
|
|
@@ -480,6 +480,58 @@ class CommandMetadataAPI:
|
|
|
480
480
|
|
|
481
481
|
return f"{indent_str}{value}"
|
|
482
482
|
|
|
483
|
+
@staticmethod
|
|
484
|
+
def get_workflow_definition_display_text(workflow_info: Dict[str, Any]) -> str:
|
|
485
|
+
"""
|
|
486
|
+
Convert workflow definition dict to human-readable text display.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
workflow_info: Workflow info dict from get_workflow_info()
|
|
490
|
+
{
|
|
491
|
+
"workflow_name": str,
|
|
492
|
+
"available_contexts": list[str],
|
|
493
|
+
"context_inheritance_model": dict,
|
|
494
|
+
"context_hierarchy_model": dict
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Human-readable text representation of workflow definition
|
|
499
|
+
"""
|
|
500
|
+
lines: List[str] = ["Workflow Definition:"]
|
|
501
|
+
|
|
502
|
+
# Workflow name
|
|
503
|
+
if workflow_name := workflow_info.get("workflow_name"):
|
|
504
|
+
lines.append(f" name: {workflow_name}")
|
|
505
|
+
|
|
506
|
+
# Available contexts
|
|
507
|
+
if contexts := workflow_info.get("available_contexts"):
|
|
508
|
+
lines.append(" available_contexts:")
|
|
509
|
+
for ctx in contexts:
|
|
510
|
+
display_name = "global" if ctx == "*" else ctx
|
|
511
|
+
lines.append(f" - {display_name}")
|
|
512
|
+
|
|
513
|
+
# Context inheritance (if present and non-empty)
|
|
514
|
+
if inheritance := workflow_info.get("context_inheritance_model"):
|
|
515
|
+
if inheritance: # Only show if not empty dict
|
|
516
|
+
lines.append(" context_inheritance:")
|
|
517
|
+
for ctx, parents in sorted(inheritance.items()):
|
|
518
|
+
if parents: # Only show if has parents
|
|
519
|
+
display_ctx = "global" if ctx == "*" else ctx
|
|
520
|
+
parent_list = ", ".join(["global" if p == "*" else p for p in parents])
|
|
521
|
+
lines.append(f" {display_ctx}: [{parent_list}]")
|
|
522
|
+
|
|
523
|
+
# Context hierarchy (if present and non-empty)
|
|
524
|
+
if hierarchy := workflow_info.get("context_hierarchy_model"):
|
|
525
|
+
if hierarchy: # Only show if not empty dict
|
|
526
|
+
lines.append(" context_hierarchy:")
|
|
527
|
+
for parent, children in sorted(hierarchy.items()):
|
|
528
|
+
if children: # Only show if has children
|
|
529
|
+
display_parent = "global" if parent == "*" else parent
|
|
530
|
+
children_list = ", ".join(["global" if c == "*" else c for c in children])
|
|
531
|
+
lines.append(f" {display_parent}: [{children_list}]")
|
|
532
|
+
|
|
533
|
+
return "\n".join(lines)
|
|
534
|
+
|
|
483
535
|
@staticmethod
|
|
484
536
|
def get_command_display_text(
|
|
485
537
|
subject_workflow_path: str,
|
|
@@ -499,7 +551,7 @@ class CommandMetadataAPI:
|
|
|
499
551
|
cme_workflow_path=cme_workflow_path,
|
|
500
552
|
active_context_name=active_context_name,
|
|
501
553
|
)
|
|
502
|
-
|
|
554
|
+
|
|
503
555
|
# Build minimal context info (inheritance/containment if available)
|
|
504
556
|
context_info: Dict[str, Any] = {
|
|
505
557
|
"name": active_context_name,
|
|
@@ -529,10 +581,8 @@ class CommandMetadataAPI:
|
|
|
529
581
|
empty_display = CommandMetadataAPI._prune_empty(empty_display, remove_keys={"default"})
|
|
530
582
|
rendered = CommandMetadataAPI._to_yaml_like(empty_display, omit_command_name=False)
|
|
531
583
|
# Ensure a visible header is present
|
|
532
|
-
header = "Commands available:"
|
|
533
|
-
if
|
|
534
|
-
return header
|
|
535
|
-
return f"{header}\n{rendered}"
|
|
584
|
+
header = "Commands available in the current context:"
|
|
585
|
+
return f"{header}\n{rendered}" if rendered.strip() else header
|
|
536
586
|
|
|
537
587
|
# Build the combined display by stitching per-command strings while keeping
|
|
538
588
|
# a single "Commands available:" header and blank lines between commands
|
|
@@ -553,7 +603,7 @@ class CommandMetadataAPI:
|
|
|
553
603
|
empty_display = CommandMetadataAPI._prune_empty(empty_display, remove_keys={"default"})
|
|
554
604
|
return CommandMetadataAPI._to_yaml_like(empty_display, omit_command_name=False)
|
|
555
605
|
|
|
556
|
-
combined_lines: List[str] = ["Commands available:"]
|
|
606
|
+
combined_lines: List[str] = [f"Commands available in the current context ({context_info['display_name']}):"]
|
|
557
607
|
for idx, text in enumerate(parts):
|
|
558
608
|
lines = text.splitlines()
|
|
559
609
|
if idx > 0:
|
|
@@ -563,6 +613,56 @@ class CommandMetadataAPI:
|
|
|
563
613
|
|
|
564
614
|
return "\n".join(combined_lines)
|
|
565
615
|
|
|
616
|
+
@staticmethod
|
|
617
|
+
def get_suggested_commands_metadata(
|
|
618
|
+
subject_workflow_path: str,
|
|
619
|
+
cme_workflow_path: str,
|
|
620
|
+
active_context_name: str,
|
|
621
|
+
suggested_command_names: list[str],
|
|
622
|
+
for_agents: bool = False,
|
|
623
|
+
) -> str:
|
|
624
|
+
"""
|
|
625
|
+
Get metadata display text for ONLY the suggested commands.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
subject_workflow_path: Path to the subject workflow
|
|
629
|
+
cme_workflow_path: Path to the CME workflow
|
|
630
|
+
active_context_name: Active context name
|
|
631
|
+
suggested_command_names: List of suggested command names (can be short names or qualified)
|
|
632
|
+
for_agents: Include agent-specific fields
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
YAML-like formatted string with metadata for suggested commands only
|
|
636
|
+
"""
|
|
637
|
+
if not suggested_command_names:
|
|
638
|
+
return "No suggested commands provided."
|
|
639
|
+
|
|
640
|
+
parts: list[str] = []
|
|
641
|
+
for suggested_cmd in suggested_command_names:
|
|
642
|
+
# Try to get metadata for this command
|
|
643
|
+
# The command name could be qualified (Context/command) or short (command)
|
|
644
|
+
if part := CommandMetadataAPI.get_command_display_text_for_command(
|
|
645
|
+
subject_workflow_path=subject_workflow_path,
|
|
646
|
+
cme_workflow_path=cme_workflow_path,
|
|
647
|
+
active_context_name=active_context_name,
|
|
648
|
+
qualified_command_name=suggested_cmd,
|
|
649
|
+
for_agents=for_agents,
|
|
650
|
+
):
|
|
651
|
+
parts.append(part)
|
|
652
|
+
|
|
653
|
+
if not parts:
|
|
654
|
+
return "No metadata available for suggested commands."
|
|
655
|
+
|
|
656
|
+
# Combine all parts with header
|
|
657
|
+
combined_lines: list[str] = ["Suggested commands with metadata:"]
|
|
658
|
+
for idx, text in enumerate(parts):
|
|
659
|
+
lines = text.splitlines()
|
|
660
|
+
if idx > 0:
|
|
661
|
+
combined_lines.append("") # Blank line separator
|
|
662
|
+
combined_lines.extend(lines)
|
|
663
|
+
|
|
664
|
+
return "\n".join(combined_lines)
|
|
665
|
+
|
|
566
666
|
@staticmethod
|
|
567
667
|
def get_command_display_text_for_command(
|
|
568
668
|
subject_workflow_path: str,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
LLM_SYNDATA_GEN=mistral/mistral-small-latest
|
|
2
2
|
LLM_PARAM_EXTRACTION=mistral/mistral-small-latest
|
|
3
3
|
LLM_RESPONSE_GEN=mistral/mistral-small-latest
|
|
4
|
-
LLM_PLANNER=
|
|
4
|
+
LLM_PLANNER=mistral/mistral-small-latest
|
|
5
5
|
LLM_AGENT=mistral/mistral-small-latest
|
|
6
|
+
LLM_CONVERSATION_STORE=mistral/mistral-small-latest
|
|
6
7
|
|
|
7
8
|
SPEEDDICT_FOLDERNAME=___workflow_contexts
|
|
8
9
|
SYNTHETIC_UTTERANCE_GEN_NUMOF_PERSONAS=4
|
|
@@ -3,4 +3,5 @@ LITELLM_API_KEY_SYNDATA_GEN=<API KEY for synthetic data generation model>
|
|
|
3
3
|
LITELLM_API_KEY_PARAM_EXTRACTION=<API KEY for parameter extraction model>
|
|
4
4
|
LITELLM_API_KEY_RESPONSE_GEN=<API KEY for response generation model>
|
|
5
5
|
LITELLM_API_KEY_PLANNER=<API KEY for the agent's task planner model>
|
|
6
|
-
LITELLM_API_KEY_AGENT=<API KEY for the agent model>
|
|
6
|
+
LITELLM_API_KEY_AGENT=<API KEY for the agent model>
|
|
7
|
+
LITELLM_API_KEY_CONVERSATION_STORE=<API KEY for conversation topic/summary generation model>
|