dspy 0.2.0 → 0.3.0

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.
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'sorbet-runtime'
4
- require_relative 'sorbet_module'
5
-
6
- module DSPy
7
- class SorbetPredict < DSPy::SorbetModule
8
- extend T::Sig
9
-
10
- sig { returns(T.class_of(SorbetSignature)) }
11
- attr_reader :signature_class
12
-
13
- sig { params(signature_class: T.class_of(SorbetSignature)).void }
14
- def initialize(signature_class)
15
- @signature_class = signature_class
16
- end
17
-
18
- sig { returns(String) }
19
- def system_signature
20
- <<-PROMPT
21
- Your input schema fields are:
22
- ```json
23
- #{JSON.generate(@signature_class.input_json_schema)}
24
- ```
25
- Your output schema fields are:
26
- ```json
27
- #{JSON.generate(@signature_class.output_json_schema)}
28
- ````
29
-
30
- For example, based on the schemas above, a valid interaction would be:
31
- ## Input values
32
- ```json
33
- #{JSON.generate(generate_example_input)}
34
- ```
35
- ## Output values
36
- ```json
37
- #{JSON.generate(generate_example_output)}
38
- ```
39
-
40
- All interactions will be structured in the following way, with the appropriate values filled in.
41
-
42
- ## Input values
43
- ```json
44
- {input_values}
45
- ```
46
- ## Output values
47
- Respond exclusively with the output schema fields in the json block below.
48
- ```json
49
- {output_values}
50
- ```
51
-
52
- In adhering to this structure, your objective is: #{@signature_class.description}
53
-
54
- PROMPT
55
- end
56
-
57
- sig { returns(T::Hash[Symbol, T.untyped]) }
58
- def generate_example_input
59
- example = {}
60
- @signature_class.input_struct_class.props.each do |name, prop|
61
- example[name] = case prop[:type]
62
- when T::Types::Simple
63
- case prop[:type].raw_type.to_s
64
- when "String" then "example text"
65
- when "Integer" then 42
66
- when "Float" then 3.14
67
- else "example"
68
- end
69
- else
70
- "example"
71
- end
72
- end
73
- example
74
- end
75
-
76
- sig { returns(T::Hash[Symbol, T.untyped]) }
77
- def generate_example_output
78
- example = {}
79
- @signature_class.output_struct_class.props.each do |name, prop|
80
- example[name] = case prop[:type]
81
- when T::Types::Simple
82
- if prop[:type].raw_type < T::Enum
83
- # Use the first enum value as example
84
- prop[:type].raw_type.values.first.serialize
85
- else
86
- case prop[:type].raw_type.to_s
87
- when "String" then "example result"
88
- when "Integer" then 1
89
- when "Float" then 0.95
90
- else "example"
91
- end
92
- end
93
- else
94
- "example"
95
- end
96
- end
97
- example
98
- end
99
-
100
- sig { params(input_values: T::Hash[Symbol, T.untyped]).returns(String) }
101
- def user_signature(input_values)
102
- <<-PROMPT
103
- ## Input Values
104
- ```json
105
- #{JSON.generate(input_values)}
106
- ```
107
-
108
- Respond with the corresponding output schema fields wrapped in a ```json ``` block,
109
- starting with the heading `## Output values`.
110
- PROMPT
111
- end
112
-
113
- sig { returns(DSPy::LM) }
114
- def lm
115
- DSPy.config.lm
116
- end
117
-
118
- sig { params(input_values: T.untyped).returns(T.untyped) }
119
- def forward_untyped(**input_values)
120
- DSPy.logger.info(module: self.class.to_s, **input_values)
121
-
122
- # Validate input using T::Struct
123
- begin
124
- _input_struct = @signature_class.input_struct_class.new(**input_values)
125
- rescue ArgumentError => e
126
- raise PredictionInvalidError.new({ input: e.message })
127
- end
128
-
129
- # Use the original input_values since input_struct.to_h may not be available
130
- # The input has already been validated through the struct instantiation
131
- output_attributes = lm.chat(self, input_values)
132
-
133
- # Debug: log what we got from LM
134
- DSPy.logger.info("LM returned: #{output_attributes.inspect}")
135
- DSPy.logger.info("Output attributes class: #{output_attributes.class}")
136
-
137
- # Convert string keys to symbols
138
- output_attributes = output_attributes.transform_keys(&:to_sym)
139
-
140
- # Handle enum deserialization
141
- output_props = @signature_class.output_struct_class.props
142
- output_attributes = output_attributes.map do |key, value|
143
- prop_type = output_props[key][:type] if output_props[key]
144
- if prop_type
145
- # Check if it's an enum (can be raw Class or T::Types::Simple)
146
- enum_class = if prop_type.is_a?(Class) && prop_type < T::Enum
147
- prop_type
148
- elsif prop_type.is_a?(T::Types::Simple) && prop_type.raw_type < T::Enum
149
- prop_type.raw_type
150
- end
151
-
152
- if enum_class
153
- # Deserialize enum value
154
- [key, enum_class.deserialize(value)]
155
- elsif prop_type == Float || (prop_type.is_a?(T::Types::Simple) && prop_type.raw_type == Float)
156
- # Coerce to Float
157
- [key, value.to_f]
158
- elsif prop_type == Integer || (prop_type.is_a?(T::Types::Simple) && prop_type.raw_type == Integer)
159
- # Coerce to Integer
160
- [key, value.to_i]
161
- else
162
- [key, value]
163
- end
164
- else
165
- [key, value]
166
- end
167
- end.to_h
168
-
169
- # Create output struct with validation
170
- begin
171
- output_struct = @signature_class.output_struct_class.new(**output_attributes)
172
- return output_struct
173
- rescue ArgumentError => e
174
- raise PredictionInvalidError.new({ output: e.message })
175
- rescue TypeError => e
176
- raise PredictionInvalidError.new({ output: e.message })
177
- end
178
- end
179
- end
180
- end
@@ -1,332 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require 'sorbet-runtime'
5
- require_relative 'sorbet_predict'
6
- require_relative 'sorbet_signature'
7
- require_relative 'sorbet_chain_of_thought'
8
- require 'json'
9
-
10
- module DSPy
11
- # Define a simple struct for history entries with proper type annotations
12
- class HistoryEntry < T::Struct
13
- const :step, Integer
14
- prop :thought, T.nilable(String)
15
- prop :action, T.nilable(String)
16
- prop :action_input, T.nilable(T.any(String, Numeric, T::Hash[T.untyped, T.untyped], T::Array[T.untyped]))
17
- prop :observation, T.nilable(String)
18
-
19
- # Custom serialization to ensure compatibility with the rest of the code
20
- def to_h
21
- {
22
- step: step,
23
- thought: thought,
24
- action: action,
25
- action_input: action_input,
26
- observation: observation
27
- }.compact
28
- end
29
- end
30
- # Defines the signature for ReAct reasoning using Sorbet signatures
31
- class SorbetThought < DSPy::SorbetSignature
32
- description "Generate a thought about what to do next to answer the question."
33
-
34
- input do
35
- const :question, String,
36
- description: "The question to answer"
37
- const :history, T::Array[HistoryEntry],
38
- 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."
39
- const :available_tools, String,
40
- description: "List of available tools and their JSON schemas. The agent MUST choose an action from 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."
41
- end
42
-
43
- output do
44
- const :thought, String,
45
- description: "Reasoning about what to do next, considering the history and observations."
46
- const :action, String,
47
- 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."
48
- const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
49
- 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."
50
- end
51
- end
52
-
53
- # ReAct Agent using Sorbet signatures
54
- class SorbetReAct < SorbetPredict
55
- extend T::Sig
56
-
57
- sig { returns(T.class_of(DSPy::SorbetSignature)) }
58
- attr_reader :original_signature_class
59
-
60
- sig { returns(T.class_of(T::Struct)) }
61
- attr_reader :enhanced_output_struct
62
-
63
- sig { returns(T::Hash[String, T.untyped]) }
64
- attr_reader :tools
65
-
66
- sig { returns(Integer) }
67
- attr_reader :max_iterations
68
-
69
-
70
- sig { params(signature_class: T.class_of(DSPy::SorbetSignature), tools: T::Array[T.untyped], max_iterations: Integer).void }
71
- def initialize(signature_class, tools: [], max_iterations: 5)
72
- @original_signature_class = signature_class
73
- @tools = T.let({}, T::Hash[String, T.untyped])
74
- tools.each { |tool| @tools[tool.name.downcase] = tool }
75
- @max_iterations = max_iterations
76
-
77
- # Create thought generator using SorbetPredict to preserve field descriptions
78
- @thought_generator = T.let(DSPy::SorbetPredict.new(SorbetThought), DSPy::SorbetPredict)
79
-
80
- # Create enhanced output struct with ReAct fields
81
- @enhanced_output_struct = create_enhanced_output_struct(signature_class)
82
- enhanced_output_struct = @enhanced_output_struct
83
-
84
- # Create enhanced signature class
85
- enhanced_signature = Class.new(DSPy::SorbetSignature) do
86
- # Set the description
87
- description signature_class.description
88
-
89
- # Use the same input struct
90
- @input_struct_class = signature_class.input_struct_class
91
-
92
- # Use the enhanced output struct with ReAct fields
93
- @output_struct_class = enhanced_output_struct
94
-
95
- class << self
96
- attr_reader :input_struct_class, :output_struct_class
97
- end
98
- end
99
-
100
- # Call parent constructor with enhanced signature
101
- super(enhanced_signature)
102
- end
103
-
104
- sig { params(kwargs: T.untyped).returns(T.untyped) }
105
- def forward(**kwargs)
106
- # Validate input using Sorbet struct validation
107
- input_struct = @original_signature_class.input_struct_class.new(**kwargs)
108
-
109
- # Get the question (assume first field is the question for now)
110
- question = T.cast(input_struct.serialize.values.first, String)
111
-
112
- history = T.let([], T::Array[HistoryEntry])
113
- available_tools_desc = @tools.map { |name, tool| "- #{name}: #{tool.schema}" }.join("\n")
114
-
115
- final_answer = T.let(nil, T.nilable(String))
116
- iterations_count = 0
117
- last_observation = T.let(nil, T.nilable(String))
118
- potential_answer = T.let(nil, T.nilable(String))
119
-
120
- while @max_iterations.nil? || iterations_count < @max_iterations
121
- iterations_count += 1
122
-
123
- # Get next thought from LM
124
- thought_obj = @thought_generator.forward(
125
- question: question,
126
- history: history,
127
- available_tools: available_tools_desc
128
- )
129
-
130
- thought = thought_obj.thought
131
- action = thought_obj.action
132
- action_input = thought_obj.action_input
133
-
134
- # Store this step in history
135
- step = history.length + 1
136
- current_entry = HistoryEntry.new(
137
- step: step,
138
- thought: thought,
139
- action: action,
140
- action_input: action_input
141
- )
142
- history << current_entry
143
-
144
- if action.downcase == "finish"
145
- # If action is finish, set the final answer
146
- final_answer = action_input.to_s
147
-
148
- # If final_answer is empty but we have a last observation, use it
149
- if (final_answer.nil? || final_answer.empty?) && last_observation
150
- final_answer = last_observation
151
- # Update the action_input for consistency by replacing the last entry
152
- history.pop
153
- history << HistoryEntry.new(
154
- step: step,
155
- thought: thought,
156
- action: action,
157
- action_input: final_answer
158
- )
159
- end
160
-
161
- break
162
- end
163
-
164
- # Execute action and get observation
165
- observation = execute_action(action, action_input)
166
-
167
- # Store the raw observation for potential use as the final answer
168
- last_observation = observation
169
-
170
- # Update the entry with the observation by replacing it
171
- history.pop
172
- history << HistoryEntry.new(
173
- step: step,
174
- thought: thought,
175
- action: action,
176
- action_input: action_input,
177
- observation: "Observation: #{observation}"
178
- )
179
-
180
- # Special case for add_numbers tool - if the question is about addition and we got a numeric result
181
- if action.downcase == "add_numbers" &&
182
- question.downcase.include?("plus") &&
183
- observation.to_s.match?(/^\d+(\.\d+)?$/)
184
- # This looks like it might be the final answer to an addition question
185
- potential_answer = observation.to_s
186
- end
187
- end
188
-
189
- # If we reached max iterations without a finish action
190
- if final_answer.nil?
191
- # Try to extract answer from special cases we recognized
192
- if defined?(potential_answer) && !potential_answer.nil?
193
- final_answer = potential_answer
194
- # Otherwise use the last observation as fallback
195
- elsif last_observation
196
- final_answer = last_observation
197
- else
198
- final_answer = "I was unable to determine the answer"
199
- end
200
-
201
- # Add a finish step to history
202
- step = history.length + 1
203
- history << HistoryEntry.new(
204
- step: step,
205
- thought: "I've reached the maximum number of iterations and will provide the answer based on the tools I've used.",
206
- action: "finish",
207
- action_input: final_answer
208
- )
209
- end
210
-
211
- # Create result with enhanced output struct
212
- if @enhanced_output_struct
213
- begin
214
- # Get the first output field name from the original signature
215
- output_field_name = @original_signature_class.output_struct_class.props.keys.first
216
-
217
- # Create enhanced output struct with answer and history
218
- result = @enhanced_output_struct.new(
219
- "#{output_field_name}": final_answer || "",
220
- history: history.map(&:to_h),
221
- iterations: iterations_count
222
- )
223
-
224
- # Run validation
225
- validate_output_schema!(result)
226
-
227
- result
228
- rescue => e
229
- puts "Error creating enhanced output: #{e.message}"
230
- # Fall back to basic result
231
- Struct.new(:answer, :history, :iterations).new(final_answer || "", history, iterations_count)
232
- end
233
- else
234
- # Basic result for compatibility
235
- Struct.new(:answer, :history, :iterations).new(final_answer || "", history, iterations_count)
236
- end
237
- end
238
-
239
- private
240
-
241
- sig { params(signature_class: T.class_of(DSPy::SorbetSignature)).returns(T.class_of(T::Struct)) }
242
- def create_enhanced_output_struct(signature_class)
243
- # Get original output props
244
- original_props = signature_class.output_struct_class.props
245
-
246
- # Create new struct class with ReAct fields added
247
- Class.new(T::Struct) do
248
- # Add all original fields
249
- original_props.each do |name, prop|
250
- # Extract the type and other options
251
- type = prop[:type]
252
- options = prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction)
253
-
254
- # Handle default values
255
- if options[:default]
256
- const name, type, default: options[:default]
257
- elsif options[:factory]
258
- const name, type, factory: options[:factory]
259
- else
260
- const name, type
261
- end
262
- end
263
-
264
- # Add ReAct-specific fields
265
- const :history, T::Array[T::Hash[Symbol, T.untyped]]
266
- const :iterations, Integer
267
- end
268
- end
269
-
270
- sig { params(action: String, action_input: T.untyped).returns(String) }
271
- def execute_action(action, action_input)
272
- tool_name = action.downcase
273
- tool = @tools[tool_name]
274
- return "Tool '#{action}' not found. Available tools: #{@tools.keys.join(', ')}" unless tool
275
-
276
- begin
277
- result = if action_input.nil? ||
278
- (action_input.is_a?(String) && action_input.strip.empty?)
279
- # No input provided
280
- tool.dynamic_call({})
281
- else
282
- # Pass the action_input directly to dynamic_call, which can handle
283
- # either a Hash or a JSON string
284
- tool.dynamic_call(action_input)
285
- end
286
- result.to_s
287
- rescue => e
288
- "Error executing tool '#{action}': #{e.message}"
289
- end
290
- end
291
-
292
- sig { params(output: T.untyped).void }
293
- def validate_output_schema!(output)
294
- # Validate that output is an instance of the enhanced output struct
295
- unless output.is_a?(@enhanced_output_struct)
296
- raise "Output must be an instance of #{@enhanced_output_struct}, got #{output.class}"
297
- end
298
-
299
- # Validate original signature output fields are present
300
- @original_signature_class.output_struct_class.props.each do |field_name, _prop|
301
- unless output.respond_to?(field_name)
302
- raise "Missing required field: #{field_name}"
303
- end
304
- end
305
-
306
- # Validate ReAct-specific fields
307
- unless output.respond_to?(:history) && output.history.is_a?(Array)
308
- raise "Missing or invalid history field"
309
- end
310
-
311
- unless output.respond_to?(:iterations) && output.iterations.is_a?(Integer)
312
- raise "Missing or invalid iterations field"
313
- end
314
- end
315
-
316
- sig { override.returns(T::Hash[Symbol, T.untyped]) }
317
- def generate_example_output
318
- example = super
319
- example[:history] = [
320
- {
321
- step: 1,
322
- thought: "I need to think about this question...",
323
- action: "some_tool",
324
- action_input: "input for tool",
325
- observation: "result from tool"
326
- }
327
- ]
328
- example[:iterations] = 1
329
- example
330
- end
331
- end
332
- end