fastworkflow 2.15.6__py3-none-any.whl → 2.15.8__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.
@@ -0,0 +1,242 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Any, Callable, Literal
3
+
4
+ from litellm import ContextWindowExceededError
5
+
6
+ import dspy
7
+ from dspy.adapters.types.tool import Tool
8
+ from dspy.primitives.module import Module
9
+ from dspy.signatures.signature import ensure_signature
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ if TYPE_CHECKING:
14
+ from dspy.signatures.signature import Signature
15
+
16
+
17
+ class fastWorkflowReAct(Module):
18
+ def __init__(self, signature: type["Signature"], tools: list[Callable], max_iters: int = 10):
19
+ """
20
+ ReAct stands for "Reasoning and Acting," a popular paradigm for building tool-using agents.
21
+ In this approach, the language model is iteratively provided with a list of tools and has
22
+ to reason about the current situation. The model decides whether to call a tool to gather more
23
+ information or to finish the task based on its reasoning process. The DSPy version of ReAct is
24
+ generalized to work over any signature, thanks to signature polymorphism.
25
+
26
+ Args:
27
+ signature: The signature of the module, which defines the input and output of the react module.
28
+ tools (list[Callable]): A list of functions, callable objects, or `dspy.Tool` instances.
29
+ max_iters (Optional[int]): The maximum number of iterations to run. Defaults to 10.
30
+
31
+ Example:
32
+
33
+ ```python
34
+ def get_weather(city: str) -> str:
35
+ return f"The weather in {city} is sunny."
36
+
37
+ react = dspy.ReAct(signature="question->answer", tools=[get_weather])
38
+ pred = react(question="What is the weather in Tokyo?")
39
+ ```
40
+ """
41
+ super().__init__()
42
+ self.signature = signature = ensure_signature(signature)
43
+ self.max_iters = max_iters
44
+
45
+ tools = [t if isinstance(t, Tool) else Tool(t) for t in tools]
46
+ tools = {tool.name: tool for tool in tools}
47
+
48
+ inputs = ", ".join([f"`{k}`" for k in signature.input_fields.keys()])
49
+ outputs = ", ".join([f"`{k}`" for k in signature.output_fields.keys()])
50
+ instr = [f"{signature.instructions}\n"] if signature.instructions else []
51
+
52
+ instr.extend(
53
+ [
54
+ 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.",
55
+ f"Your goal is to use one or more of the supplied tools to collect any necessary information for producing {outputs}.\n",
56
+ "To do this, you will interleave next_thought, next_tool_name, and next_tool_args in each turn, and also when finishing the task.",
57
+ "After each tool call, you receive a resulting observation, which gets appended to your trajectory.\n",
58
+ "When writing next_thought, you may reason about the current situation and plan for future steps.",
59
+ "When selecting the next_tool_name and its next_tool_args, the tool must be one of:\n",
60
+ ]
61
+ )
62
+
63
+ tools["finish"] = Tool(
64
+ func=lambda: "Completed.",
65
+ name="finish",
66
+ 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.",
67
+ args={},
68
+ )
69
+
70
+ for idx, tool in enumerate(tools.values()):
71
+ instr.append(f"({idx + 1}) {tool}")
72
+ instr.append("When providing `next_tool_args`, the value inside the field must be in JSON format")
73
+
74
+ react_signature = (
75
+ dspy.Signature({**signature.input_fields}, "\n".join(instr))
76
+ .append("trajectory", dspy.InputField(), type_=str)
77
+ .append("next_thought", dspy.OutputField(), type_=str)
78
+ .append("next_tool_name", dspy.OutputField(), type_=Literal[tuple(tools.keys())])
79
+ .append("next_tool_args", dspy.OutputField(), type_=dict[str, Any])
80
+ )
81
+
82
+ fallback_signature = dspy.Signature(
83
+ {**signature.input_fields, **signature.output_fields},
84
+ signature.instructions,
85
+ ).append("trajectory", dspy.InputField(), type_=str)
86
+
87
+ self.tools = tools
88
+ self.react = dspy.Predict(react_signature)
89
+ self.extract = dspy.ChainOfThought(fallback_signature)
90
+
91
+ self.inputs = {}
92
+ self.current_trajectory = {}
93
+
94
+ def _format_trajectory(self, trajectory: dict[str, Any]):
95
+ adapter = dspy.settings.adapter or dspy.ChatAdapter()
96
+ trajectory_signature = dspy.Signature(f"{', '.join(trajectory.keys())} -> x")
97
+ return adapter.format_user_message_content(trajectory_signature, trajectory)
98
+
99
+ def forward(self, **input_args):
100
+ self.inputs = input_args
101
+
102
+ trajectory = {}
103
+ max_iters = input_args.pop("max_iters", self.max_iters)
104
+ for idx in range(max_iters):
105
+ try:
106
+ pred = self._call_with_potential_trajectory_truncation(self.react, trajectory, **input_args)
107
+ except ValueError as err:
108
+ logger.warning(f"Ending the trajectory: Agent failed to select a valid tool: {_fmt_exc(err)}")
109
+ break
110
+
111
+ trajectory[f"thought_{idx}"] = pred.next_thought
112
+ trajectory[f"tool_name_{idx}"] = pred.next_tool_name
113
+ trajectory[f"tool_args_{idx}"] = pred.next_tool_args
114
+
115
+ self.current_trajectory[f"action_{idx}"] = f'{pred.next_tool_name}: {pred.next_tool_args}'
116
+
117
+ try:
118
+ trajectory[f"observation_{idx}"] = self.tools[pred.next_tool_name](**pred.next_tool_args)
119
+ except Exception as err:
120
+ trajectory[f"observation_{idx}"] = f"Execution error in {pred.next_tool_name}: {_fmt_exc(err)}"
121
+
122
+ if pred.next_tool_name == "finish":
123
+ break
124
+
125
+ extract = self._call_with_potential_trajectory_truncation(self.extract, trajectory, **input_args)
126
+ return dspy.Prediction(trajectory=trajectory, **extract)
127
+
128
+ async def aforward(self, **input_args):
129
+ trajectory = {}
130
+ max_iters = input_args.pop("max_iters", self.max_iters)
131
+ for idx in range(max_iters):
132
+ try:
133
+ pred = await self._async_call_with_potential_trajectory_truncation(self.react, trajectory, **input_args)
134
+ except ValueError as err:
135
+ logger.warning(f"Ending the trajectory: Agent failed to select a valid tool: {_fmt_exc(err)}")
136
+ break
137
+
138
+ trajectory[f"thought_{idx}"] = pred.next_thought
139
+ trajectory[f"tool_name_{idx}"] = pred.next_tool_name
140
+ trajectory[f"tool_args_{idx}"] = pred.next_tool_args
141
+
142
+ try:
143
+ trajectory[f"observation_{idx}"] = await self.tools[pred.next_tool_name].acall(**pred.next_tool_args)
144
+ except Exception as err:
145
+ trajectory[f"observation_{idx}"] = f"Execution error in {pred.next_tool_name}: {_fmt_exc(err)}"
146
+
147
+ if pred.next_tool_name == "finish":
148
+ break
149
+
150
+ extract = await self._async_call_with_potential_trajectory_truncation(self.extract, trajectory, **input_args)
151
+ return dspy.Prediction(trajectory=trajectory, **extract)
152
+
153
+ def _call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
154
+ for _ in range(3):
155
+ try:
156
+ return module(
157
+ **input_args,
158
+ trajectory=self._format_trajectory(trajectory),
159
+ )
160
+ except ContextWindowExceededError:
161
+ logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
162
+ trajectory = self.truncate_trajectory(trajectory)
163
+
164
+ async def _async_call_with_potential_trajectory_truncation(self, module, trajectory, **input_args):
165
+ for _ in range(3):
166
+ try:
167
+ return await module.acall(
168
+ **input_args,
169
+ trajectory=self._format_trajectory(trajectory),
170
+ )
171
+ except ContextWindowExceededError:
172
+ logger.warning("Trajectory exceeded the context window, truncating the oldest tool call information.")
173
+ trajectory = self.truncate_trajectory(trajectory)
174
+
175
+ def truncate_trajectory(self, trajectory):
176
+ """Truncates the trajectory so that it fits in the context window.
177
+
178
+ Users can override this method to implement their own truncation logic.
179
+ """
180
+ keys = list(trajectory.keys())
181
+ if len(keys) < 4:
182
+ # Every tool call has 4 keys: thought, tool_name, tool_args, and observation.
183
+ raise ValueError(
184
+ "The trajectory is too long so your prompt exceeded the context window, but the trajectory cannot be "
185
+ "truncated because it only has one tool call."
186
+ )
187
+
188
+ for key in keys[:4]:
189
+ trajectory.pop(key)
190
+
191
+ return trajectory
192
+
193
+
194
+ def _fmt_exc(err: BaseException, *, limit: int = 5) -> str:
195
+ """
196
+ Return a one-string traceback summary.
197
+ * `limit` - how many stack frames to keep (from the innermost outwards).
198
+ """
199
+
200
+ import traceback
201
+
202
+ return "\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__, limit=limit)).strip()
203
+
204
+
205
+ """
206
+ Thoughts and Planned Improvements for dspy.ReAct.
207
+
208
+ TOPIC 01: How Trajectories are Formatted, or rather when they are formatted.
209
+
210
+ Right now, both sub-modules are invoked with a `trajectory` argument, which is a string formatted in `forward`. Though
211
+ the formatter uses a general adapter.format_fields, the tracing of DSPy only sees the string, not the formatting logic.
212
+
213
+ What this means is that, in demonstrations, even if the user adjusts the adapter for a fixed program, the demos' format
214
+ will not update accordingly, but the inference-time trajectories will.
215
+
216
+ One way to fix this is to support `format=fn` in the dspy.InputField() for "trajectory" in the signatures. But this
217
+ means that care must be taken that the adapter is accessed at `forward` runtime, not signature definition time.
218
+
219
+ Another potential fix is to more natively support a "variadic" input field, where the input is a list of dictionaries,
220
+ or a big dictionary, and have each adapter format it accordingly.
221
+
222
+ Trajectories also affect meta-programming modules that view the trace later. It's inefficient O(n^2) to view the
223
+ trace of every module repeating the prefix.
224
+
225
+
226
+ TOPIC 03: Simplifying ReAct's __init__ by moving modular logic to the Tool class.
227
+ * Handling exceptions and error messages.
228
+ * More cleanly defining the "finish" tool, perhaps as a runtime-defined function?
229
+
230
+
231
+ TOPIC 04: Default behavior when the trajectory gets too long.
232
+
233
+
234
+ TOPIC 05: Adding more structure around how the instruction is formatted.
235
+ * Concretely, it's now a string, so an optimizer can and does rewrite it freely.
236
+ * An alternative would be to add more structure, such that a certain template is fixed but values are variable?
237
+
238
+
239
+ TOPIC 06: Idiomatically allowing tools that maintain state across iterations, but not across different `forward` calls.
240
+ * So the tool would be newly initialized at the start of each `forward` call, but maintain state across iterations.
241
+ * This is pretty useful for allowing the agent to keep notes or count certain things, etc.
242
+ """
@@ -285,8 +285,24 @@ Today's date is {today}.
285
285
  trainset=trainset
