dspy 0.27.1 → 0.27.3
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/chain_of_thought.rb +29 -37
- data/lib/dspy/code_act.rb +2 -2
- data/lib/dspy/context.rb +87 -34
- data/lib/dspy/errors.rb +2 -0
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +37 -35
- data/lib/dspy/lm/adapters/gemini_adapter.rb +45 -21
- data/lib/dspy/lm/adapters/openai/schema_converter.rb +70 -40
- data/lib/dspy/lm/adapters/openai_adapter.rb +35 -8
- data/lib/dspy/lm/retry_handler.rb +15 -6
- data/lib/dspy/lm/strategies/gemini_structured_output_strategy.rb +21 -8
- data/lib/dspy/lm.rb +54 -11
- data/lib/dspy/memory/local_embedding_engine.rb +27 -11
- data/lib/dspy/memory/memory_manager.rb +26 -9
- data/lib/dspy/mixins/type_coercion.rb +96 -3
- data/lib/dspy/module.rb +20 -2
- data/lib/dspy/observability/observation_type.rb +65 -0
- data/lib/dspy/observability.rb +7 -0
- data/lib/dspy/predict.rb +27 -37
- data/lib/dspy/re_act.rb +94 -35
- data/lib/dspy/signature.rb +12 -0
- data/lib/dspy/tools/base.rb +57 -85
- data/lib/dspy/tools/github_cli_toolset.rb +330 -0
- data/lib/dspy/tools/toolset.rb +33 -60
- data/lib/dspy/type_system/sorbet_json_schema.rb +263 -0
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +1 -0
- metadata +5 -3
- data/lib/dspy/lm/cache_manager.rb +0 -151
data/lib/dspy/re_act.rb
CHANGED
@@ -66,6 +66,13 @@ module DSPy
|
|
66
66
|
extend T::Sig
|
67
67
|
include Mixins::StructBuilder
|
68
68
|
|
69
|
+
# AvailableTool struct for better type safety in ReAct agents
|
70
|
+
class AvailableTool < T::Struct
|
71
|
+
const :name, String
|
72
|
+
const :description, String
|
73
|
+
const :schema, T::Hash[Symbol, T.untyped]
|
74
|
+
end
|
75
|
+
|
69
76
|
FINISH_ACTION = "finish"
|
70
77
|
sig { returns(T.class_of(DSPy::Signature)) }
|
71
78
|
attr_reader :original_signature_class
|
@@ -87,6 +94,9 @@ module DSPy
|
|
87
94
|
tools.each { |tool| @tools[tool.name.downcase] = tool }
|
88
95
|
@max_iterations = max_iterations
|
89
96
|
|
97
|
+
# Create dynamic ActionEnum class with tool names + finish
|
98
|
+
@action_enum_class = create_action_enum_class
|
99
|
+
|
90
100
|
# Create dynamic signature classes that include the original input fields
|
91
101
|
thought_signature = create_thought_signature(signature_class)
|
92
102
|
observation_signature = create_observation_signature(signature_class)
|
@@ -143,9 +153,34 @@ module DSPy
|
|
143
153
|
|
144
154
|
private
|
145
155
|
|
156
|
+
# Creates a dynamic ActionEnum class with tool names and "finish"
|
157
|
+
sig { returns(T.class_of(T::Enum)) }
|
158
|
+
def create_action_enum_class
|
159
|
+
tool_names = @tools.keys
|
160
|
+
all_actions = tool_names + [FINISH_ACTION]
|
161
|
+
|
162
|
+
# Create a dynamic enum class using proper T::Enum pattern
|
163
|
+
enum_class = Class.new(T::Enum)
|
164
|
+
|
165
|
+
# Build the enums block code dynamically
|
166
|
+
enum_definitions = all_actions.map do |action_name|
|
167
|
+
const_name = action_name.upcase.gsub(/[^A-Z0-9_]/, '_')
|
168
|
+
"#{const_name} = new(#{action_name.inspect})"
|
169
|
+
end.join("\n ")
|
170
|
+
|
171
|
+
enum_class.class_eval <<~RUBY
|
172
|
+
enums do
|
173
|
+
#{enum_definitions}
|
174
|
+
end
|
175
|
+
RUBY
|
176
|
+
|
177
|
+
enum_class
|
178
|
+
end
|
179
|
+
|
146
180
|
# Creates a dynamic Thought signature that includes the original input fields
|
147
181
|
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
|
148
182
|
def create_thought_signature(signature_class)
|
183
|
+
action_enum_class = @action_enum_class
|
149
184
|
# Create new class that inherits from DSPy::Signature
|
150
185
|
Class.new(DSPy::Signature) do
|
151
186
|
# Set description
|
@@ -154,21 +189,21 @@ module DSPy
|
|
154
189
|
# Define input fields
|
155
190
|
input do
|
156
191
|
const :input_context, String,
|
157
|
-
|
192
|
+
description: "Serialized representation of all input fields"
|
158
193
|
const :history, T::Array[HistoryEntry],
|
159
|
-
|
160
|
-
const :available_tools, T::Array[
|
161
|
-
|
194
|
+
description: "Previous thoughts and actions, including observations from tools."
|
195
|
+
const :available_tools, T::Array[AvailableTool],
|
196
|
+
description: "Array of available tools with their JSON schemas."
|
162
197
|
end
|
163
198
|
|
164
199
|
# Define output fields (same as ThoughtBase)
|
165
200
|
output do
|
166
201
|
const :thought, String,
|
167
|
-
|
168
|
-
const :action,
|
169
|
-
|
202
|
+
description: "Reasoning about what to do next, considering the history and observations."
|
203
|
+
const :action, action_enum_class,
|
204
|
+
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."
|
170
205
|
const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
|
171
|
-
|
206
|
+
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."
|
172
207
|
end
|
173
208
|
end
|
174
209
|
end
|
@@ -184,19 +219,19 @@ module DSPy
|
|
184
219
|
# Define input fields
|
185
220
|
input do
|
186
221
|
const :input_context, String,
|
187
|
-
|
222
|
+
description: "Serialized representation of all input fields"
|
188
223
|
const :history, T::Array[HistoryEntry],
|
189
|
-
|
224
|
+
description: "Previous thoughts, actions, and observations."
|
190
225
|
const :observation, String,
|
191
|
-
|
226
|
+
description: "The result from the last action"
|
192
227
|
end
|
193
228
|
|
194
229
|
# Define output fields (same as ReActObservationBase)
|
195
230
|
output do
|
196
231
|
const :interpretation, String,
|
197
|
-
|
232
|
+
description: "Interpretation of the observation"
|
198
233
|
const :next_step, NextStep,
|
199
|
-
|
234
|
+
description: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
|
200
235
|
end
|
201
236
|
end
|
202
237
|
end
|
@@ -205,7 +240,14 @@ module DSPy
|
|
205
240
|
sig { params(input_struct: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
206
241
|
def execute_react_reasoning_loop(input_struct)
|
207
242
|
history = T.let([], T::Array[HistoryEntry])
|
208
|
-
available_tools_desc = @tools.map { |name, tool|
|
243
|
+
available_tools_desc = @tools.map { |name, tool|
|
244
|
+
schema = JSON.parse(tool.schema)
|
245
|
+
AvailableTool.new(
|
246
|
+
name: name,
|
247
|
+
description: tool.description,
|
248
|
+
schema: schema.transform_keys(&:to_sym)
|
249
|
+
)
|
250
|
+
}
|
209
251
|
final_answer = T.let(nil, T.nilable(String))
|
210
252
|
iterations_count = 0
|
211
253
|
last_observation = T.let(nil, T.nilable(String))
|
@@ -239,11 +281,12 @@ module DSPy
|
|
239
281
|
end
|
240
282
|
|
241
283
|
# Executes a single iteration of the ReAct loop
|
242
|
-
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[
|
284
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
|
243
285
|
def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
|
244
|
-
# Track each iteration with span
|
286
|
+
# Track each iteration with agent span
|
245
287
|
DSPy::Context.with_span(
|
246
288
|
operation: 'react.iteration',
|
289
|
+
**DSPy::ObservationType::Agent.langfuse_attributes,
|
247
290
|
'dspy.module' => 'ReAct',
|
248
291
|
'react.iteration' => iteration,
|
249
292
|
'react.max_iterations' => @max_iterations,
|
@@ -271,12 +314,15 @@ module DSPy
|
|
271
314
|
thought_obj.action, thought_obj.action_input, iteration
|
272
315
|
)
|
273
316
|
|
317
|
+
# Convert action enum to string for processing and storage
|
318
|
+
action_str = thought_obj.action.respond_to?(:serialize) ? thought_obj.action.serialize : thought_obj.action.to_s
|
319
|
+
|
274
320
|
# Track tools used
|
275
|
-
tools_used <<
|
321
|
+
tools_used << action_str.downcase if valid_tool?(thought_obj.action)
|
276
322
|
|
277
323
|
# Add to history
|
278
324
|
history << create_history_entry(
|
279
|
-
iteration, thought_obj.thought,
|
325
|
+
iteration, thought_obj.thought, action_str,
|
280
326
|
thought_obj.action_input, observation
|
281
327
|
)
|
282
328
|
|
@@ -290,7 +336,7 @@ module DSPy
|
|
290
336
|
end
|
291
337
|
|
292
338
|
emit_iteration_complete_event(
|
293
|
-
iteration, thought_obj.thought,
|
339
|
+
iteration, thought_obj.thought, action_str,
|
294
340
|
thought_obj.action_input, observation, tools_used
|
295
341
|
)
|
296
342
|
|
@@ -340,30 +386,39 @@ module DSPy
|
|
340
386
|
final_answer.nil? && (@max_iterations.nil? || iterations_count < @max_iterations)
|
341
387
|
end
|
342
388
|
|
343
|
-
sig { params(action: T.nilable(String)).returns(T::Boolean) }
|
389
|
+
sig { params(action: T.nilable(T.any(String, T::Enum))).returns(T::Boolean) }
|
344
390
|
def finish_action?(action)
|
345
|
-
|
391
|
+
return false unless action
|
392
|
+
action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
|
393
|
+
action_str.downcase == FINISH_ACTION
|
346
394
|
end
|
347
395
|
|
348
|
-
sig { params(action: T.nilable(String)).returns(T::Boolean) }
|
396
|
+
sig { params(action: T.nilable(T.any(String, T::Enum))).returns(T::Boolean) }
|
349
397
|
def valid_tool?(action)
|
350
|
-
|
398
|
+
return false unless action
|
399
|
+
action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
|
400
|
+
!!@tools[action_str.downcase]
|
351
401
|
end
|
352
402
|
|
353
|
-
sig { params(action: T.nilable(String), action_input: T.untyped, iteration: Integer).returns(String) }
|
403
|
+
sig { params(action: T.nilable(T.any(String, T::Enum)), action_input: T.untyped, iteration: Integer).returns(String) }
|
354
404
|
def execute_tool_with_instrumentation(action, action_input, iteration)
|
355
|
-
|
405
|
+
return "Unknown action: #{action}. Available actions: #{@tools.keys.join(', ')}, finish" unless action
|
406
|
+
|
407
|
+
action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
|
408
|
+
|
409
|
+
if @tools[action_str.downcase]
|
356
410
|
DSPy::Context.with_span(
|
357
411
|
operation: 'react.tool_call',
|
412
|
+
**DSPy::ObservationType::Tool.langfuse_attributes,
|
358
413
|
'dspy.module' => 'ReAct',
|
359
414
|
'react.iteration' => iteration,
|
360
|
-
'tool.name' =>
|
415
|
+
'tool.name' => action_str.downcase,
|
361
416
|
'tool.input' => action_input
|
362
417
|
) do
|
363
|
-
execute_action(
|
418
|
+
execute_action(action_str, action_input)
|
364
419
|
end
|
365
420
|
else
|
366
|
-
"Unknown action: #{
|
421
|
+
"Unknown action: #{action_str}. Available actions: #{@tools.keys.join(', ')}, finish"
|
367
422
|
end
|
368
423
|
end
|
369
424
|
|
@@ -378,7 +433,7 @@ module DSPy
|
|
378
433
|
)
|
379
434
|
end
|
380
435
|
|
381
|
-
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[
|
436
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[AvailableTool], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
|
382
437
|
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
383
438
|
return { should_finish: false } if observation.include?("Unknown action")
|
384
439
|
|
@@ -397,7 +452,7 @@ module DSPy
|
|
397
452
|
{ should_finish: true, final_answer: final_answer }
|
398
453
|
end
|
399
454
|
|
400
|
-
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[
|
455
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer).returns(String) }
|
401
456
|
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
402
457
|
final_thought = @thought_generator.forward(
|
403
458
|
input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
|
@@ -405,7 +460,8 @@ module DSPy
|
|
405
460
|
available_tools: available_tools_desc
|
406
461
|
)
|
407
462
|
|
408
|
-
|
463
|
+
action_str = final_thought.action.respond_to?(:serialize) ? final_thought.action.serialize : final_thought.action.to_s
|
464
|
+
if action_str.downcase != FINISH_ACTION
|
409
465
|
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
410
466
|
observation_result.interpretation
|
411
467
|
else
|
@@ -419,7 +475,7 @@ module DSPy
|
|
419
475
|
|
420
476
|
sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation: String, tools_used: T::Array[String]).void }
|
421
477
|
def emit_iteration_complete_event(iteration, thought, action, action_input, observation, tools_used)
|
422
|
-
DSPy.
|
478
|
+
DSPy.event('react.iteration_complete', {
|
423
479
|
'react.iteration' => iteration,
|
424
480
|
'react.thought' => thought,
|
425
481
|
'react.action' => action,
|
@@ -432,7 +488,7 @@ module DSPy
|
|
432
488
|
sig { params(iterations_count: Integer, final_answer: T.nilable(String), tools_used: T::Array[String], history: T::Array[HistoryEntry]).void }
|
433
489
|
def handle_max_iterations_if_needed(iterations_count, final_answer, tools_used, history)
|
434
490
|
if iterations_count >= @max_iterations && final_answer.nil?
|
435
|
-
DSPy.
|
491
|
+
DSPy.event('react.max_iterations', {
|
436
492
|
'react.iteration_count' => iterations_count,
|
437
493
|
'react.max_iterations' => @max_iterations,
|
438
494
|
'react.tools_used' => tools_used.uniq,
|
@@ -514,7 +570,7 @@ module DSPy
|
|
514
570
|
example
|
515
571
|
end
|
516
572
|
|
517
|
-
sig { params(action_input: T.untyped, last_observation: T.nilable(String), step: Integer, thought: String, action: String, history: T::Array[HistoryEntry]).returns(String) }
|
573
|
+
sig { params(action_input: T.untyped, last_observation: T.nilable(String), step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry]).returns(String) }
|
518
574
|
def handle_finish_action(action_input, last_observation, step, thought, action, history)
|
519
575
|
final_answer = action_input.to_s
|
520
576
|
|
@@ -523,11 +579,14 @@ module DSPy
|
|
523
579
|
final_answer = last_observation
|
524
580
|
end
|
525
581
|
|
582
|
+
# Convert action enum to string for storage in history
|
583
|
+
action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
|
584
|
+
|
526
585
|
# Always add the finish action to history
|
527
586
|
history << HistoryEntry.new(
|
528
587
|
step: step,
|
529
588
|
thought: thought,
|
530
|
-
action:
|
589
|
+
action: action_str,
|
531
590
|
action_input: final_answer,
|
532
591
|
observation: nil # No observation for finish action
|
533
592
|
)
|
data/lib/dspy/signature.rb
CHANGED
@@ -207,6 +207,12 @@ module DSPy
|
|
207
207
|
{ type: "number" }
|
208
208
|
elsif type == Numeric
|
209
209
|
{ type: "number" }
|
210
|
+
elsif type == Date
|
211
|
+
{ type: "string", format: "date" }
|
212
|
+
elsif type == DateTime
|
213
|
+
{ type: "string", format: "date-time" }
|
214
|
+
elsif type == Time
|
215
|
+
{ type: "string", format: "date-time" }
|
210
216
|
elsif [TrueClass, FalseClass].include?(type)
|
211
217
|
{ type: "boolean" }
|
212
218
|
elsif type < T::Struct
|
@@ -225,6 +231,12 @@ module DSPy
|
|
225
231
|
{ type: "number" }
|
226
232
|
when "Numeric"
|
227
233
|
{ type: "number" }
|
234
|
+
when "Date"
|
235
|
+
{ type: "string", format: "date" }
|
236
|
+
when "DateTime"
|
237
|
+
{ type: "string", format: "date-time" }
|
238
|
+
when "Time"
|
239
|
+
{ type: "string", format: "date-time" }
|
228
240
|
when "TrueClass", "FalseClass"
|
229
241
|
{ type: "boolean" }
|
230
242
|
when "T::Boolean"
|
data/lib/dspy/tools/base.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'sorbet-runtime'
|
4
4
|
require 'json'
|
5
|
+
require_relative '../type_system/sorbet_json_schema'
|
6
|
+
require_relative '../mixins/type_coercion'
|
5
7
|
|
6
8
|
module DSPy
|
7
9
|
module Tools
|
@@ -9,6 +11,7 @@ module DSPy
|
|
9
11
|
class Base
|
10
12
|
extend T::Sig
|
11
13
|
extend T::Helpers
|
14
|
+
include DSPy::Mixins::TypeCoercion
|
12
15
|
|
13
16
|
class << self
|
14
17
|
extend T::Sig
|
@@ -30,14 +33,14 @@ module DSPy
|
|
30
33
|
|
31
34
|
# Get the JSON schema for the call method based on its Sorbet signature
|
32
35
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
33
|
-
def
|
36
|
+
def call_schema_object
|
34
37
|
method_obj = instance_method(:call)
|
35
38
|
sig_info = T::Utils.signature_for_method(method_obj)
|
36
39
|
|
37
40
|
if sig_info.nil?
|
38
41
|
# Fallback for methods without signatures
|
39
42
|
return {
|
40
|
-
type:
|
43
|
+
type: "object",
|
41
44
|
properties: {},
|
42
45
|
required: []
|
43
46
|
}
|
@@ -50,10 +53,8 @@ module DSPy
|
|
50
53
|
sig_info.arg_types.each do |param_name, param_type|
|
51
54
|
next if param_name == :block # Skip block parameters
|
52
55
|
|
53
|
-
|
54
|
-
|
55
|
-
description: "Parameter #{param_name}"
|
56
|
-
}
|
56
|
+
schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(param_type)
|
57
|
+
properties[param_name] = schema.merge({ description: "Parameter #{param_name}" })
|
57
58
|
|
58
59
|
# Check if parameter is required (not nilable)
|
59
60
|
unless param_type.class.name.include?('Union') && param_type.name.include?('NilClass')
|
@@ -65,10 +66,8 @@ module DSPy
|
|
65
66
|
sig_info.kwarg_types.each do |param_name, param_type|
|
66
67
|
next if param_name == :block # Skip block parameters
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
description: "Parameter #{param_name}"
|
71
|
-
}
|
69
|
+
schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(param_type)
|
70
|
+
properties[param_name] = schema.merge({ description: "Parameter #{param_name}" })
|
72
71
|
|
73
72
|
# Check if parameter is required by looking at required kwarg names
|
74
73
|
if sig_info.req_kwarg_names.include?(param_name)
|
@@ -79,54 +78,25 @@ module DSPy
|
|
79
78
|
end
|
80
79
|
|
81
80
|
{
|
82
|
-
type:
|
81
|
+
type: "object",
|
83
82
|
properties: properties,
|
84
83
|
required: required
|
85
84
|
}
|
86
85
|
end
|
87
86
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
{ type: :string }
|
98
|
-
elsif raw_type == Integer
|
99
|
-
{ type: :integer }
|
100
|
-
elsif raw_type == Float
|
101
|
-
{ type: :number }
|
102
|
-
elsif raw_type == Numeric
|
103
|
-
{ type: :number }
|
104
|
-
elsif raw_type == TrueClass || raw_type == FalseClass
|
105
|
-
{ type: :boolean }
|
106
|
-
elsif raw_type == T::Boolean
|
107
|
-
{ type: :boolean }
|
108
|
-
else
|
109
|
-
{ type: :string, description: "#{raw_type} (converted to string)" }
|
110
|
-
end
|
111
|
-
elsif sorbet_type.is_a?(T::Types::Union)
|
112
|
-
# Handle nilable types
|
113
|
-
non_nil_types = sorbet_type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
114
|
-
if non_nil_types.length == 1
|
115
|
-
result = sorbet_type_to_json_schema(non_nil_types.first)
|
116
|
-
result[:description] = "#{result[:description] || ''} (optional)".strip
|
117
|
-
result
|
118
|
-
else
|
119
|
-
{ type: :string, description: "Union type (converted to string)" }
|
120
|
-
end
|
121
|
-
elsif sorbet_type.is_a?(T::Types::TypedArray)
|
122
|
-
{
|
123
|
-
type: :array,
|
124
|
-
items: sorbet_type_to_json_schema(sorbet_type.type)
|
87
|
+
# Get the full tool schema for LLM tools format
|
88
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
89
|
+
def call_schema
|
90
|
+
{
|
91
|
+
type: 'function',
|
92
|
+
function: {
|
93
|
+
name: 'call',
|
94
|
+
description: "Call the #{self.name} tool",
|
95
|
+
parameters: call_schema_object
|
125
96
|
}
|
126
|
-
|
127
|
-
{ type: :string, description: "#{sorbet_type} (converted to string)" }
|
128
|
-
end
|
97
|
+
}
|
129
98
|
end
|
99
|
+
|
130
100
|
end
|
131
101
|
|
132
102
|
# Instance methods that tools can use
|
@@ -143,7 +113,7 @@ module DSPy
|
|
143
113
|
# Get the JSON schema string for the tool, formatted for LLM consumption
|
144
114
|
sig { returns(String) }
|
145
115
|
def schema
|
146
|
-
schema_obj = self.class.
|
116
|
+
schema_obj = self.class.call_schema_object
|
147
117
|
tool_info = {
|
148
118
|
name: name,
|
149
119
|
description: description,
|
@@ -152,11 +122,17 @@ module DSPy
|
|
152
122
|
JSON.generate(tool_info)
|
153
123
|
end
|
154
124
|
|
125
|
+
# Get the full call schema compatible with LLM tools format
|
126
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
127
|
+
def call_schema
|
128
|
+
self.class.call_schema
|
129
|
+
end
|
130
|
+
|
155
131
|
# Dynamic call method for ReAct agent - parses JSON arguments and calls the typed method
|
156
132
|
sig { params(args_json: T.untyped).returns(T.untyped) }
|
157
133
|
def dynamic_call(args_json)
|
158
134
|
# Parse arguments based on the call schema
|
159
|
-
schema = self.class.
|
135
|
+
schema = self.class.call_schema_object
|
160
136
|
|
161
137
|
if schema[:properties].empty?
|
162
138
|
# No parameters - call without arguments
|
@@ -178,12 +154,34 @@ module DSPy
|
|
178
154
|
|
179
155
|
# Convert string keys to symbols and validate types
|
180
156
|
kwargs = {}
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
157
|
+
|
158
|
+
# Get method signature for type information
|
159
|
+
method_obj = self.class.instance_method(:call)
|
160
|
+
sig_info = T::Utils.signature_for_method(method_obj)
|
161
|
+
|
162
|
+
if sig_info
|
163
|
+
# Handle kwargs using type signature information
|
164
|
+
sig_info.kwarg_types.each do |param_name, param_type|
|
165
|
+
next if param_name == :block
|
166
|
+
|
167
|
+
key = param_name.to_s
|
168
|
+
if args.key?(key)
|
169
|
+
kwargs[param_name] = coerce_value_to_type(args[key], param_type)
|
170
|
+
elsif schema[:required].include?(key)
|
171
|
+
return "Error: Missing required parameter: #{key}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Handle positional args if any
|
176
|
+
sig_info.arg_types.each do |param_name, param_type|
|
177
|
+
next if param_name == :block
|
178
|
+
|
179
|
+
key = param_name.to_s
|
180
|
+
if args.key?(key)
|
181
|
+
kwargs[param_name] = coerce_value_to_type(args[key], param_type)
|
182
|
+
elsif schema[:required].include?(key)
|
183
|
+
return "Error: Missing required parameter: #{key}"
|
184
|
+
end
|
187
185
|
end
|
188
186
|
end
|
189
187
|
|
@@ -195,32 +193,6 @@ module DSPy
|
|
195
193
|
|
196
194
|
# Subclasses must implement their own call method with their own signature
|
197
195
|
|
198
|
-
protected
|
199
|
-
|
200
|
-
# Convert argument to the expected type based on JSON schema
|
201
|
-
sig { params(value: T.untyped, schema: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
202
|
-
def convert_argument_type(value, schema)
|
203
|
-
case schema[:type]
|
204
|
-
when :integer
|
205
|
-
value.is_a?(Integer) ? value : value.to_i
|
206
|
-
when :number
|
207
|
-
# Always convert to Float for :number types to ensure compatibility with strict Float signatures
|
208
|
-
value.to_f
|
209
|
-
when :boolean
|
210
|
-
case value
|
211
|
-
when true, false
|
212
|
-
value
|
213
|
-
when "true", "1", 1
|
214
|
-
true
|
215
|
-
when "false", "0", 0
|
216
|
-
false
|
217
|
-
else
|
218
|
-
!!value
|
219
|
-
end
|
220
|
-
else
|
221
|
-
value.to_s
|
222
|
-
end
|
223
|
-
end
|
224
196
|
end
|
225
197
|
end
|
226
198
|
end
|