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
@@ -9,6 +9,9 @@ from typing import Optional
9
9
  from dotenv import dotenv_values
10
10
  from queue import Empty
11
11
 
12
+ import dspy
13
+
14
+
12
15
  # Instantiate a global console for consistent styling
13
16
  console = None
14
17
 
@@ -44,6 +47,7 @@ def main():
44
47
 
45
48
 
46
49
  import fastworkflow
50
+ from fastworkflow.utils import dspy_utils
47
51
  from fastworkflow.command_executor import CommandExecutor
48
52
  from .agent_module import initialize_dspy_agent
49
53
 
@@ -174,7 +178,7 @@ def main():
174
178
  exit(1)
175
179
 
176
180
  # this could be None
177
- LITELLM_API_KEY_AGENT = fastworkflow.get_env_var("LITELLM_API_KEY_AGENT")
181
+ lm = dspy_utils.get_lm("LLM_AGENT", "LITELLM_API_KEY_AGENT")
178
182
 
179
183
  startup_action: Optional[fastworkflow.Action] = None
180
184
  if args.startup_action:
@@ -204,12 +208,7 @@ def main():
204
208
  StartupProgress.end()
205
209
 
206
210
  try:
207
- react_agent = initialize_dspy_agent(
208
- fastworkflow.chat_session,
209
- LLM_AGENT,
210
- LITELLM_API_KEY_AGENT,
211
- clear_cache=True
212
- )
211
+ react_agent = initialize_dspy_agent(fastworkflow.chat_session)
213
212
  except (EnvironmentError, RuntimeError) as e:
214
213
  console.print(f"[bold red]Failed to initialize DSPy agent:[/bold red] {e}")
215
214
  exit(1)
@@ -240,7 +239,8 @@ def main():
240
239
  # Function to run agent processing in a separate thread
241
240
  def process_agent_query():
242
241
  try:
243
- agent_response_container["response"] = react_agent(user_query=user_input_str)
242
+ with dspy.context(lm=lm):
243
+ agent_response_container["response"] = react_agent(user_query=user_input_str)
244
244
  except Exception as e:
245
245
  agent_response_container["error"] = e
246
246
 
@@ -23,7 +23,9 @@ clarification_response_queue: Queue[str] = Queue()
23
23
  # DSPy Signature for the High-Level Planning Agent
24
24
  class PlanningAgentSignature(dspy.Signature):
25
25
  """
26
- Prepare and execute a plan for building the final answer using the WorkflowAssistant tool.
26
+ Create a minimal step based todo list based only on the commands in the user query
27
+ Then, execute the plan for building the final answer using the WorkflowAssistant tool.
28
+ Double-check that all the tasks in the todo list have been completed before returning the final answer.
27
29
  """
28
30
  user_query = dspy.InputField(desc="The user's full input or question.")
29
31
  final_answer = dspy.OutputField(desc="The agent's comprehensive response to the user after interacting with the workflow.")
@@ -76,7 +78,7 @@ def _format_mcp_result_for_agent(mcp_result) -> str:
76
78
 
77
79
  def _build_assistant_tool_documentation(available_tools: List[Dict]) -> str:
78
80
  """Build simplified tool documentation for the main agent's WorkflowAssistant tool."""
79
-
81
+
80
82
  # Guidance for the MAIN AGENT on how to call WorkflowAssistant
81
83
  main_agent_guidance = """
82
84
  Use the WorkflowAssistant to interact with a suite of underlying tools to assist the user.
@@ -91,7 +93,7 @@ def _build_assistant_tool_documentation(available_tools: List[Dict]) -> str:
91
93
  tool_docs = []
92
94
  for tool_def in available_tools:
93
95
  tool_name = tool_def['name']
94
- tool_desc = tool_def['description']
96
+ tool_desc = tool_def['description'].split("\n")[0]
95
97
 
96
98
  # Main agent does not need the detailed input schema, only name, description and parameters.
97
99
  tool_docs.append(
@@ -144,30 +146,18 @@ def _ask_user_tool(prompt: str) -> str:
144
146
  return clarification_response_queue.get()
145
147
 
146
148
 
147
- def initialize_dspy_agent(chat_session: fastworkflow.ChatSession, LLM_AGENT: str, LITELLM_API_KEY_AGENT: Optional[str] = None, max_iters: int = 25, clear_cache: bool = False):
149
+ def initialize_dspy_agent(chat_session: fastworkflow.ChatSession, max_iters: int = 25):
148
150
  """