286
286
  )
287
287
 
288
+ def basic_checks(args, pred):
289
+ for field_name in field_names:
290
+ # return 0 if it extracts an example value instead of correct value | None
291
+ extracted_param_value = getattr(pred, field_name)
292
+ examples = model_class.model_fields[field_name].examples
293
+ if extracted_param_value in examples:
294
+ return 0.0
295
+ return 1.0
296
+
297
+ # Create a refined module that tries up to 3 times
298
+ best_of_3 = dspy.BestOfN(
299
+ module=compiled_model,
300
+ N=3,
301
+ reward_fn=basic_checks,
302
+ threshold=1.0)
303
+
288
304
  try:
289
- dspy_result = compiled_model(command=self.command)
305
+ dspy_result = best_of_3(command=self.command)
290
306
  for field_name in field_names:
291
307
  default = model_class.model_fields[field_name].default
292
308
  param_dict[field_name] = getattr(dspy_result, field_name, default)
@@ -428,27 +444,14 @@ Today's date is {today}.
428
444
  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:
429
445
  is_available_from = type(cmd_parameters).model_fields.get(missing_field).json_schema_extra.get("available_from")
430
446
  if is_available_from:
431
- message += f"abort and use the {' or '.join(is_available_from)} command(s) to get {missing_field} information. OR...\n"
447
+ msg_prefix = "abort and "
448
+ if "run_as_agent" in app_workflow.context:
449
+ msg_prefix = ""
450
+ message += f"{msg_prefix}use the {' or '.join(is_available_from)} command(s) to get {missing_field} information. OR...\n"
432
451
 
