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.
Files changed (42) hide show
  1. fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
  2. fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
  3. fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
  4. fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
  5. fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
  6. fastworkflow/chat_session.py +379 -206
  7. fastworkflow/cli.py +80 -165
  8. fastworkflow/command_context_model.py +73 -7
  9. fastworkflow/command_executor.py +14 -5
  10. fastworkflow/command_metadata_api.py +106 -6
  11. fastworkflow/examples/fastworkflow.env +2 -1
  12. fastworkflow/examples/fastworkflow.passwords.env +2 -1
  13. fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
  14. fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
  15. fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
  16. fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
  17. fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
  18. fastworkflow/intent_clarification_agent.py +131 -0
  19. fastworkflow/mcp_server.py +3 -3
  20. fastworkflow/run/__main__.py +33 -40
  21. fastworkflow/run_fastapi_mcp/README.md +373 -0
  22. fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
  23. fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
  24. fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
  25. fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
  26. fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
  27. fastworkflow/run_fastapi_mcp/utils.py +517 -0
  28. fastworkflow/train/__main__.py +1 -1
  29. fastworkflow/utils/chat_adapter.py +99 -0
  30. fastworkflow/utils/python_utils.py +4 -4
  31. fastworkflow/utils/react.py +258 -0
  32. fastworkflow/utils/signatures.py +338 -139
  33. fastworkflow/workflow.py +1 -5
  34. fastworkflow/workflow_agent.py +185 -133
  35. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
  36. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
  37. fastworkflow/run_agent/__main__.py +0 -294
  38. fastworkflow/run_agent/agent_module.py +0 -194
  39. /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
  40. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
  41. {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
  42. {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 examples train {example_name}")
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 examples train {example_name}")
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 examples train {example_name}")
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 examples train {example_name}")
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 run_example(args):
430
- # sourcery skip: extract-duplicate-method, extract-method, remove-redundant-fstring, split-or-ifs
431
- """Run an existing example workflow."""
432
- # Create a spinner for the initial check
433
- spinner = Spinner("dots", text="[bold green]Preparing to run example...[/bold green]")
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
- with Live(spinner, refresh_per_second=10):
436
- # Check if example exists in the local examples directory
437
- local_examples_dir = Path("./examples")
438
- workflow_path = local_examples_dir / args.name
439
-
440
- if workflow_path.is_dir():
441
- # Get the appropriate env files for this example workflow
442
- env_file_path, passwords_file_path = find_default_env_files(local_examples_dir)
443
-
444
- # Check if the files exist
445
- env_file = Path(env_file_path)
446
- passwords_file = Path(passwords_file_path)
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 env_file.exists() or not passwords_file.exists():
459
- rprint(f"[bold red]Error:[/bold red] Required environment files not found:")
460
- if not env_file.exists():
461
- rprint(f" - {env_file} (not found)")
462
- if not passwords_file.exists():
463
- rprint(f" - {passwords_file} (not found)")
464
- rprint("\nPlease run the following command to fetch the example and its environment files:")
465
- rprint(f" fastworkflow examples fetch {args.name}")
466
- rprint("\nAfter fetching, edit the passwords file to add your API keys:")
467
- rprint(f" {local_examples_dir}/fastworkflow.passwords.env")
468
- rprint("\nThen train the example before running it:")
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
- # Check if the example has been trained
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
- "-m", "fastworkflow.run",
489
- str(workflow_path),
490
- str(env_file),
491
- str(passwords_file)
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
- # Forward agent mode flag if requested
495
- if getattr(args, "run_as_agent", False):
496
- cmd.append("--run_as_agent")
497
-
498
- try:
499
- rprint(f"[bold green]Starting interactive session...[/bold green]")
500
- # Replace the current process with the run command
501
- # This ensures that the interactive prompt works correctly
502
- os.execvp(sys.executable, cmd)
503
- except Exception as e:
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 CommandDirectory, get_cached_command_directory
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
+ }
@@ -45,16 +45,21 @@ class CommandExecutor(CommandExecutorInterface):
45
45
  command = command)
46
46
  )
47
47
 
48
- if command_output.command_handled or not command_output.success:
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 = ChatSession.get_active_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 not rendered.strip():
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=openrouter/openai/gpt-oss-20b:free
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>