dspy 0.32.0 → 0.33.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3f15cdf0298a37b8ddf1ffb484e15e01e9bb8f794154d2a993dd91beaa4d22e
4
- data.tar.gz: f6d0376e92a84d0086257546111e3f456043d814f9dd051a18ce84569e30ff1e
3
+ metadata.gz: 52dc686ff0347f7844a3b6fc476b31737f3467d5d179974f34a98b8dbbd12073
4
+ data.tar.gz: 0e39c94a4766c481167268f49e42277d297b688ee9f960181785062e69f91572
5
5
  SHA512:
6
- metadata.gz: 5c26922fcf7fd867b6469bdcae24f3c2937a2f2e89b0583026a478c766c4138896709d98d598814d09220a8becd7f624bc824ad3a9f569bbac60abf7ec4e49d7
7
- data.tar.gz: a251452f69f157a026e0c23bc912f9ddc2252d4e977e2ebbc3f2276ffd293fe7ff4af7aaa97c7807c21e5cdd805fd50d54046016c44664fb153b87788e5a9a16
6
+ metadata.gz: bb4fb2ce89ed600e971a07cfabe3eb9edd344563aa77df57304dbec565121eeb9c8a53ba4cdd66f04c81cb3b1231d222a59fb4a15962680051c07de49c080dca
7
+ data.tar.gz: 2543dd3bc228c98a1ab82c14ce8fffbed86342fa661af674976b90b10752334a5606257401f9ff953bd82f36aa29fe816895acec9e30a5219e8c8dc2d3ea1727
@@ -37,7 +37,7 @@ module DSPy
37
37
  when ->(type) { hash_type?(type) }
38
38
  coerce_hash_value(value, prop_type)
39
39
  when ->(type) { type == String || simple_type_match?(type, String) }
40
- value.to_s
40
+ coerce_to_string(value)
41
41
  when ->(type) { enum_type?(type) }
42
42
  coerce_enum_value(value, prop_type)
43
43
  when ->(type) { type == Float || simple_type_match?(type, Float) }
@@ -295,6 +295,18 @@ module DSPy
295
295
  nil
296
296
  end
297
297
 
298
+ # Coerces a value to String with strict type checking
299
+ # Only allows String (passthrough) and Symbol (to_s) - rejects other types
300
+ sig { params(value: T.untyped).returns(String) }
301
+ def coerce_to_string(value)
302
+ case value
303
+ when String then value
304
+ when Symbol then value.to_s
305
+ else
306
+ raise TypeError, "Cannot coerce #{value.class} to String - expected String or Symbol"
307
+ end
308
+ end
309
+
298
310
  # Coerces a value to an enum, handling both strings and existing enum instances
299
311
  sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
300
312
  def coerce_enum_value(value, prop_type)
data/lib/dspy/module.rb CHANGED
@@ -179,6 +179,47 @@ module DSPy
179
179
  named_predictors.map { |(_, predictor)| predictor }
180
180
  end
181
181
 
182
+ # Override Dry::Configurable's configure to propagate LM to child predictors
183
+ # When you configure an agent's LM, it automatically propagates to all child predictors
184
+ # returned by named_predictors, recursively.
185
+ #
186
+ # @example Basic usage
187
+ # agent.configure { |c| c.lm = DSPy::LM.new('openai/gpt-4o') }
188
+ # # All internal predictors now use gpt-4o
189
+ #
190
+ # @example Fine-grained control (configure then override)
191
+ # agent.configure { |c| c.lm = cheap_lm }
192
+ # agent.configure_predictor('thought_generator') { |c| c.lm = expensive_lm }
193
+ #
194
+ # @return [self] for method chaining
195
+ sig { params(block: T.proc.params(config: T.untyped).void).returns(T.self_type) }
196
+ def configure(&block)
197
+ super(&block)
198
+ propagate_lm_to_children(config.lm) if config.lm
199
+ self
200
+ end
201
+
202
+ # Configure a specific child predictor by name
203
+ # Use this for fine-grained control when different predictors need different LMs
204
+ #
205
+ # @param predictor_name [String] The name of the predictor (e.g., 'thought_generator')
206
+ # @yield [config] Configuration block
207
+ # @return [self] for method chaining
208
+ # @raise [ArgumentError] if predictor_name is not found
209
+ #
210
+ # @example
211
+ # agent.configure_predictor('thought_generator') { |c| c.lm = expensive_lm }
212
+ sig { params(predictor_name: String, block: T.proc.params(config: T.untyped).void).returns(T.self_type) }
213
+ def configure_predictor(predictor_name, &block)
214
+ _, predictor = named_predictors.find { |name, _| name == predictor_name }
215
+ unless predictor
216
+ available = named_predictors.map(&:first).join(', ')
217
+ raise ArgumentError, "Unknown predictor: #{predictor_name}. Available: #{available}"
218
+ end
219
+ predictor.configure(&block)
220
+ self
221
+ end
222
+
182
223
  def instrument_forward_call(call_args, call_kwargs)
