fastworkflow 2.13.5__py3-none-any.whl → 2.14.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
  2. fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/go_up.py +1 -1
  3. fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/reset_context.py +1 -1
  4. fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +98 -166
  5. fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +7 -3
  6. fastworkflow/build/genai_postprocessor.py +143 -149
  7. fastworkflow/chat_session.py +42 -11
  8. fastworkflow/command_metadata_api.py +794 -0
  9. fastworkflow/command_routing.py +4 -1
  10. fastworkflow/examples/fastworkflow.env +1 -1
  11. fastworkflow/examples/fastworkflow.passwords.env +1 -0
  12. fastworkflow/examples/hello_world/_commands/add_two_numbers.py +1 -0
  13. fastworkflow/examples/retail_workflow/_commands/calculate.py +67 -0
  14. fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py +4 -1
  15. fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +13 -1
  16. fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -1
  17. fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py +6 -1
  18. fastworkflow/examples/retail_workflow/_commands/get_order_details.py +22 -10
  19. fastworkflow/examples/retail_workflow/_commands/get_product_details.py +12 -4
  20. fastworkflow/examples/retail_workflow/_commands/get_user_details.py +21 -5
  21. fastworkflow/examples/retail_workflow/_commands/list_all_product_types.py +4 -1
  22. fastworkflow/examples/retail_workflow/_commands/modify_pending_order_address.py +3 -0
  23. fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +12 -0
  24. fastworkflow/examples/retail_workflow/_commands/modify_pending_order_payment.py +7 -1
  25. fastworkflow/examples/retail_workflow/_commands/modify_user_address.py +3 -0
  26. fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +10 -1
  27. fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
  28. fastworkflow/examples/retail_workflow/tools/calculate.py +1 -1
  29. fastworkflow/mcp_server.py +52 -44
  30. fastworkflow/run/__main__.py +9 -5
  31. fastworkflow/run_agent/__main__.py +8 -8
  32. fastworkflow/run_agent/agent_module.py +6 -16
  33. fastworkflow/utils/command_dependency_graph.py +130 -143
  34. fastworkflow/utils/dspy_utils.py +11 -0
  35. fastworkflow/utils/signatures.py +7 -0
  36. fastworkflow/workflow_agent.py +186 -0
  37. {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/METADATA +12 -3
  38. {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/RECORD +41 -40
  39. fastworkflow/agent_integration.py +0 -239
  40. fastworkflow/examples/retail_workflow/_commands/parameter_dependency_graph.json +0 -36
  41. {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/LICENSE +0 -0
  42. {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/WHEEL +0 -0
  43. {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/entry_points.txt +0 -0
@@ -76,7 +76,10 @@ class RoutingDefinition(BaseModel):
76
76
  """Returns the list of command names available in the given context."""
77
77
  if context not in self.contexts:
78
78
  raise ValueError(f"Context '{context}' not found in the workflow.")
79
- return self.contexts[context]
79
+ commands = self.contexts[context]
80
+ if 'wildcard' in commands:
81
+ commands = [cmd for cmd in commands if cmd != 'wildcard']
82
+ return commands
80
83
 
81
84
  def get_command_class(self, command_name: str, module_type: ModuleType) -> Optional[Type[Any]]:
82
85
  """
@@ -1,7 +1,7 @@
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_AGENT=bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0
4
+ LLM_PLANNER=openrouter/openai/gpt-oss-20b:free
5
5
  LLM_AGENT=mistral/mistral-small-latest
6
6
 
7
7
  SPEEDDICT_FOLDERNAME=___workflow_contexts
@@ -2,4 +2,5 @@
2
2
  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
+ LITELLM_API_KEY_PLANNER=<API KEY for the agent's task planner model>
5
6
  LITELLM_API_KEY_AGENT=<API KEY for the agent model>
@@ -11,6 +11,7 @@ from ..application.add_two_numbers import add_two_numbers
11
11
 
12
12
 
13
13
  class Signature:
14
+ """Takes two floating point numbers and returns their sum"""
14
15
  class Input(BaseModel):
15
16
  first_num: float = Field(description="First number")
16
17
  second_num: float = Field(description="Second number")
@@ -0,0 +1,67 @@
1
+ from typing import List
2
+
3
+ import fastworkflow
4
+ from pydantic import BaseModel, Field, ConfigDict
5
+ from fastworkflow.workflow import Workflow
6
+ from fastworkflow import CommandOutput, CommandResponse
7
+ from fastworkflow.train.generate_synthetic import generate_diverse_utterances
8
+
9
+ from ..retail_data import load_data
10
+ from ..tools.calculate import Calculate
11
+
12
+
13
+ class Signature:
14
+ """Calculate a mathematical expression"""
15
+
16
+ class Input(BaseModel):
17
+ expression: str = Field(
18
+ default="NOT_FOUND",
19
+ description=(
20
+ "The mathematical expression to calculate. Allowed characters: digits, "
21
+ "+, -, *, /, parentheses, spaces."
22
+ ),
23
+ pattern=r"^(NOT_FOUND|[0-9+\-*/().\s]+)$",
24
+ examples=["(2 + 3) * 4", "10 / 2 + 3", "(8-3)/5"],
25
+ )
26
+
27
+ model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
28
+
29
+ class Output(BaseModel):
30
+ result: str = Field(
31
+ description=(
32
+ "Calculated result as a string. Returns an error message for invalid input."
33
+ )
34
+ )
35
+
36
+ plain_utterances: List[str] = [
37
+ "What is 2 + 2?",
38
+ "Calculate (2 + 3) * 4",
39
+ "Can you compute 10 / 2 + 3?",
40
+ "Evaluate (8-3)/5",
41
+ "Please calculate 7 * (6 - 2)",
42
+ ]
43
+
44
+ template_utterances: List[str] = []
45
+
46
+ @staticmethod
47
+ def generate_utterances(workflow: fastworkflow.Workflow, command_name: str) -> List[str]:
48
+ return [
49
+ command_name.split('/')[-1].lower().replace('_', ' ')
50
+ ] + generate_diverse_utterances(Signature.plain_utterances, command_name)
51
+
52
+
53
+ class ResponseGenerator:
54
+ def __call__(self, workflow: Workflow, command: str, command_parameters: Signature.Input) -> CommandOutput:
55
+ output = self._process_command(workflow, command_parameters)
56
+ response = f"Result: {output.result}"
57
+ return CommandOutput(
58
+ workflow_id=workflow.id,
59
+ command_responses=[CommandResponse(response=response)],
60
+ )
61
+
62
+ def _process_command(self, workflow: Workflow, input: Signature.Input) -> Signature.Output:
63
+ data = load_data()
64
+ result = Calculate.invoke(data=data, expression=input.expression)
65
+ return Signature.Output(result=result)
66
+
67
+
@@ -24,7 +24,10 @@ class Signature:
24
24
  default="NOT_FOUND",
25
25
  description="The order ID to cancel (must start with #)",
26
26
  pattern=r"^(#[\w\d]+|NOT_FOUND)$",
27
- examples=["#123", "#abc123", "#order456"]
27
+ examples=["#123", "#abc123", "#order456"],
28
+ json_schema_extra={
29
+ "available_from": ["get_user_details"]
30
+ }
28
31
  )
29
32
  ]
30
33
 
@@ -17,21 +17,33 @@ class Signature:
17
17
  description="The order ID to exchange (must start with #)",
18
18
  pattern=r"^(#[\w\d]+|NOT_FOUND)$",
19
19
  examples=["#W0000000"],
20
+ json_schema_extra={
21
+ "available_from": ["get_user_details"]
22
+ }
20
23
  )
21
24
  item_ids: List[str] = Field(
22
25
  default_factory=list,
23
26
  description="The item IDs to be exchanged",
24
27
  examples=["1008292230"],
28
+ json_schema_extra={
29
+ "available_from": ["get_order_details"]
30
+ }
25
31
  )
26
32
  new_item_ids: List[str] = Field(
27
33
  default_factory=list,
28
34
  description="The new item IDs to exchange for",
29
35
  examples=["1008292230"],
36
+ json_schema_extra={
37
+ "available_from": ["get_product_details"]
38
+ }
30
39
  )
31
40
  payment_method_id: str = Field(
32
41
  default="NOT_FOUND",
33
- description="Payment method ID for price difference. You can get this from order details->payment_history",
42
+ description="Payment method ID for price difference.",
34
43
  examples=["gift_card_0000000", "credit_card_0000000"],
44
+ json_schema_extra={
45
+ "available_from": ["get_order_details", "get_user_details"]
46
+ }
35
47
  )
36
48
 
37
49
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
@@ -28,7 +28,12 @@ class Signature:
28
28
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
29
29
 
30
30
  class Output(BaseModel):
31
- user_id: str = Field(description="User identifier returned by lookup.")
31
+ user_id: str = Field(
32
+ description="User identifier returned by lookup.",
33
+ json_schema_extra={
34
+ "used_by": ["get_user_details"]
35
+ }
36
+ )
32
37
 
33
38
  plain_utterances: List[str] = [
34
39
  "Can you tell me the ID linked to john.doe@example.com?",
@@ -35,7 +35,12 @@ class Signature:
35
35
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
36
36
 
37
37
  class Output(BaseModel):
38
- user_id: str = Field(description="User identifier returned by lookup.")
38
+ user_id: str = Field(
39
+ description="User identifier returned by lookup.",
40
+ json_schema_extra={
41
+ "used_by": ["get_user_details"]
42
+ }
43
+ )
39
44
 
40
45
  # ------------------------------------------------------------------
41
46
  # Utterances
@@ -16,24 +16,36 @@ class Signature:
16
16
  order_id: str = Field(
17
17
  default="NOT_FOUND",
18
18
  description=(
19
- "The order ID to get details for (must start with #). You can get order id's "
20
- "from get_user_details, given a user id."
19
+ "The order ID to get details for (must start with #)"
21
20
  ),
22
21
  pattern=r"^(#[\w\d]+|NOT_FOUND)$",
23
22
  examples=["#W0000000"],
23
+ json_schema_extra={
24
+ "available_from": ["get_user_details"]
25
+ }
24
26
  )
25
27
 
26
28
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
27
29
 
28
30
  class Output(BaseModel):
29
- order_details: str = Field(description=(
30
- "Detailed information about the order such as "
31
- "shipping address, "
32
- "items ordered with details including name, product and item id, price and options, "
33
- "fulfillments with details including tracking id and item ids, "
34
- "order status, and"
35
- "payment history with details including transaction type, amount and payment method id"
36
- ))
31
+ order_details: str = Field(
32
+ description=(
33
+ "Detailed information about the order such as "
34
+ "shipping address, "
35
+ "items ordered with details including name, product and item id, price and options, "
36
+ "fulfillments with details including tracking id and item ids, "
37
+ "order status, and"
38
+ "payment history with details including transaction type, amount and payment method id"
39
+ ),
40
+ json_schema_extra={
41
+ "used_by": [
42
+ "exchange_delivered_order_items",
43
+ "get_product_details",
44
+ "modify_pending_order_items",
45
+ "return_delivered_order_items",
46
+ ]
47
+ },
48
+ )
37
49
 
38
50
  plain_utterances: List[str] = [
39
51
  "Can you tell me what's going on with my order #W1234567?",
@@ -17,15 +17,23 @@ class Signature:
17
17
  description="The product ID (numeric string)",
18
18
  pattern=r"^(\d{10}|NOT_FOUND)$",
19
19
  examples=["6086499569"],
20
+ json_schema_extra={
21
+ "available_from": ["get_order_details", "list_all_product_types"]
22
+ }
20
23
  )
21
24
 
22
25
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
23
26
 
24
27
  class Output(BaseModel):
25
- product_details: str = Field(description=(
26
- "Detailed product information of all product variants "
27
- "including item id, options, availability and price"
28
- ))
28
+ product_details: str = Field(
29
+ description=(
30
+ "Detailed product information of all product variants of a given product type"
31
+ "including item id, options, availability and price"
32
+ ),
33
+ json_schema_extra={
34
+ "used_by": ["exchange_delivered_order_items", "modify_pending_order_items"]
35
+ }
36
+ )
29
37
 
30
38
  plain_utterances: List[str] = [
31
39
  "Can you give me more information about this product?",
@@ -17,16 +17,32 @@ class Signature:
17
17
  description="The user ID to get details for",
18
18
  pattern=r"^([a-z]+_[a-z]+_\d+|NOT_FOUND)$",
19
19
  examples=["sara_doe_496"],
20
+ json_schema_extra={
21
+ "available_from": ["find_user_id_by_email", "find_user_id_by_name_zip"]
22
+ }
20
23
  )
21
24
 
22
25
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
23
26
 
24
27
  class Output(BaseModel):
25
- user_details: str = Field(description=(
26
- "Detailed user information such as "
27
- "first and last name, address, email, payment methods and "
28
- "the list of order id's"
29
- ))
28
+ user_details: str = Field(
29
+ description=(
30
+ "Detailed user information such as "
31
+ "first and last name, address, email, payment methods and "
32
+ "the list of order id's"
33
+ ),
34
+ json_schema_extra={
35
+ "used_by": [
36
+ "cancel_pending_order",
37
+ "exchange_delivered_order_items",
38
+ "get_order_details",
39
+ "modify_pending_order_address",
40
+ "modify_pending_order_items",
41
+ "modify_pending_order_payment",
42
+ "return_delivered_order_items",
43
+ ]
44
+ },
45
+ )
30
46
 
31
47
  plain_utterances: List[str] = [
32
48
  "Can you pull up my account info?",
@@ -20,7 +20,10 @@ class Signature:
20
20
 
21
21
  class Output(BaseModel):
22
22
  status: str = Field(
23
- description="List of product type names separated by comma, or a JSON string representation of that list.",
23
+ description="List of product type and product id tuples, or a JSON string representation of that list.",
24
+ json_schema_extra={
25
+ "used_by": ["get_product_details"]
26
+ }
24
27
  )
25
28
 
26
29
  # ---------------------------------------------------------------------
@@ -17,6 +17,9 @@ class Signature:
17
17
  description="The order ID to modify (must start with #)",
18
18
  pattern=r"^(#W\d+|NOT_FOUND)$",
19
19
  examples=["#W0000000"],
20
+ json_schema_extra={
21
+ "available_from": ["get_user_details"]
22
+ }
20
23
  )
21
24
  address1: str = Field(default="NOT_FOUND", description="First line of address", examples=["123 Main St"])
22
25
  address2: str = Field(default="NOT_FOUND", description="Second line of address", examples=["Apt 1"])
@@ -17,22 +17,34 @@ class Signature:
17
17
  description="The order ID to modify (must start with #)",
18
18
  pattern=r"^(#W\d+|NOT_FOUND)$",
19
19
  examples=["#W0000000"],
20
+ json_schema_extra={
21
+ "available_from": ["get_user_details"]
22
+ }
20
23
  )
21
24
  item_ids: List[str] = Field(
22
25
  default_factory=list,
23
26
  description="List of item IDs to be modified",
24
27
  examples=["1008292230", "2468135790"],
28
+ json_schema_extra={
29
+ "available_from": ["get_order_details"]
30
+ }
25
31
  )
26
32
  new_item_ids: List[str] = Field(
27
33
  default_factory=list,
28
34
  description="List of new item IDs to replace with",
29
35
  examples=["1008292231", "2468135791"],
36
+ json_schema_extra={
37
+ "available_from": ["get_product_details"]
38
+ }
30
39
  )
31
40
  payment_method_id: str = Field(
32
41
  default="NOT_FOUND",
33
42
  description="Payment method ID for price differences",
34
43
  pattern=r"^((gift_card|credit_card)_\d+|NOT_FOUND)$",
35
44
  examples=["gift_card_0000000", "credit_card_0000000"],
45
+ json_schema_extra={
46
+ "available_from": ["get_order_details", "get_user_details"]
47
+ }
36
48
  )
37
49
 
38
50
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
@@ -10,19 +10,25 @@ from ..tools.modify_pending_order_payment import ModifyPendingOrderPayment
10
10
 
11
11
 
12
12
  class Signature:
13
- """Modify pending order payment"""
13
+ """Modify pending order payment method"""
14
14
  class Input(BaseModel):
15
15
  order_id: str = Field(
16
16
  default="NOT_FOUND",
17
17
  description="The order ID to modify (must start with #)",
18
18
  pattern=r"^(#W\d+|NOT_FOUND)$",
19
19
  examples=["#W0000000"],
20
+ json_schema_extra={
21
+ "available_from": ["get_user_details"]
22
+ }
20
23
  )
21
24
  payment_method_id: str = Field(
22
25
  default="NOT_FOUND",
23
26
  description="Payment method ID to switch to",
24
27
  pattern=r"^((gift_card|credit_card)_\d+|NOT_FOUND)$",
25
28
  examples=["gift_card_0000000", "credit_card_0000000"],
29
+ json_schema_extra={
30
+ "available_from": ["get_user_details"]
31
+ }
26
32
  )
27
33
 
28
34
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
@@ -17,6 +17,9 @@ class Signature:
17
17
  description="The user ID to modify",
18
18
  pattern=r"^([a-z]+_[a-z]+_\d+|NOT_FOUND)$",
19
19
  examples=["sara_doe_496"],
20
+ json_schema_extra={
21
+ "available_from": ["find_user_id_by_email", "find_user_id_by_name_zip"]
22
+ }
20
23
  )
21
24
  address1: str = Field(default="NOT_FOUND", description="First line of address", examples=["123 Main St"])
22
25
  address2: str = Field(default="NOT_FOUND", description="Second line of address", examples=["Apt 1"])
@@ -16,18 +16,27 @@ class Signature:
16
16
  default="NOT_FOUND",
17
17
  description="The order ID for return (must start with #)",
18
18
  pattern=r"^(#W\d+|NOT_FOUND)$",
19
- examples=["#W0000000"],
19
+ examples=["#W3456890"],
20
+ json_schema_extra={
21
+ "available_from": ["get_user_details"]
22
+ }
20
23
  )
21
24
  item_ids: List[str] = Field(
22
25
  default_factory=list,
23
26
  description="List of item IDs to be returned",
24
27
  examples=["1008292230"],
28
+ json_schema_extra={
29
+ "available_from": ["get_order_details"]
30
+ }
25
31
  )
26
32
  payment_method_id: str = Field(
27
33
  default="NOT_FOUND",
28
34
  description="Payment method ID for refund",
29
35
  pattern=r"^((gift_card|credit_card)_\d+|NOT_FOUND)$",
30
36
  examples=["gift_card_0000000", "credit_card_0000000"],
37
+ json_schema_extra={
38
+ "available_from": ["get_order_details"]
39
+ }
31
40
  )
32
41
 
33
42
  model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
@@ -10,7 +10,7 @@ from ..tools.transfer_to_human_agents import TransferToHumanAgents
10
10
 
11
11
 
12
12
  class Signature:
13
- """Transfer to a human agent"""
13
+ """Transfer to a human agent as the last resort"""
14
14
  class Input(BaseModel):
15
15
  summary: str = Field(
16
16
  default="NOT_FOUND",
@@ -7,7 +7,7 @@ from .tool import Tool
7
7
  class Calculate(Tool):
8
8
  @staticmethod
9
9
  def invoke(data: Dict[str, Any], expression: str) -> str:
10
- if not all(char in "0123456789+-*/(). " for char in expression):
10
+ if any(char not in "0123456789+-*/(). " for char in expression):
11
11
  return "Error: invalid characters in expression"
12
12
  try:
13
13
  # Evaluate the mathematical expression safely
@@ -11,6 +11,7 @@ from fastworkflow.command_executor import CommandExecutor
11
11
  from fastworkflow.command_directory import CommandDirectory
12
12
  from fastworkflow.command_routing import RoutingDefinition, RoutingRegistry, ModuleType
13
13
  from uuid import uuid4
14
+ from fastworkflow.command_metadata_api import CommandMetadataAPI
14
15
 
15
16
 
16
17
  class FastWorkflowMCPServer:
@@ -50,64 +51,68 @@ class FastWorkflowMCPServer:
50
51
  active_ctx = active_ctx_name if active_ctx_name in routing.contexts else '*'
51
52
  command_names = routing.get_command_names(active_ctx)
52
53
 
54
+ # Centralized command metadata (docstrings, inputs, plain_utterances)
55
+ cme_path = fastworkflow.get_internal_workflow_path("command_metadata_extraction")
56
+ enhanced_meta = CommandMetadataAPI.get_enhanced_command_info(
57
+ subject_workflow_path=workflow_folderpath,
58
+ cme_workflow_path=cme_path,
59
+ active_context_name=active_ctx_name,
60
+ )
61
+ meta_by_fq = {m.get("qualified_name"): m for m in enhanced_meta.get("commands", [])}
62
+
63
+ # Centralized parameters for building schemas
64
+ params_by_cmd = CommandMetadataAPI.get_params_for_all_commands(workflow_folderpath)
65
+
53
66
  tools = []
54
67
  for command_name in command_names:
55
- # Get command parameters class to build schema
56
- command_parameters_class = routing.get_command_class(
57
- command_name,
58
- ModuleType.COMMAND_PARAMETERS_CLASS
59
- )
60
-
61
- # Build JSON schema from Pydantic model
68
+ # Centralized metadata for this command (if any)
69
+ meta_for_cmd = meta_by_fq.get(command_name)
70
+ # Build JSON schema from centralized API params
62
71
  input_schema = {
63
72
  "type": "object",
64
73
  "properties": {},
65
74
  "required": []
66
75
  }
67
76
 
68
- description = f"Executes {command_name}"
69
-
70
- if command_parameters_class:
71
- field_descriptions = []
72
- model_fields = command_parameters_class.model_fields
73
- for field_name, field_info in model_fields.items():
74
- # Convert Pydantic field to JSON schema property
75
- prop = {"type": "string"} # Default type
76
- if hasattr(field_info, 'description') and field_info.description:
77
- prop["description"] = field_info.description
78
- field_descriptions.append(field_info.description)
79
- else:
80
- field_descriptions.append(field_name)
81
-
82
- if hasattr(field_info, 'default') and field_info.default:
83
- prop["default"] = field_info.default
84
- # Add to required if default is NOT_FOUND
85
- if field_info.default == NOT_FOUND:
86
- input_schema["required"].append(field_name)
87
- elif field_info.is_required():
77
+ # Build input schema and required fields
78
+ field_descriptions = []
79
+ for param in params_by_cmd.get(command_name, {}).get("inputs", []):
80
+ field_name = param.get("name") or "param"
81
+ prop = {"type": "string"}
82
+ if desc := param.get("description"):
83
+ prop["description"] = desc
84
+ field_descriptions.append(desc)
85
+ else:
86
+ field_descriptions.append(field_name)
87
+ if (default := param.get("default")) is not None:
88
+ if default == NOT_FOUND:
88
89
  input_schema["required"].append(field_name)
89
-
90
- input_schema["properties"][field_name] = prop
91
-
92
- if field_descriptions:
93
- description = f"Input: {', '.join(field_descriptions)}"
94
- if command_parameters_class.__doc__:
95
- description = f"{description}. {command_parameters_class.__doc__.strip()}"
90
+ input_schema["properties"][field_name] = prop
91
+
92
+ # Use centralized display generator for a single command for rich description
93
+ description = CommandMetadataAPI.get_command_display_text_for_command(
94
+ subject_workflow_path=workflow_folderpath,
95
+ cme_workflow_path=cme_path,
96
+ active_context_name=active_ctx_name,
97
+ qualified_command_name=command_name,
98
+ for_agents=True,
99
+ omit_command_name=True,
100
+ )
96
101
 
97
102
  # Add standard FastWorkflow parameters
98
- input_schema["properties"]["command"] = {
99
- "type": "string",
100
- "description": "Natural language command or query"
101
- }
103
+ # input_schema["properties"]["command"] = {
104
+ # "type": "string",
105
+ # "description": "Natural language command or query"
106
+ # }
102
107
 
103
- input_schema["properties"]["workitem_path"] = {
104
- "type": "string",
105
- "description": "Command context (optional)",
106
- "default": active_ctx
107
- }
108
+ # input_schema["properties"]["workitem_path"] = {
109
+ # "type": "string",
110
+ # "description": "Command context (optional)",
111
+ # "default": active_ctx
112
+ # }
108
113
 
109
114
  tool_def = {
110
- "name": command_name,
115
+ "name": command_name.split("/")[-1],
111
116
  "description": description,
112
117
  "inputSchema": input_schema,
113
118
  "annotations": {
@@ -118,6 +123,9 @@ class FastWorkflowMCPServer:
118
123
  "openWorldHint": True # FastWorkflow can interact with external systems
119
124
  }
120
125
  }
126
+ # Attach plain_utterances from centralized metadata if present
127
+ if meta_for_cmd and meta_for_cmd.get("plain_utterances"):
128
+ tool_def["annotations"]["plain_utterances"] = meta_for_cmd["plain_utterances"]
121
129
  tools.append(tool_def)
122
130
 
123
131
  return tools
@@ -73,14 +73,18 @@ def run_main(args):
73
73
  all_renderables.append(Rule(style="dim"))
74
74
 
75
75
  # Process each command response
76
+ num_responses = len(command_output.command_responses)
76
77
  for i, command_response in enumerate(command_output.command_responses, start=1):
77
78
  if command_response.response:
78
- all_renderables.extend(
79
- (
80
- Text(f"Command Response {i}", style="bold green"),
81
- Text(command_response.response, style="green")
79
+ if num_responses > 1:
80
+ all_renderables.extend(
81
+ (
82
+ Text(f"Command Response {i}", style="bold green"),
83
+ Text(command_response.response, style="green")
84
+ )
82
85
  )
83
- )
86
+ else:
87
+ all_renderables.append(Text(command_response.response, style="green"))
84
88
 
85
89
  if command_response.artifacts:
86
90
  all_renderables.extend(