fastworkflow 2.15.5__py3-none-any.whl → 2.17.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
- fastworkflow/chat_session.py +379 -206
- fastworkflow/cli.py +80 -165
- fastworkflow/command_context_model.py +73 -7
- fastworkflow/command_executor.py +14 -5
- fastworkflow/command_metadata_api.py +106 -6
- fastworkflow/examples/fastworkflow.env +2 -1
- fastworkflow/examples/fastworkflow.passwords.env +2 -1
- fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
- fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
- fastworkflow/intent_clarification_agent.py +131 -0
- fastworkflow/mcp_server.py +3 -3
- fastworkflow/run/__main__.py +33 -40
- fastworkflow/run_fastapi_mcp/README.md +373 -0
- fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
- fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
- fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
- fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
- fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
- fastworkflow/run_fastapi_mcp/utils.py +517 -0
- fastworkflow/train/__main__.py +1 -1
- fastworkflow/utils/chat_adapter.py +99 -0
- fastworkflow/utils/python_utils.py +4 -4
- fastworkflow/utils/react.py +258 -0
- fastworkflow/utils/signatures.py +338 -139
- fastworkflow/workflow.py +1 -5
- fastworkflow/workflow_agent.py +185 -133
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
- fastworkflow/run_agent/__main__.py +0 -294
- fastworkflow/run_agent/agent_module.py +0 -194
- /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal
|
|
3
|
+
|
|
4
|
+
from litellm import ContextWindowExceededError
|
|
5
|
+
from litellm import exceptions as litellm_exceptions
|
|
6
|
+
|
|
7
|
+
import dspy
|
|
8
|
+
from dspy.adapters.types.tool import Tool
|
|
9
|
+
from dspy.primitives.module import Module
|
|
10
|
+
from dspy.signatures.signature import ensure_signature
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dspy.signatures.signature import Signature
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class fastWorkflowReAct(Module):
|
|
19
|
+
def __init__(self, signature: type["Signature"], tools: list[Callable], max_iters: int = 10):
|
|
20
|
+
"""
|
|
21
|
+
ReAct stands for "Reasoning and Acting," a popular paradigm for building tool-using agents.
|
|
22
|
+
In this approach, the language model is iteratively provided with a list of tools and has
|
|
23
|
+
to reason about the current situation. The model decides whether to call a tool to gather more
|
|
24
|
+
information or to finish the task based on its reasoning process. The DSPy version of ReAct is
|
|
25
|
+
generalized to work over any signature, thanks to signature polymorphism.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
signature: The signature of the module, which defines the input and output of the react module.
|
|
29
|
+
tools (list[Callable]): A list of functions, callable objects, or `dspy.Tool` instances.
|
|
30
|
+
max_iters (Optional[int]): The maximum number of iterations to run. Defaults to 10.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
def get_weather(city: str) -> str:
|
|
36
|
+
return f"The weather in {city} is sunny."
|
|
37
|
+
|
|
38
|
+
react = dspy.ReAct(signature="question->answer", tools=[get_weather])
|
|
39
|
+
pred = react(question="What is the weather in Tokyo?")
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.signature = signature = ensure_signature(signature)
|
|
44
|
+
self.max_iters = max_iters
|
|
45
|
+
self.iteration_counter = 0
|
|
46
|
+
|
|
47
|
+
tools = [t if isinstance(t, Tool) else Tool(t) for t in tools]
|
|
48
|
+
tools = {tool.name: tool for tool in tools}
|
|
49
|
+
|
|
50
|
+
inputs = ", ".join([f"`{k}`" for k in signature.input_fields.keys()])
|
|
51
|
+
outputs = ", ".join([f"`{k}`" for k in signature.output_fields.keys()])
|
|
52
|
+
instr = [f"{signature.instructions}\n"] if signature.instructions else []
|
|
53
|
+
|
|
54
|
+
instr.extend(
|
|
55
|
+
[
|
|
56
|
+
f"You are an Agent. In each episode, you will be given the fields {inputs} as input. And you can see your past trajectory so far.",
|
|
57
|
+
f"Your goal is to use one or more of the supplied tools to collect any necessary information for producing {outputs}.\n",
|
|
58
|
+
"To do this, you will interleave next_thought, next_tool_name, and next_tool_args in each turn, and also when finishing the task.",
|
|
59
|
+
"After each tool call, you receive a resulting observation, which gets appended to your trajectory.\n",
|
|
60
|
+
"When writing next_thought, you may reason about the current situation and plan for future steps.",
|
|
61
|
+
"When selecting the next_tool_name and its next_tool_args, the tool must be one of:\n",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
tools["finish"] = Tool(
|
|
66
|
+
func=lambda: "Completed.",
|
|
67
|
+
name="finish",
|
|
68
|
+
desc=f"Marks the task as complete. That is, signals that all information for producing the outputs, i.e. {outputs}, are now available to be extracted.",
|
|
69
|
+
args={},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
instr.extend(f"({idx + 1}) {tool}" for idx, tool in enumerate(tools.values()))
|
|
73
|
+
instr.append("When providing `next_tool_args`, the value inside the field must be in JSON format")
|
|
74
|
+
|
|
75
|
+
# Build the ReAct signature with trajectory and available_commands inputs.
|
|
76
|
+
# available_commands is injected into system message by CommandsSystemPreludeAdapter
|
|
77
|
+
# (see fastworkflow/utils/chat_adapter.py) and is NOT included in the trajectory
|
|
78
|
+
# formatting to avoid token bloat across iterations.
|
|
79
|
+
react_signature = (
|
|
80
|
+
dspy.Signature({**signature.input_fields}, "\n".join(instr))
|
|
81
|
+
.append("trajectory", dspy.InputField(), type_=str)
|
|
82
|
+
.append("available_commands", dspy.InputField(), type_=str)
|
|
83
|
+
.append("next_thought", dspy.OutputField(), type_=str)
|
|
84
|
+
.append("next_tool_name", dspy.OutputField(), type_=Literal[tuple(tools.keys())])
|
|
85
|
+
.append("next_tool_args", dspy.OutputField(), type_=dict[str, Any])
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
fallback_signature = dspy.Signature(
|
|
89
|
+
{**signature.input_fields, **signature.output_fields},
|
|
90
|
+
signature.instructions,
|
|
91
|
+
).append("trajectory", dspy.InputField(), type_=str).append("available_commands", dspy.InputField(), type_=str)
|
|
92
|
+
|
|
93
|
+
self.tools = tools
|
|
94
|
+
self.react = dspy.Predict(react_signature)
|
|
95
|
+
self.extract = dspy.ChainOfThought(fallback_signature)
|
|
96
|
+
|
|
97
|
+
self.inputs = {}
|
|
98
|
+
self.current_trajectory = {}
|
|
99
|
+
|
|
100
|
+
def _format_trajectory(self, trajectory: dict[str, Any]):
|
|
101
|
+
adapter = dspy.settings.adapter or dspy.ChatAdapter()
|
|
102
|
+
trajectory_signature = dspy.Signature(f"{', '.join(trajectory.keys())} -> x")
|
|
103
|
+
return adapter.format_user_message_content(trajectory_signature, trajectory)
|
|
104
|
+
|
|
105
|
+
def forward(self, **input_args):
|
|
106
|
+
self.inputs = input_args
|
|
107
|
+
|
|
108
|
+
trajectory = {}
|
|
109
|
+
max_iters = input_args.pop("max_iters", self.max_iters)
|
|
110
|
+
idx = 0
|
|
111
|
+
while True:
|
|
112
|
+
try:
|
|
113
|
+
pred = self._call_with_potential_trajectory_truncation(self.react, trajectory, **input_args)
|
|
114
|
+
except ValueError as err:
|
|
115
|
+
logger.warning(f"Ending the trajectory: Agent failed to select a valid tool: {_fmt_exc(err)}")
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
trajectory[f"thought_{idx}"] = pred.next_thought
|
|
119
|
+
trajectory[f"tool_name_{idx}"] = pred.next_tool_name
|
|
120
|
+
trajectory[f"tool_args_{idx}"] = pred.next_tool_args
|
|
121
|
+
|
|
122
|
+
self.current_trajectory[f"action_{idx}"] = f'{pred.next_tool_name}: {pred.next_tool_args}'
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
trajectory[f"observation_{idx}"] = self.tools[pred.next_tool_name](**pred.next_tool_args)
|
|
126
|
+
except Exception as err:
|
|
127
|
+
trajectory[f"observation_{idx}"] = f"Execution error in {pred.next_tool_name}: {_fmt_exc(err)}"
|
|
128
|
+
|
|
129
|
+
if pred.next_tool_name == "finish":
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
idx += 1 # this is the counter for the index of the entire trajectory
|
|
133
|
+
self.iteration_counter += 1 # this counter just determines the number of times we run the react agent and it's reset everytime we call the user for clarification
|
|
134
|
+
if self.iteration_counter >= max_iters:
|
|
135
|
+
logger.warning("Max iterations reached")
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
extract = self._call_with_potential_trajectory_truncation(self.extract, trajectory, **input_args)
|
|
139
|
+
return dspy.Prediction(trajectory=trajectory, **extract)
|
|
140
|
+
|
|
141
|
+
async def aforward(self, **input_args):
|
|
142
|
+
trajectory = {}
|
|
143
|
+
max_iters = input_args.pop("max_iters", self.max_iters)
|
|
144
|
+
for idx in range(max_iters):
|
|
145
|
+
try:
|
|
146
|
+
pred = await self._async_call_with_potential_trajectory_truncation(self.react, trajectory, **input_args)
|
|
147
|
+
except ValueError as err:
|
|
148
|
+
logger.warning(f"Ending the trajectory: Agent failed to select a valid tool: {_fmt_exc(err)}")
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
trajectory[f"thought_{idx}"] = pred.next_thought
|
|
152
|
+
trajectory[f"tool_name_{idx}"] = pred.next_tool_name
|
|
153
|
+
trajectory[f"tool_args_{idx}"] = pred.next_tool_args
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
trajectory[f"observation_{idx}"] = await self.tools[pred.next_tool_name].acall(**pred.next_tool_args)
|
|
157
|
+
except Exception as err:
|
|
158
|
+
trajectory[f"observation_{idx}"] = f"Execution error in {pred.next_tool_name}: {_fmt_exc(err)}"
|
|
159
|
+
|
|
160
|
+
if pred.next_tool_name == "finish":
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
extract = await self._async_call_with_potential_trajectory_truncation(self.extract, trajectory, **input_args)
|
|
164
|
+
return dspy.Prediction(trajectory=trajectory, **extract)
|
|
165
|
+
|
|
166
|
+
def _call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
|
|
167
|
+
for _ in range(3):
|
|
168
|
+
try:
|
|
169
|
+
return module(
|
|
170
|
+
**input_args,
|
|
171
|
+
trajectory=self._format_trajectory(trajectory),
|
|
172
|
+
)
|
|
173
|
+
except litellm_exceptions.BadRequestError:
|
|
174
|
+
logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
|
|
175
|
+
trajectory = self.truncate_trajectory(trajectory)
|
|
176
|
+
except ContextWindowExceededError:
|
|
177
|
+
logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
|
|
178
|
+
trajectory = self.truncate_trajectory(trajectory)
|
|
179
|
+
|
|
180
|
+
async def _async_call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
|
|
181
|
+
for _ in range(3):
|
|
182
|
+
try:
|
|
183
|
+
return await module.acall(
|
|
184
|
+
**input_args,
|
|
185
|
+
trajectory=self._format_trajectory(trajectory),
|
|
186
|
+
)
|
|
187
|
+
except ContextWindowExceededError:
|
|
188
|
+
logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
|
|
189
|
+
trajectory = self.truncate_trajectory(trajectory)
|
|
190
|
+
|
|
191
|
+
def truncate_trajectory(self, trajectory):
|
|
192
|
+
"""Truncates the trajectory so that it fits in the context window.
|
|
193
|
+
|
|
194
|
+
Users can override this method to implement their own truncation logic.
|
|
195
|
+
"""
|
|
196
|
+
keys = list(trajectory.keys())
|
|
197
|
+
if len(keys) < 4:
|
|
198
|
+
# Every tool call has 4 keys: thought, tool_name, tool_args, and observation.
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"The trajectory is too long so your prompt exceeded the context window, but the trajectory cannot be "
|
|
201
|
+
"truncated because it only has one tool call."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
for key in keys[:4]:
|
|
205
|
+
trajectory.pop(key)
|
|
206
|
+
|
|
207
|
+
return trajectory
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _fmt_exc(err: BaseException, *, limit: int = 5) -> str:
|
|
211
|
+
"""
|
|
212
|
+
Return a one-string traceback summary.
|
|
213
|
+
* `limit` - how many stack frames to keep (from the innermost outwards).
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
import traceback
|
|
217
|
+
|
|
218
|
+
return "\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__, limit=limit)).strip()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
Thoughts and Planned Improvements for dspy.ReAct.
|
|
223
|
+
|
|
224
|
+
TOPIC 01: How Trajectories are Formatted, or rather when they are formatted.
|
|
225
|
+
|
|
226
|
+
Right now, both sub-modules are invoked with a `trajectory` argument, which is a string formatted in `forward`. Though
|
|
227
|
+
the formatter uses a general adapter.format_fields, the tracing of DSPy only sees the string, not the formatting logic.
|
|
228
|
+
|
|
229
|
+
What this means is that, in demonstrations, even if the user adjusts the adapter for a fixed program, the demos' format
|
|
230
|
+
will not update accordingly, but the inference-time trajectories will.
|
|
231
|
+
|
|
232
|
+
One way to fix this is to support `format=fn` in the dspy.InputField() for "trajectory" in the signatures. But this
|
|
233
|
+
means that care must be taken that the adapter is accessed at `forward` runtime, not signature definition time.
|
|
234
|
+
|
|
235
|
+
Another potential fix is to more natively support a "variadic" input field, where the input is a list of dictionaries,
|
|
236
|
+
or a big dictionary, and have each adapter format it accordingly.
|
|
237
|
+
|
|
238
|
+
Trajectories also affect meta-programming modules that view the trace later. It's inefficient O(n^2) to view the
|
|
239
|
+
trace of every module repeating the prefix.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
TOPIC 03: Simplifying ReAct's __init__ by moving modular logic to the Tool class.
|
|
243
|
+
* Handling exceptions and error messages.
|
|
244
|
+
* More cleanly defining the "finish" tool, perhaps as a runtime-defined function?
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
TOPIC 04: Default behavior when the trajectory gets too long.
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
TOPIC 05: Adding more structure around how the instruction is formatted.
|
|
251
|
+
* Concretely, it's now a string, so an optimizer can and does rewrite it freely.
|
|
252
|
+
* An alternative would be to add more structure, such that a certain template is fixed but values are variable?
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
TOPIC 06: Idiomatically allowing tools that maintain state across iterations, but not across different `forward` calls.
|
|
256
|
+
* So the tool would be newly initialized at the start of each `forward` call, but maintain state across iterations.
|
|
257
|
+
* This is pretty useful for allowing the agent to keep notes or count certain things, etc.
|
|
258
|
+
"""
|