183
224
  ensure_module_subscriptions!
184
225
 
@@ -255,6 +296,21 @@ module DSPy
255
296
 
256
297
  private
257
298
 
299
+ # Propagate LM configuration to child predictors recursively
300
+ # Skips children that already have an explicit LM configured
301
+ sig { params(lm: T.untyped).void }
302
+ def propagate_lm_to_children(lm)
303
+ named_predictors.each do |(name, predictor)|
304
+ next if predictor == self # Skip self-references (Predict returns [['self', self]])
305
+
306
+ # Only propagate if child doesn't have explicit LM configured
307
+ unless predictor.config.lm
308
+ # Recursive: configure calls propagate_lm_to_children on the child too
309
+ predictor.configure { |c| c.lm = lm }
310
+ end
311
+ end
312
+ end
313
+
258
314
  def ensure_module_subscriptions!
259
315
  return if @module_subscriptions_registered
260
316
 
data/lib/dspy/re_act.rb CHANGED
@@ -9,23 +9,28 @@ require 'json'
9
9
  require_relative 'mixins/struct_builder'
10
10
 
11
11
  module DSPy
12
+ # Type alias for tool input parameters - provides semantic meaning in schemas
13
+ ToolInput = T.type_alias { T.nilable(T::Hash[String, T.untyped]) }
14
+
12
15
  # Define a simple struct for history entries with proper type annotations
13
16
  class HistoryEntry < T::Struct
14
17
  const :step, Integer
15
18
  prop :thought, T.nilable(String)
16
19
  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]))
20
+ prop :tool_input, ToolInput
18
21
  prop :observation, T.untyped
19
22
 
20
23
  # Custom serialization to ensure compatibility with the rest of the code
24
+ # Note: We don't use .compact here to ensure tool_input is always present as a key,
25
+ # even when nil, for consistent history entry structure
21
26
  def to_h
22
27
  {
23
28
  step: step,
24
29
  thought: thought,
25
30
  action: action,
26
- action_input: action_input,
31
+ tool_input: tool_input,
27
32
  observation: observation
28
- }.compact
33
+ }
29
34
  end
30
35
  end
31
36
  # Base class for ReAct thought generation - will be customized per input type
@@ -37,8 +42,10 @@ module DSPy
37
42
  description: "Reasoning about what to do next, considering the history and observations."
38
43
  const :action, String,
39
44
  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."
40
- const :action_input, T.untyped,
41
- 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 result based on processing the input data. This result MUST be directly taken from the relevant Observation in the history if available."
45
+ const :tool_input, ToolInput,
46
+ description: "Input for the chosen tool action. Required when action is a tool name. MUST be a JSON object matching the tool's parameter schema. Set to null when action is \"finish\"."
47
+ const :final_answer, T.nilable(String),
48
+ description: "The final answer to return. Required when action is \"finish\". Must match the expected output type. Set to null when action is a tool name."
42
49
  end
43
50
  end
44
51
 
@@ -72,10 +79,12 @@ module DSPy
72
79
  class TypeMismatchError < StandardError; end
73
80
 
74
81
  # AvailableTool struct for better type safety in ReAct agents
82
+ # Schema is stored as a pre-serialized string (JSON or BAML) to avoid
83
+ # T.untyped issues during schema format conversion
75
84
  class AvailableTool < T::Struct
76
85
  const :name, String
77
86
  const :description, String
78
- const :schema, T::Hash[Symbol, T.untyped]
87
+ const :schema, String
79
88
  end
80
89
 
81
90
  FINISH_ACTION = "finish"
@@ -211,7 +220,7 @@ module DSPy
211
220
  step: entry.step,
212
221
  thought: entry.thought,
213
222
  action: entry.action,
214
- action_input: serialize_for_llm(entry.action_input),
223
+ tool_input: serialize_for_llm(entry.tool_input),
215
224
  observation: serialize_for_llm(entry.observation)
216
225
  }.compact
217
226
  end
@@ -244,22 +253,26 @@ module DSPy
244
253
  def create_action_enum_class
245
254
  tool_names = @tools.keys