149
151
  Configures and returns a high-level DSPy ReAct planning agent.
150
152
  The workflow tool agent is already integrated in the ChatSession.
151
153
 
152
154
  Args:
153
155
  chat_session: ChatSession instance (should be in agent mode)
154
- LLM_AGENT: Language model name
155
- LITELLM_API_KEY_AGENT: API key for the language model
156
156
  max_iters: Maximum iterations for the ReAct agent
157
- clear_cache: If True, clears DSPy cache before initialization
158
157
 
159
158
  Raises:
160
159
  EnvironmentError: If LLM_AGENT is not set.
161
- RuntimeError: If there's an error configuring the DSPy LM.
162
160
  """
163
- if not LLM_AGENT:
164
- print(f"{Fore.RED}Error: DSPy Language Model name not provided.{Style.RESET_ALL}")
165
- raise EnvironmentError("DSPy Language Model name not provided.")
166
-
167
- # Configure DSPy LM for the high-level agent
168
- lm = dspy.LM(model=LLM_AGENT, api_key=LITELLM_API_KEY_AGENT)
169
- dspy.settings.configure(lm=lm)
170
-
171
161
  # Get available tools for documentation
172
162
  mcp_server = FastWorkflowMCPServer(chat_session)
173
163
  available_tools = mcp_server.list_tools()
@@ -1,16 +1,17 @@
1
1
  import json
2
2
  import os
3
3
  from dataclasses import dataclass
4
- from typing import Dict, List, Any, Optional
4
+ from typing import Dict, List, Any
5
5
 
6
6
  from sentence_transformers import SentenceTransformer, util as st_util
7
7
 
8
- import dspy # type: ignore
8
+ # import dspy # type: ignore
9
9
 
10
- import fastworkflow # For env configuration when using DSPy
10
+ # import fastworkflow # For env configuration when using DSPy
11
11
  from fastworkflow.command_directory import CommandDirectory
12
12
  from fastworkflow.command_routing import RoutingDefinition
13
13
  from fastworkflow.utils import python_utils
14
+ from fastworkflow.command_metadata_api import CommandMetadataAPI
14
15
 
15
16
 
16
17
  @dataclass
@@ -27,53 +28,39 @@ class CommandParams:
27
28
  outputs: List[ParamMeta]
28
29
 
29
30
 
30
- def _serialize_type_str(t: Any) -> str:
31
- try:
32
- # Basic serialization for common typing and classes
33
- return t.__name__ if hasattr(t, "__name__") else str(t)
34
- except Exception:
35
- return str(t)
31
+ # def _serialize_type_str(t: Any) -> str:
32
+ # try:
33
+ # # Basic serialization for common typing and classes
34
+ # return t.__name__ if hasattr(t, "__name__") else str(t)
35
+ # except Exception:
36
+ # return str(t)
36
37
 
37
38
 
38
39
  def _collect_command_params(workflow_path: str) -> Dict[str, CommandParams]:
39
- directory = CommandDirectory.load(workflow_path)
40
- routing = RoutingDefinition.build(workflow_path)
40
+ params_map = CommandMetadataAPI.get_params_for_all_commands(workflow_path)
41
41
 
42
42
  results: Dict[str, CommandParams] = {}
43
-
44
- for qualified_name in directory.get_commands():
45
- # Hydrate metadata and import module
46
- directory.ensure_command_hydrated(qualified_name)
47
- metadata = directory.get_command_metadata(qualified_name)
48
- module_path = metadata.parameter_extraction_signature_module_path or metadata.response_generation_module_path
49
- if not module_path:
50
- continue
51
- module = python_utils.get_module(str(module_path), workflow_path)
52
- signature_cls = getattr(module, "Signature", None)
53
- if not signature_cls:
54
- continue
55
-
56
- inputs: List[ParamMeta] = []
57
- outputs: List[ParamMeta] = []
58
-
59
- InputModel = getattr(signature_cls, "Input", None)
60
- if InputModel is not None and hasattr(InputModel, "model_fields"):
61
- for name, field in InputModel.model_fields.items():
62
- desc = getattr(field, "description", "") or ""
63
- examples = getattr(field, "examples", []) or []
64
- type_str = _serialize_type_str(field.annotation)
65
- inputs.append(ParamMeta(name=name, type_str=type_str, description=str(desc), examples=list(examples)))
66
-
67
- OutputModel = getattr(signature_cls, "Output", None)
68
- if OutputModel is not None and hasattr(OutputModel, "model_fields"):
69
- for name, field in OutputModel.model_fields.items():
70
- desc = getattr(field, "description", "") or ""
71
- examples = getattr(field, "examples", []) or []
72
- type_str = _serialize_type_str(field.annotation)
73
- outputs.append(ParamMeta(name=name, type_str=type_str, description=str(desc), examples=list(examples)))
74
-
75
- if contexts := routing.get_contexts_for_command(qualified_name):
76
- results[qualified_name] = CommandParams(inputs=inputs, outputs=outputs)
43
+ for qualified_name, io in params_map.items():
44
+ inputs = [
45
+ ParamMeta(
46
+ name=p.get("name", ""),
47
+ type_str=str(p.get("type_str", "")),
48
+ description=str(p.get("description", "")),
49
+ examples=list(p.get("examples", []) or []),
50
+ )
51
+ for p in io.get("inputs", [])
52
+ ]
53
+ outputs = [
54
+ ParamMeta(
55
+ name=p.get("name", ""),
56
+ type_str=str(p.get("type_str", "")),
57
+ description=str(p.get("description", "")),
58
+ examples=list(p.get("examples", []) or []),
59
+ )
60
+ for p in io.get("outputs", [])
61
+ ]
62
+ # Include all discovered commands; context overlap is handled later
63
+ results[qualified_name] = CommandParams(inputs=inputs, outputs=outputs)
77
64
 
78
65
  return results
79
66
 
@@ -129,112 +116,112 @@ def _semantic_match(out_param: ParamMeta, in_param: ParamMeta, threshold: float
129
116
  # ----------------------------------------------------------------------------
130
117
 
131
118
  # Lazy singletons
132
- _llm_initialized: bool = False # type: ignore
133
- _llm_module: Optional["CommandDependencyModule"] = None # type: ignore
119
+ # _llm_initialized: bool = False # type: ignore
120
+ # _llm_module: Optional["CommandDependencyModule"] = None # type: ignore
134
121
 
135
122
 
136
- def _initialize_dspy_llm_if_needed() -> None:
137
- """Initialize DSPy LM once using FastWorkflow environment.
123
+ # def _initialize_dspy_llm_if_needed() -> None:
124
+ # """Initialize DSPy LM once using FastWorkflow environment.
138
125
 