433
452
  if invalid_fields:
434
453
  message += f"{INVALID_INFORMATION_ERRMSG}" + ", ".join(invalid_fields) + "\n"
435
454
 
436
- with suppress(Exception):
437
- graph_path = os.path.join(app_workflow.folderpath, "command_dependency_graph.json")
438
- suggestions_texts: list[str] = []
439
- for field in missing_fields:
440
- plans = get_dependency_suggestions(graph_path, subject_command_name, field, min_weight=0.7, max_depth=3)
441
- if plans:
442
- # Format a concise plan: main command and any immediate sub-steps
443
- top = plans[0]
444
- def format_plan(p):
445
- if not p.get('sub_plans'):
446
- return p['command']
447
- return p['command'] + " -> " + " -> ".join(sp['command'] for sp in p['sub_plans'])
448
- suggestions_texts.append(f"To get '{field}', try: {format_plan(top)}")
449
- if suggestions_texts:
450
- message += "\n" + "\n".join(suggestions_texts) + "\n"
451
-
452
455
  for field, suggestions in all_suggestions.items():
453
456
  if suggestions:
454
457
  is_format_instruction = any(("format" in str(s).lower() or "pattern" in str(s).lower()) for s in suggestions)
@@ -468,6 +471,7 @@ Today's date is {today}.
468
471
  if combined_fields:
469
472
  combined_fields_str = ", ".join(combined_fields)
470
473
  message += f"\nProvide corrected parameter values in the exact order specified below, separated by commas:\n{combined_fields_str}"
471
- message += "\nFor parameter values that include a comma, provide separately from other values, and one at a time."
474
+ if "run_as_agent" not in app_workflow.context:
475
+ message += "\nFor parameter values that include a comma, provide separately from other values, and one at a time."
472
476
 
473
477
  return (False, message, all_suggestions)