246
255
  all_actions = tool_names + [FINISH_ACTION]
247
-
256
+
248
257
  # Create a dynamic enum class using proper T::Enum pattern
249
258
  enum_class = Class.new(T::Enum)
250
-
259
+
260
+ # Give the anonymous class a proper name for BAML schema rendering
261
+ # This overrides the default behavior that returns #<Class:0x...>
262
+ enum_class.define_singleton_method(:name) { 'ActionEnum' }
263
+
251
264
  # Build the enums block code dynamically
252
265
  enum_definitions = all_actions.map do |action_name|
253
266
  const_name = action_name.upcase.gsub(/[^A-Z0-9_]/, '_')
254
267
  "#{const_name} = new(#{action_name.inspect})"
255
268
  end.join("\n ")
256
-
269
+
257
270
  enum_class.class_eval <<~RUBY
258
271
  enums do
259
272
  #{enum_definitions}
260
273
  end
261
274
  RUBY
262
-
275
+
263
276
  enum_class
264
277
  end
265
278
 
@@ -272,6 +285,11 @@ module DSPy
272
285
  else
273
286
  String
274
287
  end
288
+
289
+ # Get the output field type for the final_answer field
290
+ output_field_name = signature_class.output_struct_class.props.keys.first
291
+ output_field_type = signature_class.output_struct_class.props[output_field_name][:type_object]
292
+
275
293
  # Create new class that inherits from DSPy::Signature
276
294
  Class.new(DSPy::Signature) do
277
295
  # Set description
@@ -287,14 +305,16 @@ module DSPy
287
305
  description: "Array of available tools with their JSON schemas."
288
306
  end
289
307
 
290
- # Define output fields (same as ThoughtBase)
308
+ # Define output fields with separate tool_input and final_answer
291
309
  output do
292
310
  const :thought, String,
293
311
  description: "Reasoning about what to do next, considering the history and observations."
294
312
  const :action, action_enum_class,
295
313
  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."
296
- const :action_input, T.untyped,
297
- 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 result based on processing the input data."
314
+ const :tool_input, ToolInput,
315
+ description: "Input for the chosen tool action. Required when action is a tool name. MUST be a JSON object matching the tool's parameter schema. Set to null when action is \"finish\"."
316
+ const :final_answer, T.nilable(output_field_type),
317
+ description: "The final answer to return. Required when action is \"finish\". Must match the expected output type. Set to null when action is a tool name."
298
318
  end
299
319
  end
300
320
  end
@@ -337,11 +357,10 @@ module DSPy
337
357
  def execute_react_reasoning_loop(input_struct)
338
358
  history = T.let([], T::Array[HistoryEntry])
339
359
  available_tools_desc = @tools.map { |name, tool|
340
- schema = JSON.parse(tool.schema)
341
360
  AvailableTool.new(
342
361
  name: name,
343
362
  description: tool.description,
344
- schema: schema.transform_keys(&:to_sym)
363
+ schema: tool.schema
345
364
  )
346
365
  }
347
366
  final_answer = T.let(nil, T.untyped)
@@ -399,7 +418,7 @@ module DSPy
399
418
  # Process thought result
400
419
  if finish_action?(thought_obj.action)
401
420
  final_answer = handle_finish_action(
402
- thought_obj.action_input, last_observation, iteration,
421
+ thought_obj.final_answer, last_observation, iteration,
403
422
  thought_obj.thought, thought_obj.action, history
404
423
  )
405
424
  return { should_finish: true, final_answer: final_answer }
@@ -407,19 +426,19 @@ module DSPy
407
426
 
408
427
  # Execute tool action
409
428
  observation = execute_tool_with_instrumentation(
410
- thought_obj.action, thought_obj.action_input, iteration
429
+ thought_obj.action, thought_obj.tool_input, iteration
411
430
  )
412
431
 
413
432
  # Convert action enum to string for processing and storage
414
433
  action_str = thought_obj.action.respond_to?(:serialize) ? thought_obj.action.serialize : thought_obj.action.to_s
415
-
434
+
416
435
  # Track tools used
417
436
  tools_used << action_str.downcase if valid_tool?(thought_obj.action)
418
437
 
419
438
  # Add to history
420
439
  history << create_history_entry(
421
440
  iteration, thought_obj.thought, action_str,
422
- thought_obj.action_input, observation
441
+ thought_obj.tool_input, observation
423
442
  )
424
443
 
425
444
  # Process observation and decide next step
@@ -433,7 +452,7 @@ module DSPy
433
452
 