139
- Controlled by env vars:
140
- - LLM_COMMAND_METADATA_GEN (model id for LiteLLM via DSPy)
141
- - LITELLM_API_KEY_COMMANDMETADATA_GEN (API key)
142
- """
143
- global _llm_initialized, _llm_module
144
- if _llm_initialized:
145
- return
146
-
147
- model = fastworkflow.get_env_var("LLM_COMMAND_METADATA_GEN")
148
- api_key = fastworkflow.get_env_var("LITELLM_API_KEY_COMMANDMETADATA_GEN")
149
- lm = dspy.LM(model=model, api_key=api_key, max_tokens=1000)
150
- dspy.settings.configure(lm=lm)
151
-
152
- # Define signature and module only if dspy is available
153
- class CommandDependencySignature(dspy.Signature): # type: ignore
154
- """Analyze if two commands have a dependency relationship.
155
-
156
- There is a dependency relationship if and only if the outputs from one command can be used directly as inputs of the other.
157
- Tip for figuring out dependency direction: Commands with hard-to-remember inputs (such as id) typically depend on commands with easy to remember inputs (such as name, email).
158
- """
159
-
160
- cmd_x_name: str = dspy.InputField(desc="Name of command X")
161
- cmd_x_inputs: str = dspy.InputField(desc="Input parameters of command X (name:type)")
162
- cmd_x_outputs: str = dspy.InputField(desc="Output parameters of command X (name:type)")
163
-
164
- cmd_y_name: str = dspy.InputField(desc="Name of command Y")
165
- cmd_y_inputs: str = dspy.InputField(desc="Input parameters of command Y (name:type)")
166
- cmd_y_outputs: str = dspy.InputField(desc="Output parameters of command Y (name:type)")
167
-
168
- has_dependency: bool = dspy.OutputField(
169
- desc="True if there's a dependency between the commands"
170
- )
171
- direction: str = dspy.OutputField(
172
- desc="Direction: 'x_depends_on_y', 'y_depends_on_x', or 'none'"
173
- )
126
+ # Controlled by env vars:
127
+ # - LLM_COMMAND_METADATA_GEN (model id for LiteLLM via DSPy)
128
+ # - LITELLM_API_KEY_COMMANDMETADATA_GEN (API key)
129
+ # """
130
+ # global _llm_initialized, _llm_module
131
+ # if _llm_initialized:
132
+ # return
174
133
 
