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 +4 -4
- data/lib/dspy/mixins/type_coercion.rb +13 -1
- data/lib/dspy/module.rb +56 -0
- data/lib/dspy/re_act.rb +61 -42
- data/lib/dspy/schema/sorbet_json_schema.rb +7 -6
- data/lib/dspy/tools/schema.rb +39 -0
- data/lib/dspy/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52dc686ff0347f7844a3b6fc476b31737f3467d5d179974f34a98b8dbbd12073
|
|
4
|
+
data.tar.gz: 0e39c94a4766c481167268f49e42277d297b688ee9f960181785062e69f91572
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 :
|
|
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
|
-
|
|
31
|
+
tool_input: tool_input,
|
|
27
32
|
observation: observation
|
|
28
|
-
}
|
|
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 :
|
|
41
|
-
description: "Input for the chosen action.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 :
|
|
297
|
-
description: "Input for the chosen action.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)),
|
|
617
|
-
def execute_tool_with_instrumentation(action,
|
|
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' =>
|
|
652
|
+
'tool.input' => tool_input
|
|
634
653
|
) do
|
|
635
|
-
execute_action(action_str,
|
|
654
|
+
execute_action(action_str, tool_input)
|
|
636
655
|
end
|
|
637
656
|
end
|
|
638
657
|
|
|
639
|
-
sig { params(step: Integer, thought: String, action: String,
|
|
640
|
-
def create_history_entry(step, thought, action,
|
|
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
|
-
|
|
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.
|
|
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,
|
|
692
|
-
def emit_iteration_complete_event(iteration, thought, action,
|
|
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.
|
|
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,
|
|
824
|
-
def execute_action(action,
|
|
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
|
|
851
|
+
if tool_input.nil? || tool_input.empty?
|
|
833
852
|
tool.dynamic_call({})
|
|
834
853
|
else
|
|
835
|
-
tool.dynamic_call(
|
|
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
|
-
|
|
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(
|
|
885
|
-
def handle_finish_action(
|
|
886
|
-
final_answer =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
125
|
-
description: "A mapping where keys are #{
|
|
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
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.
|
|
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.
|
|
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.
|
|
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
|