434
453
  emit_iteration_complete_event(
435
454
  iteration, thought_obj.thought, action_str,
436
- thought_obj.action_input, observation, tools_used
455
+ thought_obj.tool_input, observation, tools_used
437
456
  )
438
457
 
439
458
  {
@@ -613,8 +632,8 @@ module DSPy
613
632
  !!@tools[action_str.downcase]
614
633
  end
615
634
 
616
- sig { params(action: T.nilable(T.any(String, T::Enum)), action_input: T.untyped, iteration: Integer).returns(T.untyped) }
617
- def execute_tool_with_instrumentation(action, action_input, iteration)
635
+ sig { params(action: T.nilable(T.any(String, T::Enum)), tool_input: ToolInput, iteration: Integer).returns(T.untyped) }
636
+ def execute_tool_with_instrumentation(action, tool_input, iteration)
618
637
  raise InvalidActionError, "No action provided" unless action
619
638
 
620
639
  action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
@@ -630,19 +649,19 @@ module DSPy
630
649
  'dspy.module' => 'ReAct',
631
650
  'react.iteration' => iteration,
632
651
  'tool.name' => action_str.downcase,
633
- 'tool.input' => action_input
652
+ 'tool.input' => tool_input
634
653
  ) do
635
- execute_action(action_str, action_input)
654
+ execute_action(action_str, tool_input)
636
655
  end
637
656
  end
638
657
 
639
- sig { params(step: Integer, thought: String, action: String, action_input: T.untyped, observation: T.untyped).returns(HistoryEntry) }
640
- def create_history_entry(step, thought, action, action_input, observation)
658
+ sig { params(step: Integer, thought: String, action: String, tool_input: ToolInput, observation: T.untyped).returns(HistoryEntry) }
659
+ def create_history_entry(step, thought, action, tool_input, observation)
641
660
  HistoryEntry.new(
642
661
  step: step,
643
662
  thought: thought,
644
663
  action: action,
645
- action_input: action_input,
664
+ tool_input: tool_input,
646
665
  observation: observation
647
666
  )
648
667
  end
@@ -684,17 +703,17 @@ module DSPy
684
703
  end
685
704
  handle_finish_action(forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history)
686
705
  else
687
- handle_finish_action(final_thought.action_input, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history)
706
+ handle_finish_action(final_thought.final_answer, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history)
688
707
  end
689
708
  end
690
709
 
691
- sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation: T.untyped, tools_used: T::Array[String]).void }
692
- def emit_iteration_complete_event(iteration, thought, action, action_input, observation, tools_used)
710
+ sig { params(iteration: Integer, thought: String, action: String, tool_input: ToolInput, observation: T.untyped, tools_used: T::Array[String]).void }
711
+ def emit_iteration_complete_event(iteration, thought, action, tool_input, observation, tools_used)
693
712
  DSPy.event('react.iteration_complete', {
694
713
  'react.iteration' => iteration,
695
714
  'react.thought' => thought,
696
715
  'react.action' => action,
697
- 'react.action_input' => action_input,
716
+ 'react.tool_input' => tool_input,
698
717
  'react.observation' => observation,
699
718
  'react.tools_used' => tools_used.uniq
700
719
  })
@@ -820,8 +839,8 @@ module DSPy
820
839
  end
821
840
 
822
841
  # Tool execution method
823
- sig { params(action: String, action_input: T.untyped).returns(T.untyped) }
824
- def execute_action(action, action_input)
842
+ sig { params(action: String, tool_input: ToolInput).returns(T.untyped) }
843
+ def execute_action(action, tool_input)
825
844
  tool_name = action.downcase
826
845
  tool = @tools[tool_name]
827
846
 
@@ -829,10 +848,10 @@ module DSPy
829
848
  raise InvalidActionError, "Tool '#{action}' not found" unless tool
830
849
 
831
850
  # Execute tool - let errors propagate
832
- if action_input.nil? || (action_input.is_a?(String) && action_input.strip.empty?)
851
+ if tool_input.nil? || tool_input.empty?
833
852
  tool.dynamic_call({})
834
853
  else
835
- tool.dynamic_call(action_input)
854
+ tool.dynamic_call(tool_input)
836
855
  end
837
856
  end
838
857
 
@@ -872,7 +891,7 @@ module DSPy
872
891
  step: 1,
873
892
  thought: "I need to think about this question...",
874
893
  action: "some_tool",
875
- action_input: "input for tool",
894
+ tool_input: { "param" => "value" },
876
895
  observation: "result from tool"
877
896
  }
878
897
  ]
@@ -881,9 +900,9 @@ module DSPy
881
900
  example