175
- class CommandDependencyModule(dspy.Module): # type: ignore
176
- def __init__(self):
177
- super().__init__()
178
- self.generate = dspy.ChainOfThought(CommandDependencySignature)
179
-
180
- def forward(
181
- self,
182
- cmd_x_name: str,
183
- cmd_x_inputs: str,
184
- cmd_x_outputs: str,
185
- cmd_y_name: str,
186
- cmd_y_inputs: str,
187
- cmd_y_outputs: str,
188
- ) -> tuple[bool, str]:
189
- prediction = self.generate(
190
- cmd_x_name=cmd_x_name,
191
- cmd_x_inputs=cmd_x_inputs,
192
- cmd_x_outputs=cmd_x_outputs,
193
- cmd_y_name=cmd_y_name,
194
- cmd_y_inputs=cmd_y_inputs,
195
- cmd_y_outputs=cmd_y_outputs,
196
- )
197
- return prediction.has_dependency, prediction.direction
134
+ # model = fastworkflow.get_env_var("LLM_COMMAND_METADATA_GEN")
135
+ # api_key = fastworkflow.get_env_var("LITELLM_API_KEY_COMMANDMETADATA_GEN")
136
+ # lm = dspy.LM(model=model, api_key=api_key, max_tokens=1000)
198
137
 
199
- _llm_module = CommandDependencyModule()
200
- _llm_initialized = True
138
+ # # Define signature and module only if dspy is available
139
+ # class CommandDependencySignature(dspy.Signature): # type: ignore
140
+ # """Analyze if two commands have a dependency relationship.
201
141
 
142
+ # There is a dependency relationship if and only if the outputs from one command can be used directly as inputs of the other.
143
+ # Tip for figuring out dependency direction: Commands with hard-to-remember inputs (such as id) typically depend on commands with easy to remember inputs (such as name, email).
144
+ # """
202
145
 
