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.
- fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/go_up.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/reset_context.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +98 -166
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +7 -3
- fastworkflow/build/genai_postprocessor.py +143 -149
- fastworkflow/chat_session.py +42 -11
- fastworkflow/command_metadata_api.py +794 -0
- fastworkflow/command_routing.py +4 -1
- fastworkflow/examples/fastworkflow.env +1 -1
- fastworkflow/examples/fastworkflow.passwords.env +1 -0
- fastworkflow/examples/hello_world/_commands/add_two_numbers.py +1 -0
- fastworkflow/examples/retail_workflow/_commands/calculate.py +67 -0
- fastworkflow/examples/retail_workflow/_commands/cancel_pending_order.py +4 -1
- fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +13 -1
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -1
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_name_zip.py +6 -1
- fastworkflow/examples/retail_workflow/_commands/get_order_details.py +22 -10
- fastworkflow/examples/retail_workflow/_commands/get_product_details.py +12 -4
- fastworkflow/examples/retail_workflow/_commands/get_user_details.py +21 -5
- fastworkflow/examples/retail_workflow/_commands/list_all_product_types.py +4 -1
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_address.py +3 -0
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +12 -0
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_payment.py +7 -1
- fastworkflow/examples/retail_workflow/_commands/modify_user_address.py +3 -0
- fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +10 -1
- fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
- fastworkflow/examples/retail_workflow/tools/calculate.py +1 -1
- fastworkflow/mcp_server.py +52 -44
- fastworkflow/run/__main__.py +9 -5
- fastworkflow/run_agent/__main__.py +8 -8
- fastworkflow/run_agent/agent_module.py +6 -16
- fastworkflow/utils/command_dependency_graph.py +130 -143
- fastworkflow/utils/dspy_utils.py +11 -0
- fastworkflow/utils/signatures.py +7 -0
- fastworkflow/workflow_agent.py +186 -0
- {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/METADATA +12 -3
- {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/RECORD +41 -40
- fastworkflow/agent_integration.py +0 -239
- fastworkflow/examples/retail_workflow/_commands/parameter_dependency_graph.json +0 -36
- {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/LICENSE +0 -0
- {fastworkflow-2.13.5.dist-info → fastworkflow-2.14.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
123
|
+
# def _initialize_dspy_llm_if_needed() -> None:
|
|
124
|
+
# """Initialize DSPy LM once using FastWorkflow environment.
|
|
138
125
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
fastworkflow/utils/dspy_utils.py
CHANGED
|
@@ -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."""
|
fastworkflow/utils/signatures.py
CHANGED
|
@@ -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
|
+
)
|