dspy 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/README.md +328 -249
- data/lib/dspy/chain_of_thought.rb +151 -11
- data/lib/dspy/instrumentation/token_tracker.rb +54 -0
- data/lib/dspy/instrumentation.rb +113 -0
- data/lib/dspy/lm/adapter.rb +41 -0
- data/lib/dspy/lm/adapter_factory.rb +59 -0
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +96 -0
- data/lib/dspy/lm/adapters/openai_adapter.rb +53 -0
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +81 -0
- data/lib/dspy/lm/errors.rb +10 -0
- data/lib/dspy/lm/response.rb +28 -0
- data/lib/dspy/lm.rb +92 -40
- data/lib/dspy/module.rb +51 -6
- data/lib/dspy/predict.rb +135 -15
- data/lib/dspy/re_act.rb +366 -191
- data/lib/dspy/schema_adapters.rb +55 -0
- data/lib/dspy/signature.rb +282 -10
- data/lib/dspy/subscribers/logger_subscriber.rb +215 -0
- data/lib/dspy/tools/{sorbet_tool.rb → base.rb} +33 -33
- data/lib/dspy/tools.rb +1 -1
- data/lib/dspy.rb +4 -9
- metadata +60 -28
- data/lib/dspy/ext/dry_schema.rb +0 -94
- data/lib/dspy/sorbet_chain_of_thought.rb +0 -91
- data/lib/dspy/sorbet_module.rb +0 -47
- data/lib/dspy/sorbet_predict.rb +0 -180
- data/lib/dspy/sorbet_re_act.rb +0 -332
- data/lib/dspy/sorbet_signature.rb +0 -218
- data/lib/dspy/types.rb +0 -3
data/lib/dspy/re_act.rb
CHANGED
@@ -1,253 +1,428 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require_relative 'predict'
|
6
|
+
require_relative 'signature'
|
7
|
+
require_relative 'chain_of_thought'
|
8
|
+
require 'json'
|
9
|
+
require_relative 'instrumentation'
|
10
|
+
|
1
11
|
module DSPy
|
2
|
-
# Define
|
12
|
+
# Define a simple struct for history entries with proper type annotations
|
13
|
+
class HistoryEntry < T::Struct
|
14
|
+
const :step, Integer
|
15
|
+
prop :thought, T.nilable(String)
|
16
|
+
prop :action, T.nilable(String)
|
17
|
+
prop :action_input, T.nilable(T.any(String, Numeric, T::Hash[T.untyped, T.untyped], T::Array[T.untyped]))
|
18
|
+
prop :observation, T.nilable(String)
|
19
|
+
|
20
|
+
# Custom serialization to ensure compatibility with the rest of the code
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
step: step,
|
24
|
+
thought: thought,
|
25
|
+
action: action,
|
26
|
+
action_input: action_input,
|
27
|
+
observation: observation
|
28
|
+
}.compact
|
29
|
+
end
|
30
|
+
end
|
31
|
+
# Defines the signature for ReAct reasoning using Sorbet signatures
|
3
32
|
class Thought < DSPy::Signature
|
4
33
|
description "Generate a thought about what to do next to answer the question."
|
5
34
|
|
6
35
|
input do
|
7
|
-
|
8
|
-
|
9
|
-
|
36
|
+
const :question, String,
|
37
|
+
description: "The question to answer"
|
38
|
+
const :history, T::Array[HistoryEntry],
|
39
|
+
description: "Previous thoughts and actions, including observations from tools. The agent MUST use information from the history to inform its actions and final answer. Each entry is a hash representing a step in the reasoning process."
|
40
|
+
const :available_tools, T::Array[T::Hash[String, T.untyped]],
|
41
|
+
description: "Array of available tools with their JSON schemas. The agent MUST choose an action from the tool names in this list or use \"finish\". For each tool, use the name exactly as specified and provide action_input as a JSON object matching the tool's schema."
|
10
42
|
end
|
11
43
|
|
12
44
|
output do
|
13
|
-
|
14
|
-
|
15
|
-
|
45
|
+
const :thought, String,
|
46
|
+
description: "Reasoning about what to do next, considering the history and observations."
|
47
|
+
const :action, String,
|
48
|
+
description: "The action to take. MUST be one of the tool names listed in `available_tools` input, or the literal string \"finish\" to provide the final answer."
|
49
|
+
const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
|
50
|
+
description: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final answer to the original question. This answer MUST be directly taken from the relevant Observation in the history if available. For example, if an observation showed \"Observation: 100.0\", and you are finishing, this field MUST be \"100.0\". Do not leave empty if finishing with an observed answer."
|
16
51
|
end
|
17
52
|
end
|
18
53
|
|
19
|
-
|
54
|
+
class NextStep < T::Enum
|
55
|
+
enums do
|
56
|
+
Continue = new("continue")
|
57
|
+
Finish = new("finish")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines the signature for processing observations and deciding next steps
|
20
62
|
class ReActObservation < DSPy::Signature
|
21
63
|
description "Process the observation from a tool and decide what to do next."
|
22
64
|
|
23
65
|
input do
|
24
|
-
|
25
|
-
|
26
|
-
|
66
|
+
const :question, String,
|
67
|
+
description: "The original question"
|
68
|
+
const :history, T::Array[HistoryEntry],
|
69
|
+
description: "Previous thoughts, actions, and observations. Each entry is a hash representing a step in the reasoning process."
|
70
|
+
const :observation, String,
|
71
|
+
description: "The result from the last action"
|
27
72
|
end
|
28
73
|
|
29
74
|
output do
|
30
|
-
|
31
|
-
|
75
|
+
const :interpretation, String,
|
76
|
+
description: "Interpretation of the observation"
|
77
|
+
const :next_step, NextStep,
|
78
|
+
description: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
|
32
79
|
end
|
33
80
|
end
|
34
81
|
|
35
|
-
# ReAct Agent
|
36
|
-
class ReAct <
|
37
|
-
|
82
|
+
# ReAct Agent using Sorbet signatures
|
83
|
+
class ReAct < Predict
|
84
|
+
extend T::Sig
|
38
85
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
end
|
50
|
-
end
|
86
|
+
FINISH_ACTION = "finish"
|
87
|
+
sig { returns(T.class_of(DSPy::Signature)) }
|
88
|
+
attr_reader :original_signature_class
|
89
|
+
|
90
|
+
sig { returns(T.class_of(T::Struct)) }
|
91
|
+
attr_reader :enhanced_output_struct
|
92
|
+
|
93
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
94
|
+
attr_reader :tools
|
51
95
|
|
96
|
+
sig { returns(Integer) }
|
97
|
+
attr_reader :max_iterations
|
98
|
+
|
99
|
+
|
100
|
+
sig { params(signature_class: T.class_of(DSPy::Signature), tools: T::Array[T.untyped], max_iterations: Integer).void }
|
52
101
|
def initialize(signature_class, tools: [], max_iterations: 5)
|
53
|
-
|
54
|
-
@
|
55
|
-
@
|
56
|
-
@observation_processor = DSPy::Predict.new(ReActObservation)
|
57
|
-
@tools = tools.map { |tool| [tool.name.downcase, tool] }.to_h # Ensure tool names are stored lowercased for lookup
|
102
|
+
@original_signature_class = signature_class
|
103
|
+
@tools = T.let({}, T::Hash[String, T.untyped])
|
104
|
+
tools.each { |tool| @tools[tool.name.downcase] = tool }
|
58
105
|
@max_iterations = max_iterations
|
59
106
|
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
107
|
+
# Create thought generator using Predict to preserve field descriptions
|
108
|
+
@thought_generator = T.let(DSPy::Predict.new(Thought), DSPy::Predict)
|
109
|
+
|
110
|
+
# Create observation processor using Predict to preserve field descriptions
|
111
|
+
@observation_processor = T.let(DSPy::Predict.new(ReActObservation), DSPy::Predict)
|
112
|
+
|
113
|
+
# Create enhanced output struct with ReAct fields
|
114
|
+
@enhanced_output_struct = create_enhanced_output_struct(signature_class)
|
115
|
+
enhanced_output_struct = @enhanced_output_struct
|
116
|
+
|
117
|
+
# Create enhanced signature class
|
118
|
+
enhanced_signature = Class.new(DSPy::Signature) do
|
119
|
+
# Set the description
|
120
|
+
description signature_class.description
|
121
|
+
|
122
|
+
# Use the same input struct
|
123
|
+
@input_struct_class = signature_class.input_struct_class
|
124
|
+
|
125
|
+
# Use the enhanced output struct with ReAct fields
|
126
|
+
@output_struct_class = enhanced_output_struct
|
127
|
+
|
128
|
+
class << self
|
129
|
+
attr_reader :input_struct_class, :output_struct_class
|
68
130
|
end
|
69
|
-
optional(:iterations).value(:integer).meta(description: 'Number of iterations taken by the ReAct agent.')
|
70
131
|
end
|
71
132
|
|
72
|
-
#
|
73
|
-
|
133
|
+
# Call parent constructor with enhanced signature
|
134
|
+
super(enhanced_signature)
|
74
135
|
end
|
75
136
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
137
|
+
sig { params(kwargs: T.untyped).returns(T.untyped).override }
|
138
|
+
def forward(**kwargs)
|
139
|
+
lm = config.lm || DSPy.config.lm
|
140
|
+
# Prepare instrumentation payload
|
141
|
+
input_fields = kwargs.keys.map(&:to_s)
|
142
|
+
available_tools = @tools.keys
|
143
|
+
|
144
|
+
# Instrument the entire ReAct agent lifecycle
|
145
|
+
result = Instrumentation.instrument('dspy.react', {
|
146
|
+
signature_class: @original_signature_class.name,
|
147
|
+
model: lm.model,
|
148
|
+
provider: lm.provider,
|
149
|
+
input_fields: input_fields,
|
150
|
+
max_iterations: @max_iterations,
|
151
|
+
available_tools: available_tools
|
152
|
+
}) do
|
153
|
+
# Validate input using Sorbet struct validation
|
154
|
+
input_struct = @original_signature_class.input_struct_class.new(**kwargs)
|
155
|
+
|
156
|
+
# Get the question (assume first field is the question for now)
|
157
|
+
question = T.cast(input_struct.serialize.values.first, String)
|
158
|
+
|
159
|
+
history = T.let([], T::Array[HistoryEntry])
|
160
|
+
available_tools_desc = @tools.map { |name, tool| JSON.parse(tool.schema) }
|
161
|
+
|
162
|
+
final_answer = T.let(nil, T.nilable(String))
|
163
|
+
iterations_count = 0
|
164
|
+
last_observation = T.let(nil, T.nilable(String))
|
165
|
+
tools_used = []
|
166
|
+
|
167
|
+
while @max_iterations.nil? || iterations_count < @max_iterations
|
168
|
+
iterations_count += 1
|
169
|
+
|
170
|
+
# Instrument each iteration
|
171
|
+
iteration_result = Instrumentation.instrument('dspy.react.iteration', {
|
172
|
+
iteration: iterations_count,
|
173
|
+
max_iterations: @max_iterations,
|
174
|
+
history_length: history.length,
|
175
|
+
tools_used_so_far: tools_used.uniq
|
176
|
+
}) do
|
177
|
+
# Get next thought from LM
|
178
|
+
thought_obj = @thought_generator.forward(
|
179
|
+
question: question,
|
180
|
+
history: history,
|
181
|
+
available_tools: available_tools_desc
|
182
|
+
)
|
183
|
+
step = iterations_count
|
184
|
+
thought = thought_obj.thought
|
185
|
+
action = thought_obj.action
|
186
|
+
action_input = thought_obj.action_input
|
187
|
+
|
188
|
+
# Break if finish action
|
189
|
+
if action&.downcase == 'finish'
|
190
|
+
final_answer = handle_finish_action(action_input, last_observation, step, thought, action, history)
|
191
|
+
break
|
192
|
+
end
|
82
193
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
194
|
+
# Track tools used
|
195
|
+
tools_used << action.downcase if action && @tools[action.downcase]
|
196
|
+
|
197
|
+
# Execute action
|
198
|
+
observation = if action && @tools[action.downcase]
|
199
|
+
# Instrument tool call
|
200
|
+
Instrumentation.instrument('dspy.react.tool_call', {
|
201
|
+
iteration: iterations_count,
|
202
|
+
tool_name: action.downcase,
|
203
|
+
tool_input: action_input
|
204
|
+
}) do
|
205
|
+
execute_action(action, action_input)
|
206
|
+
end
|
207
|
+
else
|
208
|
+
"Unknown action: #{action}. Available actions: #{@tools.keys.join(', ')}, finish"
|
209
|
+
end
|
210
|
+
|
211
|
+
last_observation = observation
|
212
|
+
|
213
|
+
# Add to history
|
214
|
+
history << HistoryEntry.new(
|
215
|
+
step: step,
|
216
|
+
thought: thought,
|
217
|
+
action: action,
|
218
|
+
action_input: action_input,
|
219
|
+
observation: observation
|
220
|
+
)
|
221
|
+
|
222
|
+
# Process observation to decide next step
|
223
|
+
if observation && !observation.include?("Unknown action")
|
224
|
+
observation_result = @observation_processor.forward(
|
225
|
+
question: question,
|
226
|
+
history: history,
|
227
|
+
observation: observation
|
228
|
+
)
|
88
229
|
|
89
|
-
|
90
|
-
|
230
|
+
# If observation processor suggests finishing, generate final thought
|
231
|
+
if observation_result.next_step == NextStep::Finish
|
232
|
+
final_thought = @thought_generator.forward(
|
233
|
+
question: question,
|
234
|
+
history: history,
|
235
|
+
available_tools: available_tools_desc
|
236
|
+
)
|
91
237
|
|
92
|
-
|
93
|
-
|
238
|
+
# Force finish action if observation processor suggests it
|
239
|
+
if final_thought.action&.downcase != 'finish'
|
240
|
+
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
241
|
+
observation_result.interpretation
|
242
|
+
else
|
243
|
+
observation
|
244
|
+
end
|
245
|
+
final_answer = handle_finish_action(forced_answer, last_observation, step + 1, final_thought.thought, 'finish', history)
|
246
|
+
else
|
247
|
+
final_answer = handle_finish_action(final_thought.action_input, last_observation, step + 1, final_thought.thought, final_thought.action, history)
|
248
|
+
end
|
249
|
+
break
|
250
|
+
end
|
251
|
+
end
|
94
252
|
|
95
|
-
|
96
|
-
|
97
|
-
|
253
|
+
# Emit iteration complete event
|
254
|
+
Instrumentation.emit('dspy.react.iteration_complete', {
|
255
|
+
iteration: iterations_count,
|
256
|
+
thought: thought,
|
257
|
+
action: action,
|
258
|
+
action_input: action_input,
|
259
|
+
observation: observation,
|
260
|
+
tools_used: tools_used.uniq
|
261
|
+
})
|
262
|
+
end
|
98
263
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
action = thought_result.action
|
108
|
-
current_action_input = thought_result.action_input # What LM provided
|
109
|
-
|
110
|
-
current_step_history[:thought] = thought
|
111
|
-
current_step_history[:action] = action
|
112
|
-
|
113
|
-
if action.downcase == "finish"
|
114
|
-
# If LM says 'finish' but gives empty input, try to use last observation
|
115
|
-
if current_action_input.nil? || current_action_input.strip.empty?
|
116
|
-
# Try to find the last observation in history
|
117
|
-
last_entry_with_observation = history.reverse.find { |entry| entry.observation && !entry.observation.strip.empty? }
|
118
|
-
|
119
|
-
if last_entry_with_observation
|
120
|
-
last_observation_value = last_entry_with_observation.observation.strip
|
121
|
-
DSPy.logger.info(
|
122
|
-
module: "ReAct",
|
123
|
-
status: "Finish action had empty input. Overriding with last observation.",
|
124
|
-
original_input: current_action_input,
|
125
|
-
derived_input: last_observation_value
|
126
|
-
)
|
127
|
-
current_action_input = last_observation_value # Override
|
128
|
-
else
|
129
|
-
DSPy.logger.warn(module: "ReAct", status: "Finish action had empty input, no prior Observation found in history.", original_input: current_action_input)
|
130
|
-
end
|
264
|
+
# Check if max iterations reached
|
265
|
+
if iterations_count >= @max_iterations && final_answer.nil?
|
266
|
+
Instrumentation.emit('dspy.react.max_iterations', {
|
267
|
+
iteration_count: iterations_count,
|
268
|
+
max_iterations: @max_iterations,
|
269
|
+
tools_used: tools_used.uniq,
|
270
|
+
final_history_length: history.length
|
271
|
+
})
|
131
272
|
end
|
132
|
-
final_answer = current_action_input # Set final answer from (potentially overridden) input
|
133
273
|
end
|
134
274
|
|
135
|
-
#
|
136
|
-
|
275
|
+
# Create enhanced output with all ReAct data
|
276
|
+
output_field_name = @original_signature_class.output_struct_class.props.keys.first
|
277
|
+
output_data = kwargs.merge({
|
278
|
+
history: history.map(&:to_h),
|
279
|
+
iterations: iterations_count,
|
280
|
+
tools_used: tools_used.uniq
|
281
|
+
})
|
282
|
+
output_data[output_field_name] = final_answer || "No answer reached within #{@max_iterations} iterations"
|
283
|
+
enhanced_output = @enhanced_output_struct.new(**output_data)
|
137
284
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
end
|
285
|
+
enhanced_output
|
286
|
+
end
|
287
|
+
|
288
|
+
result
|
289
|
+
end
|
144
290
|
|
145
|
-
|
146
|
-
observation_text = execute_action(action, current_action_input) # current_action_input is original for non-finish
|
147
|
-
current_step_history[:observation] = observation_text
|
148
|
-
history << HistoryEntry.new(**current_step_history) # Add completed step to history
|
291
|
+
private
|
149
292
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
final_thought_action = final_thought_result.action
|
172
|
-
final_thought_action_input_val = final_thought_result.action_input # LM provided
|
173
|
-
|
174
|
-
final_thought_step_history[:thought] = final_thought_result.thought
|
175
|
-
final_thought_step_history[:action] = final_thought_action
|
176
|
-
|
177
|
-
if final_thought_action.downcase == "finish"
|
178
|
-
if final_thought_action_input_val.nil? || final_thought_action_input_val.strip.empty?
|
179
|
-
# Find the last observation in the history array
|
180
|
-
last_entry_with_observation = history.reverse.find { |entry| entry.observation && !entry.observation.strip.empty? }
|
181
|
-
|
182
|
-
if last_entry_with_observation
|
183
|
-
last_observation_value_ft = last_entry_with_observation.observation.strip
|
184
|
-
DSPy.logger.info(
|
185
|
-
module: "ReAct",
|
186
|
-
status: "Final thought 'finish' action had empty input. Overriding with last observation.",
|
187
|
-
original_input: final_thought_action_input_val,
|
188
|
-
derived_input: last_observation_value_ft
|
189
|
-
)
|
190
|
-
final_thought_action_input_val = last_observation_value_ft # Override
|
191
|
-
else
|
192
|
-
DSPy.logger.warn(module: "ReAct", status: "Final thought 'finish' action had empty input, last observation also empty/not found cleanly.", original_input: final_thought_action_input_val)
|
193
|
-
end
|
194
|
-
else
|
195
|
-
# This case is if LM provides 'finish' but no observation to fall back on in history array (should be rare if history is populated correctly)
|
196
|
-
DSPy.logger.warn(module: "ReAct", status: "Final thought 'finish' action had empty input, no prior Observation found in history array.", original_input: final_thought_action_input_val) if (history.empty? || !history.any? { |entry| entry.observation && !entry.observation.strip.empty? })
|
197
|
-
end
|
293
|
+
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(T::Struct)) }
|
294
|
+
def create_enhanced_output_struct(signature_class)
|
295
|
+
# Get original input and output props
|
296
|
+
input_props = signature_class.input_struct_class.props
|
297
|
+
output_props = signature_class.output_struct_class.props
|
298
|
+
|
299
|
+
# Create new struct class with input, output, and ReAct fields
|
300
|
+
Class.new(T::Struct) do
|
301
|
+
# Add all input fields
|
302
|
+
input_props.each do |name, prop|
|
303
|
+
# Extract the type and other options
|
304
|
+
type = prop[:type]
|
305
|
+
options = prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction)
|
306
|
+
|
307
|
+
# Handle default values
|
308
|
+
if options[:default]
|
309
|
+
const name, type, default: options[:default]
|
310
|
+
elsif options[:factory]
|
311
|
+
const name, type, factory: options[:factory]
|
312
|
+
else
|
313
|
+
const name, type
|
198
314
|
end
|
315
|
+
end
|
199
316
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
317
|
+
# Add all output fields
|
318
|
+
output_props.each do |name, prop|
|
319
|
+
# Extract the type and other options
|
320
|
+
type = prop[:type]
|
321
|
+
options = prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction)
|
322
|
+
|
323
|
+
# Handle default values
|
324
|
+
if options[:default]
|
325
|
+
const name, type, default: options[:default]
|
326
|
+
elsif options[:factory]
|
327
|
+
const name, type, factory: options[:factory]
|
328
|
+
else
|
329
|
+
const name, type
|
330
|
+
end
|
206
331
|
end
|
332
|
+
|
333
|
+
# Add ReAct-specific fields
|
334
|
+
prop :history, T::Array[T::Hash[Symbol, T.untyped]]
|
335
|
+
prop :iterations, Integer
|
336
|
+
prop :tools_used, T::Array[String]
|
207
337
|
end
|
338
|
+
end
|
208
339
|
|
209
|
-
|
210
|
-
|
340
|
+
sig { params(action: String, action_input: T.untyped).returns(String) }
|
341
|
+
def execute_action(action, action_input)
|
342
|
+
tool_name = action.downcase
|
343
|
+
tool = @tools[tool_name]
|
344
|
+
return "Tool '#{action}' not found. Available tools: #{@tools.keys.join(', ')}" unless tool
|
211
345
|
|
212
|
-
|
213
|
-
|
346
|
+
begin
|
347
|
+
result = if action_input.nil? ||
|
348
|
+
(action_input.is_a?(String) && action_input.strip.empty?)
|
349
|
+
# No input provided
|
350
|
+
tool.dynamic_call({})
|
351
|
+
else
|
352
|
+
# Pass the action_input directly to dynamic_call, which can handle
|
353
|
+
# either a Hash or a JSON string
|
354
|
+
tool.dynamic_call(action_input)
|
355
|
+
end
|
356
|
+
result.to_s
|
357
|
+
rescue => e
|
358
|
+
"Error executing tool '#{action}': #{e.message}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
sig { params(output: T.untyped).void }
|
363
|
+
def validate_output_schema!(output)
|
364
|
+
# Validate that output is an instance of the enhanced output struct
|
365
|
+
unless output.is_a?(@enhanced_output_struct)
|
366
|
+
raise "Output must be an instance of #{@enhanced_output_struct}, got #{output.class}"
|
367
|
+
end
|
214
368
|
|
215
|
-
#
|
216
|
-
|
217
|
-
|
218
|
-
|
369
|
+
# Validate original signature output fields are present
|
370
|
+
@original_signature_class.output_struct_class.props.each do |field_name, _prop|
|
371
|
+
unless output.respond_to?(field_name)
|
372
|
+
raise "Missing required field: #{field_name}"
|
373
|
+
end
|
374
|
+
end
|
219
375
|
|
220
|
-
#
|
221
|
-
|
222
|
-
|
376
|
+
# Validate ReAct-specific fields
|
377
|
+
unless output.respond_to?(:history) && output.history.is_a?(Array)
|
378
|
+
raise "Missing or invalid history field"
|
379
|
+
end
|
223
380
|
|
224
|
-
|
225
|
-
|
226
|
-
unless output_validation_result.success?
|
227
|
-
DSPy.logger.error(module: "ReAct", status: "Internal output validation failed", errors: output_validation_result.errors.to_h, data: output_data)
|
228
|
-
raise DSPy::PredictionInvalidError.new(output_validation_result.errors)
|
381
|
+
unless output.respond_to?(:iterations) && output.iterations.is_a?(Integer)
|
382
|
+
raise "Missing or invalid iterations field"
|
229
383
|
end
|
230
384
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
poro_class.new(**output_validation_result.to_h)
|
385
|
+
unless output.respond_to?(:tools_used) && output.tools_used.is_a?(Array)
|
386
|
+
raise "Missing or invalid tools_used field"
|
387
|
+
end
|
235
388
|
end
|
236
389
|
|
237
|
-
|
390
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
391
|
+
def generate_example_output
|
392
|
+
example = super
|
393
|
+
example[:history] = [
|
394
|
+
{
|
395
|
+
step: 1,
|
396
|
+
thought: "I need to think about this question...",
|
397
|
+
action: "some_tool",
|
398
|
+
action_input: "input for tool",
|
399
|
+
observation: "result from tool"
|
400
|
+
}
|
401
|
+
]
|
402
|
+
example[:iterations] = 1
|
403
|
+
example[:tools_used] = ["some_tool"]
|
404
|
+
example
|
405
|
+
end
|
238
406
|
|
239
|
-
|
240
|
-
|
407
|
+
sig { params(action_input: T.untyped, last_observation: T.nilable(String), step: Integer, thought: String, action: String, history: T::Array[HistoryEntry]).returns(String) }
|
408
|
+
def handle_finish_action(action_input, last_observation, step, thought, action, history)
|
409
|
+
final_answer = action_input.to_s
|
241
410
|
|
242
|
-
|
243
|
-
|
411
|
+
# If final_answer is empty but we have a last observation, use it
|
412
|
+
if (final_answer.nil? || final_answer.empty?) && last_observation
|
413
|
+
final_answer = last_observation
|
244
414
|
end
|
245
415
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
416
|
+
# Always add the finish action to history
|
417
|
+
history << HistoryEntry.new(
|
418
|
+
step: step,
|
419
|
+
thought: thought,
|
420
|
+
action: action,
|
421
|
+
action_input: final_answer,
|
422
|
+
observation: nil # No observation for finish action
|
423
|
+
)
|
424
|
+
|
425
|
+
final_answer
|
251
426
|
end
|
252
427
|
end
|
253
428
|
end
|