203
- def _llm_command_dependency(
204
- cmd_x_name: str,
205
- cmd_x_params: CommandParams,
206
- cmd_y_name: str,
207
- cmd_y_params: CommandParams
208
- ) -> Optional[str]:
209
- """Check if two commands have a dependency using LLM.
146
+ # cmd_x_name: str = dspy.InputField(desc="Name of command X")
147
+ # cmd_x_inputs: str = dspy.InputField(desc="Input parameters of command X (name:type)")
148
+ # cmd_x_outputs: str = dspy.InputField(desc="Output parameters of command X (name:type)")
149
+
150
+ # cmd_y_name: str = dspy.InputField(desc="Name of command Y")
151
+ # cmd_y_inputs: str = dspy.InputField(desc="Input parameters of command Y (name:type)")
152
+ # cmd_y_outputs: str = dspy.InputField(desc="Output parameters of command Y (name:type)")
153
+
154
+ # has_dependency: bool = dspy.OutputField(
155
+ # desc="True if there's a dependency between the commands"
156
+ # )
157
+ # direction: str = dspy.OutputField(
158
+ # desc="Direction: 'x_depends_on_y', 'y_depends_on_x', or 'none'"
159
+ # )
160
+
161
+ # class CommandDependencyModule(dspy.Module): # type: ignore
162
+ # def __init__(self):
163
+ # super().__init__()
164
+ # self.generate = dspy.ChainOfThought(CommandDependencySignature)
165
+
166
+ # def forward(
167
+ # self,
168
+ # cmd_x_name: str,
169
+ # cmd_x_inputs: str,
170
+ # cmd_x_outputs: str,
171
+ # cmd_y_name: str,
172
+ # cmd_y_inputs: str,
173
+ # cmd_y_outputs: str,
174
+ # ) -> tuple[bool, str]:
175
+ # with dspy.context(lm=lm):
176
+ # prediction = self.generate(
177
+ # cmd_x_name=cmd_x_name,
178
+ # cmd_x_inputs=cmd_x_inputs,
179
+ # cmd_x_outputs=cmd_x_outputs,
180
+ # cmd_y_name=cmd_y_name,
181
+ # cmd_y_inputs=cmd_y_inputs,
182
+ # cmd_y_outputs=cmd_y_outputs,
183
+ # )
184
+ # return prediction.has_dependency, prediction.direction
185
+
186
+ # _llm_module = CommandDependencyModule()
187
+ # _llm_initialized = True
188
+
189
+
190
+ # def _llm_command_dependency(
191
+ # cmd_x_name: str,
192
+ # cmd_x_params: CommandParams,
193
+ # cmd_y_name: str,
194
+ # cmd_y_params: CommandParams
195
+ # ) -> Optional[str]:
196
+ # """Check if two commands have a dependency using LLM.
210
197
 
211
- Returns:
212
- - "x_to_y" if Y depends on X (X's outputs feed Y's inputs)
213
- - "y_to_x" if X depends on Y (Y's outputs feed X's inputs)
214
- - None if no dependency
215
- """
216
- _initialize_dspy_llm_if_needed()
217
-
218
- # Format parameters for LLM
219
- x_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.inputs])
220
- x_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.outputs])
221
- y_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.inputs])
222
- y_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.outputs])
223
-
224
- has_dep, direction = _llm_module(
225
- cmd_x_name=cmd_x_name,
226
- cmd_x_inputs=x_inputs or "none",
227
- cmd_x_outputs=x_outputs or "none",
228
- cmd_y_name=cmd_y_name,
229
- cmd_y_inputs=y_inputs or "none",
230
- cmd_y_outputs=y_outputs or "none",
231
- )
232
-
233
- if not has_dep or direction == "none":
234
- return None
235
- if direction == "x_depends_on_y":
236
- return "y_to_x" # Y's outputs -> X's inputs
237
- return "x_to_y" if direction == "y_depends_on_x" else None
198
+ # Returns:
199
+ # - "x_to_y" if Y depends on X (X's outputs feed Y's inputs)
200
+ # - "y_to_x" if X depends on Y (Y's outputs feed X's inputs)
201
+ # - None if no dependency
202
+ # """
203
+ # _initialize_dspy_llm_if_needed()
204
+
205
+ # # Format parameters for LLM
206
+ # x_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.inputs])
207
+ # x_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_x_params.outputs])
208
+ # y_inputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.inputs])
209
+ # y_outputs = ", ".join([f"{p.name}:{p.type_str}" for p in cmd_y_params.outputs])
210
+
211
+ # has_dep, direction = _llm_module(
212
+ # cmd_x_name=cmd_x_name,
213
+ # cmd_x_inputs=x_inputs or "none",
214
+ # cmd_x_outputs=x_outputs or "none",
215
+ # cmd_y_name=cmd_y_name,
216
+ # cmd_y_inputs=y_inputs or "none",
217
+ # cmd_y_outputs=y_outputs or "none",
218
+ # )
219
+
220
+ # if not has_dep or direction == "none":
221
+ # return None
222
+ # if direction == "x_depends_on_y":
223
+ # return "y_to_x" # Y's outputs -> X's inputs
224
+ # return "x_to_y" if direction == "y_depends_on_x" else None
238
225
 