882
901
  end
883
902
 
884
- sig { params(action_input: T.untyped, last_observation: T.untyped, step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry]).returns(T.untyped) }
885
- def handle_finish_action(action_input, last_observation, step, thought, action, history)
886
- final_answer = action_input
903
+ sig { params(final_answer_value: T.untyped, last_observation: T.untyped, step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry]).returns(T.untyped) }
904
+ def handle_finish_action(final_answer_value, last_observation, step, thought, action, history)
905
+ final_answer = final_answer_value
887
906
 
888
907
  # If final_answer is empty/nil but we have a last observation, use it
889
908
  if (final_answer.nil? || (final_answer.is_a?(String) && final_answer.empty?)) && last_observation
@@ -893,12 +912,12 @@ module DSPy
893
912
  # Convert action enum to string for storage in history
894
913
  action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
895
914
 
896
- # Always add the finish action to history
915
+ # Always add the finish action to history (tool_input is nil for finish actions)
897
916
  history << HistoryEntry.new(
898
917
  step: step,
899
918
  thought: thought,
900
919
  action: action_str,
901
- action_input: final_answer,
920
+ tool_input: nil,
902
921
  observation: nil # No observation for finish action
903
922
  )
904
923
 
@@ -113,16 +113,17 @@ module DSPy
113
113
  elsif type.is_a?(T::Types::TypedHash)
114
114
  # Handle hashes as objects with additionalProperties
115
115
  # TypedHash has keys and values methods to access its key and value types
116
- key_schema = self.type_to_json_schema(type.keys, visited)
116
+ # Note: propertyNames is NOT supported by OpenAI structured outputs, so we omit it
117
117
  value_schema = self.type_to_json_schema(type.values, visited)
118
-
119
- # Create a more descriptive schema for nested structures
118
+ key_type_desc = type.keys.respond_to?(:raw_type) ? type.keys.raw_type.to_s : "string"
119
+ value_type_desc = value_schema[:description] || value_schema[:type].to_s
120
+
121
+ # Create a schema compatible with OpenAI structured outputs
120
122
  {
121
123
  type: "object",
122
- propertyNames: key_schema, # Describe key constraints
123
124
  additionalProperties: value_schema,
124
- # Add a more explicit description of the expected structure
125
- description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
125
+ # Description explains the expected structure without using propertyNames
126
+ description: "A mapping where keys are #{key_type_desc}s and values are #{value_type_desc}s"
126
127
  }
127
128
  elsif type.is_a?(T::Types::FixedHash)
128
129
  # Handle fixed hashes (from type aliases like { "key" => Type })
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module DSPy
7
+ module Tools
8
+ # Represents a single parameter in a tool's schema
9
+ # Maps to JSON Schema property definitions used by LLM tool calling
10
+ class ToolParameterSchema < T::Struct
11
+ const :type, String
12
+ const :description, T.nilable(String), default: nil
13
+ const :enum, T.nilable(T::Array[String]), default: nil
14
+ end
15
+
16
+ # Represents the complete schema for a tool's parameters
17
+ # This is the "parameters" field in LLM tool definitions
18
+ class ToolSchema < T::Struct
19
+ const :type, String, default: 'object'
20
+ const :properties, T::Hash[Symbol, ToolParameterSchema], default: {}
21
+ const :required, T::Array[String], default: []
22
+
23
+ # Convert to hash format for JSON serialization
24
+ sig { returns(T::Hash[Symbol, T.untyped]) }
25
+ def to_h
26
+ {
27
+ type: type,
28
+ properties: properties.transform_values do |param|
29
+ h = { type: param.type }
30
+ h[:description] = param.description if param.description
31
+ h[:enum] = param.enum if param.enum
32
+ h
33
+ end,
34
+ required: required
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.32.0"
4
+ VERSION = "0.33.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.0
4
+ version: 0.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
@@ -99,14 +99,14 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '0.1'
102
+ version: '0.5'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '0.1'
109
+ version: '0.5'
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: sorbet-toon
112
112
  requirement: !ruby/object:Gem::Requirement
@@ -234,6 +234,7 @@ files:
234
234
  - lib/dspy/tools/base.rb
235
235
  - lib/dspy/tools/github_cli_toolset.rb
236
236
  - lib/dspy/tools/memory_toolset.rb
237
+ - lib/dspy/tools/schema.rb
237
238
  - lib/dspy/tools/text_processing_toolset.rb
238
239
  - lib/dspy/tools/toolset.rb
239
240
  - lib/dspy/type_serializer.rb