239
226
 
240
227
  def _contexts_overlap(routing: RoutingDefinition, cmd_x: str, cmd_y: str) -> bool:
@@ -2,6 +2,17 @@ import dspy
2
2
  from pydantic import BaseModel, Field
3
3
  from typing import Type, Optional, Dict, Any, Union, get_args, get_origin, Tuple, List
4
4
 
5
+ import fastworkflow
6
+ from fastworkflow.utils.logging import logger
7
+
8
+ def get_lm(model_env_var: str, api_key_env_var: Optional[str] = None, **kwargs):
9
+ """get the dspy lm object"""
10
+ model = fastworkflow.get_env_var(model_env_var)
11
+ if not model:
12
+ logger.critical(f"Critical Error:DSPy Language Model not provided. Set {model_env_var} environment variable.")
13
+ raise ValueError(f"DSPy Language Model not provided. Set {model_env_var} environment variable.")
14
+ api_key = fastworkflow.get_env_var(api_key_env_var) if api_key_env_var else None
15
+ return dspy.LM(model=model, api_key=api_key, **kwargs) if api_key else dspy.LM(model=model, **kwargs)
5
16
 
6
17
  def _process_field(field_info, is_input: bool) -> Tuple[Any, Any, bool]:
7
18
  """Process a single field and return its type, DSPy field, and optional status."""
@@ -417,6 +417,13 @@ Today's date is {today}.
417
417
  if missing_fields:
418
418
  message += f"{MISSING_INFORMATION_ERRMSG}" + ", ".join(missing_fields) + "\n"
419
419
 
420
+ for missing_field in missing_fields:
421
+ is_available_from = None
422
+ if hasattr(type(cmd_parameters).model_fields.get(missing_field), "json_schema_extra") and type(cmd_parameters).model_fields.get(missing_field).json_schema_extra:
423
+ is_available_from = type(cmd_parameters).model_fields.get(missing_field).json_schema_extra.get("available_from")
424
+ if is_available_from:
425
+ message += f"abort and use the {' or '.join(is_available_from)} command(s) to get {missing_field} information\n"
426
+
420
427
  if invalid_fields:
421
428
  message += f"{INVALID_INFORMATION_ERRMSG}" + ", ".join(invalid_fields) + "\n"
422
429
 
@@ -0,0 +1,186 @@
1
+ """
2
+ Agent integration module for fastWorkflow.
3
+ Provides workflow tool agent functionality for intelligent tool selection.
4
+ """
5
+
6
+ import dspy
7
+
8
+ import fastworkflow
9
+ from fastworkflow.utils import dspy_utils
10
+ from fastworkflow.command_metadata_api import CommandMetadataAPI
11
+ from fastworkflow.mcp_server import FastWorkflowMCPServer
12
+
13
+
14
+ class WorkflowAgentSignature(dspy.Signature):
15
+ """
16
+ Carefully review the user request and execute the todo list using available tools for building the final answer.
17
+ All the tasks in the todo list must be completed before returning the final answer.
18
+ """
19
+ user_query = dspy.InputField(desc="The natural language user query.")
20
+ final_answer = dspy.OutputField(desc="Comprehensive final answer with supporting evidence to demonstrate that all the tasks in the todo list have been completed.")
21
+
22
+ def _what_can_i_do(chat_session_obj: fastworkflow.ChatSession) -> str:
23
+ """
24
+ Returns a list of available commands, including their names and parameters.
25
+ """
26
+ current_workflow = chat_session_obj.get_active_workflow()
27
+ return CommandMetadataAPI.get_command_display_text(
28
+ subject_workflow_path=current_workflow.folderpath,
29
+ cme_workflow_path=fastworkflow.get_internal_workflow_path("command_metadata_extraction"),
30
+ active_context_name=current_workflow.current_command_context_name,
31
+ )
32
+
33
+ def _provide_missing_or_corrected_parameters(
34
+ missing_or_corrected_parameter_values: list[str|int|float|bool],
35
+ chat_session_obj: fastworkflow.ChatSession) -> str:
36
+ """
37
+ Call this tool ONLY in the parameter extraction error state to provide missing or corrected parameter values.
38
+ Missing parameter values may be found in the user query, or information already available, or by executing a different command (refer to the optional 'available_from' hint for guidance on appropriate commands to use to get the information).
39
+ If the error message indicates parameter values are improperly formatted, correct using your internal knowledge.
40
+ """
41
+ if missing_or_corrected_parameter_values:
42
+ command = ', '.join(missing_or_corrected_parameter_values)
43
+ else:
44
+ return "Provide missing or corrected parameter values or abort"
45
+
46
+ return _execute_workflow_query(command, chat_session_obj = chat_session_obj)
47
+
48
+ def _abort_current_command_to_exit_parameter_extraction_error_state(
49
+ chat_session_obj: fastworkflow.ChatSession) -> str:
50
+ """
51
+ Call this tool ONLY in the parameter extraction error state when you want to get out of the parameter extraction error state and execute a different command.
52
+ """
53
+ return _execute_workflow_query('abort', chat_session_obj = chat_session_obj)
54
+
55
+ def _execute_workflow_query(command: str, chat_session_obj: fastworkflow.ChatSession) -> str:
56
+ """
57
+ Executes the command and returns either a response, or a clarification request.
58
+ Use the "what_can_i_do" tool to get details on available commands, including their names and parameters. Fyi, values in the 'examples' field are fake and for illustration purposes only.
59
+ Commands must be formatted as follows: command_name <param1_name>param1_value</param1_name> <param2_name>param2_value</param2_name> ...
60
+ Don't use this tool to respond to a clarification requests in PARAMETER EXTRACTION ERROR state
61
+ """
62
+ # Directly invoke the command without going through queues
63
+ # This allows the agent to synchronously call workflow tools
64
+ from fastworkflow.command_executor import CommandExecutor
65
+ command_output = CommandExecutor.invoke_command(chat_session_obj, command)
66
+
67
+ # Format output - extract text from command response
68
+ if hasattr(command_output, 'command_responses') and command_output.command_responses:
69
+ response_parts = []
70
+ response_parts.extend(
71
+ cmd_response.response
72
+ for cmd_response in command_output.command_responses
73
+ if hasattr(cmd_response, 'response') and cmd_response.response
74
+ )
75
+ return "\n".join(response_parts) if response_parts else "Command executed successfully."
76
+
77
+ return "Command executed but produced no output."
78
+
79
+ def _missing_information_guidance_tool(
80
+ how_to_find_request: str,
81
+ chat_session_obj: fastworkflow.ChatSession) -> str:
82
+ """
83
+ Request guidance on finding missing information.
84
+ The how_to_find_request must be plain text without any formatting.
85
+ """
86
+ lm = dspy_utils.get_lm("LLM_AGENT", "LITELLM_API_KEY_AGENT")
87
+ with dspy.context(lm=lm):
88
+ guidance_func = dspy.ChainOfThought(
89
+ "command_info, missing_information_guidance_request -> guidance: str")
90
+ prediction = guidance_func(
91
+ command_info=_what_can_i_do(chat_session_obj),
92
+ missing_information_guidance_request=how_to_find_request)
93
+ return prediction.guidance
94
+
95
+ def _ask_user_tool(clarification_request: str, chat_session_obj: fastworkflow.ChatSession) -> str:
96
+ """
97
+ As a last resort, request clarification for missing information (only after using the missing_information_guidance_tool) or error correction from the human user.
98
+ The clarification_request must be plain text without any formatting.
99
+ """
100
+ command_output = fastworkflow.CommandOutput(
101
+ command_responses=[fastworkflow.CommandResponse(response=clarification_request)]
102
+ )
103
+ chat_session_obj.command_output_queue.put(command_output)
104
+ return chat_session_obj.user_message_queue.get()
105
+
106
+ def initialize_workflow_tool_agent(mcp_server: FastWorkflowMCPServer, max_iters: int = 25):
107
+ """
108
+ Initialize and return a DSPy ReAct agent that exposes individual MCP tools.
109
+ Each tool expects a single query string for its specific tool.
110
+
111
+ Args:
112
+ mcp_server: FastWorkflowMCPServer instance
113
+ max_iters: Maximum iterations for the ReAct agent
114
+
115
+ Returns:
116
+ DSPy ReAct agent configured with workflow tools
117
+ """
118
+ # available_tools = mcp_server.list_tools()
119
+
120
+ # if not available_tools:
121
+ # return None
122
+
123
+ chat_session_obj = mcp_server.chat_session
124
+ if not chat_session_obj:
125
+ return None
126
+
127
+ def what_can_i_do() -> str:
128
+ """
129
+ Returns a list of available commands, including their names and parameters
130
+ """
131
+ return _what_can_i_do(chat_session_obj=chat_session_obj)
132
+
133
+ def provide_missing_or_corrected_parameters(
134
+ missing_or_corrected_parameter_values: list[str|int|float|bool]) -> str:
135
+ """
136
+ Call this tool ONLY in the parameter extraction error state to provide missing or corrected parameter values.
137
+ Missing parameter values may be found in the user query, or information already available, or by executing a different command (refer to the optional 'available_from' hint for guidance on appropriate commands to use to get the information).
138
+ If the error message indicates parameter values are improperly formatted, correct using your internal knowledge.
139
+ """
140
+ return _provide_missing_or_corrected_parameters(missing_or_corrected_parameter_values, chat_session_obj=chat_session_obj)
141
+
142
+
143
+ def abort_current_command_to_exit_parameter_extraction_error_state() -> str:
144
+ """
145
+ Call this tool ONLY in the parameter extraction error state when you want to get out of the parameter extraction error state and execute a different command.
146
+ """
147
+ return _abort_current_command_to_exit_parameter_extraction_error_state(
148
+ chat_session_obj=chat_session_obj)
149
+
150
+ def execute_workflow_query(command: str) -> str:
151
+ """
152
+ Executes the command and returns either a response, or a clarification request.
153
+ Use the "what_can_i_do" tool to get details on available commands, including their names and parameters. Fyi, values in the 'examples' field are fake and for illustration purposes only.
154
+ Commands must be formatted as follows: command_name <param1_name>param1_value</param1_name> <param2_name>param2_value</param2_name> ...
155
+ Don't use this tool to respond to a clarification requests in PARAMETER EXTRACTION ERROR state
156
+ """
157
+ return _execute_workflow_query(command, chat_session_obj=chat_session_obj)
158
+
159
+ def missing_information_guidance(how_to_find_request: str) -> str:
160
+ """
161
+ Request guidance on finding missing information.
162
+ The how_to_find_request must be plain text without any formatting.
163
+ """
164
+ return _missing_information_guidance_tool(how_to_find_request, chat_session_obj=chat_session_obj)
165
+
166
+ def ask_user(clarification_request: str) -> str:
167
+ """
168
+ As a last resort, request clarification for missing information (only after using the missing_information_guidance_tool) or error correction from the human user.
169
+ The clarification_request must be plain text without any formatting.
170
+ """
171
+ return _ask_user_tool(clarification_request, chat_session_obj=chat_session_obj)
172
+
173
+ tools = [
174
+ what_can_i_do,
175
+ execute_workflow_query,
176
+ missing_information_guidance,
177
+ ask_user,
178
+ provide_missing_or_corrected_parameters,
179
+ abort_current_command_to_exit_parameter_extraction_error_state,
180
+ ]
181
+
182
+ return dspy.ReAct(
183
+ WorkflowAgentSignature,
184
+ tools=tools,
185
+ max_iters=max_iters,
186
+ )