dspy 0.27.5 → 0.28.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/README.md +28 -9
- data/lib/dspy/lm/adapter_factory.rb +1 -1
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +3 -2
- data/lib/dspy/lm/chat_strategy.rb +38 -0
- data/lib/dspy/lm/json_strategy.rb +222 -0
- data/lib/dspy/lm.rb +13 -16
- data/lib/dspy/re_act.rb +253 -68
- data/lib/dspy/signature.rb +2 -251
- data/lib/dspy/tools/base.rb +5 -7
- data/lib/dspy/type_system/sorbet_json_schema.rb +56 -18
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +0 -8
- metadata +4 -12
- data/lib/dspy/lm/retry_handler.rb +0 -132
- data/lib/dspy/lm/strategies/anthropic_extraction_strategy.rb +0 -78
- data/lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb +0 -192
- data/lib/dspy/lm/strategies/base_strategy.rb +0 -53
- data/lib/dspy/lm/strategies/enhanced_prompting_strategy.rb +0 -178
- data/lib/dspy/lm/strategies/gemini_structured_output_strategy.rb +0 -80
- data/lib/dspy/lm/strategies/openai_structured_output_strategy.rb +0 -65
- data/lib/dspy/lm/strategy_selector.rb +0 -144
- data/lib/dspy/lm/structured_output_strategy.rb +0 -17
- data/lib/dspy/strategy.rb +0 -18
data/lib/dspy/re_act.rb
CHANGED
@@ -15,7 +15,7 @@ module DSPy
|
|
15
15
|
prop :thought, T.nilable(String)
|
16
16
|
prop :action, T.nilable(String)
|
17
17
|
prop :action_input, T.nilable(T.any(String, Numeric, T::Hash[T.untyped, T.untyped], T::Array[T.untyped]))
|
18
|
-
prop :observation, T.
|
18
|
+
prop :observation, T.untyped
|
19
19
|
|
20
20
|
# Custom serialization to ensure compatibility with the rest of the code
|
21
21
|
def to_h
|
@@ -37,7 +37,7 @@ module DSPy
|
|
37
37
|
description: "Reasoning about what to do next, considering the history and observations."
|
38
38
|
const :action, String,
|
39
39
|
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.
|
40
|
+
const :action_input, T.untyped,
|
41
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."
|
42
42
|
end
|
43
43
|
end
|
@@ -66,6 +66,11 @@ module DSPy
|
|
66
66
|
extend T::Sig
|
67
67
|
include Mixins::StructBuilder
|
68
68
|
|
69
|
+
# Custom error classes
|
70
|
+
class MaxIterationsError < StandardError; end
|
71
|
+
class InvalidActionError < StandardError; end
|
72
|
+
class TypeMismatchError < StandardError; end
|
73
|
+
|
69
74
|
# AvailableTool struct for better type safety in ReAct agents
|
70
75
|
class AvailableTool < T::Struct
|
71
76
|
const :name, String
|
@@ -153,6 +158,30 @@ module DSPy
|
|
153
158
|
|
154
159
|
private
|
155
160
|
|
161
|
+
# Serialize value for LLM display
|
162
|
+
sig { params(value: T.untyped).returns(T.untyped) }
|
163
|
+
def serialize_for_llm(value)
|
164
|
+
return value if value.nil?
|
165
|
+
return value if value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
166
|
+
|
167
|
+
# For structured data, serialize to JSON-compatible format
|
168
|
+
TypeSerializer.serialize(value)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Serialize history for LLM consumption
|
172
|
+
sig { params(history: T::Array[HistoryEntry]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
173
|
+
def serialize_history_for_llm(history)
|
174
|
+
history.map do |entry|
|
175
|
+
{
|
176
|
+
step: entry.step,
|
177
|
+
thought: entry.thought,
|
178
|
+
action: entry.action,
|
179
|
+
action_input: serialize_for_llm(entry.action_input),
|
180
|
+
observation: serialize_for_llm(entry.observation)
|
181
|
+
}.compact
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
156
185
|
# Creates a dynamic ActionEnum class with tool names and "finish"
|
157
186
|
sig { returns(T.class_of(T::Enum)) }
|
158
187
|
def create_action_enum_class
|
@@ -202,7 +231,7 @@ module DSPy
|
|
202
231
|
description: "Reasoning about what to do next, considering the history and observations."
|
203
232
|
const :action, action_enum_class,
|
204
233
|
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."
|
205
|
-
const :action_input, T.
|
234
|
+
const :action_input, T.untyped,
|
206
235
|
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."
|
207
236
|
end
|
208
237
|
end
|
@@ -222,7 +251,7 @@ module DSPy
|
|
222
251
|
description: "Serialized representation of all input fields"
|
223
252
|
const :history, T::Array[HistoryEntry],
|
224
253
|
description: "Previous thoughts, actions, and observations."
|
225
|
-
const :observation,
|
254
|
+
const :observation, T.untyped,
|
226
255
|
description: "The result from the last action"
|
227
256
|
end
|
228
257
|
|
@@ -240,7 +269,7 @@ module DSPy
|
|
240
269
|
sig { params(input_struct: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
241
270
|
def execute_react_reasoning_loop(input_struct)
|
242
271
|
history = T.let([], T::Array[HistoryEntry])
|
243
|
-
available_tools_desc = @tools.map { |name, tool|
|
272
|
+
available_tools_desc = @tools.map { |name, tool|
|
244
273
|
schema = JSON.parse(tool.schema)
|
245
274
|
AvailableTool.new(
|
246
275
|
name: name,
|
@@ -248,9 +277,9 @@ module DSPy
|
|
248
277
|
schema: schema.transform_keys(&:to_sym)
|
249
278
|
)
|
250
279
|
}
|
251
|
-
final_answer = T.let(nil, T.
|
280
|
+
final_answer = T.let(nil, T.untyped)
|
252
281
|
iterations_count = 0
|
253
|
-
last_observation = T.let(nil, T.
|
282
|
+
last_observation = T.let(nil, T.untyped)
|
254
283
|
tools_used = []
|
255
284
|
|
256
285
|
while should_continue_iteration?(iterations_count, final_answer)
|
@@ -276,12 +305,12 @@ module DSPy
|
|
276
305
|
history: history,
|
277
306
|
iterations: iterations_count,
|
278
307
|
tools_used: tools_used.uniq,
|
279
|
-
final_answer: final_answer
|
308
|
+
final_answer: final_answer
|
280
309
|
}
|
281
310
|
end
|
282
311
|
|
283
312
|
# Executes a single iteration of the ReAct loop
|
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.
|
313
|
+
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.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
285
314
|
def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
|
286
315
|
# Track each iteration with agent span
|
287
316
|
DSPy::Context.with_span(
|
@@ -296,7 +325,7 @@ module DSPy
|
|
296
325
|
# Generate thought and action
|
297
326
|
thought_obj = @thought_generator.forward(
|
298
327
|
input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
|
299
|
-
history: history,
|
328
|
+
history: serialize_history_for_llm(history),
|
300
329
|
available_tools: available_tools_desc
|
301
330
|
)
|
302
331
|
|
@@ -371,26 +400,134 @@ module DSPy
|
|
371
400
|
output_field_name = @original_signature_class.output_struct_class.props.keys.first
|
372
401
|
final_answer = reasoning_result[:final_answer]
|
373
402
|
|
403
|
+
# If final_answer is nil, max iterations was reached without completion
|
404
|
+
if final_answer.nil?
|
405
|
+
iterations = reasoning_result[:iterations]
|
406
|
+
tools_used = reasoning_result[:tools_used]
|
407
|
+
raise MaxIterationsError, "Agent reached maximum iterations (#{iterations}) without producing a final answer. Tools used: #{tools_used.join(', ')}"
|
408
|
+
end
|
409
|
+
|
374
410
|
output_data = input_kwargs.merge({
|
375
411
|
history: reasoning_result[:history].map(&:to_h),
|
376
412
|
iterations: reasoning_result[:iterations],
|
377
413
|
tools_used: reasoning_result[:tools_used]
|
378
414
|
})
|
379
415
|
|
380
|
-
#
|
381
|
-
# This happens when max iterations is reached or the LLM generates an error message
|
416
|
+
# Get the expected output type
|
382
417
|
output_field_type = @original_signature_class.output_struct_class.props[output_field_name][:type_object]
|
383
|
-
|
384
|
-
|
418
|
+
|
419
|
+
# Try to deserialize final_answer to match the expected output type
|
420
|
+
deserialized_value = deserialize_final_answer(final_answer, output_field_type, reasoning_result[:history])
|
421
|
+
|
422
|
+
output_data[output_field_name] = deserialized_value
|
423
|
+
|
424
|
+
@enhanced_output_struct.new(**output_data)
|
425
|
+
end
|
426
|
+
|
427
|
+
# Find the most recent non-nil tool observation in history
|
428
|
+
sig { params(history: T::Array[HistoryEntry]).returns(T.untyped) }
|
429
|
+
def find_last_tool_observation(history)
|
430
|
+
history.reverse.find { |entry| !entry.observation.nil? }&.observation
|
431
|
+
end
|
432
|
+
|
433
|
+
# Deserialize final answer to match expected output type
|
434
|
+
# Routes to appropriate deserialization based on type classification
|
435
|
+
sig { params(final_answer: T.untyped, output_field_type: T.untyped, history: T::Array[HistoryEntry]).returns(T.untyped) }
|
436
|
+
def deserialize_final_answer(final_answer, output_field_type, history)
|
437
|
+
if scalar_type?(output_field_type)
|
438
|
+
deserialize_scalar(final_answer, output_field_type)
|
439
|
+
elsif structured_type?(output_field_type)
|
440
|
+
deserialize_structured(final_answer, output_field_type, history)
|
385
441
|
else
|
386
|
-
|
442
|
+
# Fallback for unknown types
|
443
|
+
return final_answer if type_matches?(final_answer, output_field_type)
|
444
|
+
convert_to_expected_type(final_answer, output_field_type)
|
387
445
|
end
|
446
|
+
end
|
388
447
|
|
389
|
-
|
448
|
+
# Deserialize scalar types (String, Integer, Boolean, etc.)
|
449
|
+
# Scalars: Trust LLM synthesis, minimal conversion
|
450
|
+
sig { params(final_answer: T.untyped, output_field_type: T.untyped).returns(T.untyped) }
|
451
|
+
def deserialize_scalar(final_answer, output_field_type)
|
452
|
+
# If already matches, return as-is (even if empty string for String types)
|
453
|
+
return final_answer if type_matches?(final_answer, output_field_type)
|
454
|
+
|
455
|
+
# Try basic conversion
|
456
|
+
converted = convert_to_expected_type(final_answer, output_field_type)
|
457
|
+
return converted if type_matches?(converted, output_field_type)
|
458
|
+
|
459
|
+
# Type mismatch - raise error with helpful message
|
460
|
+
expected_type = type_name(output_field_type)
|
461
|
+
actual_type = final_answer.class.name
|
462
|
+
raise TypeMismatchError, "Cannot convert final answer from #{actual_type} to #{expected_type}. Value: #{final_answer.inspect}"
|
463
|
+
end
|
464
|
+
|
465
|
+
# Deserialize structured types (arrays, hashes, structs)
|
466
|
+
# Structured: Prefer tool observation to preserve type information
|
467
|
+
sig { params(final_answer: T.untyped, output_field_type: T.untyped, history: T::Array[HistoryEntry]).returns(T.untyped) }
|
468
|
+
def deserialize_structured(final_answer, output_field_type, history)
|
469
|
+
# First, try to use the last tool observation if it matches the expected type
|
470
|
+
# This preserves type information that would be lost in LLM synthesis
|
471
|
+
last_tool_observation = find_last_tool_observation(history)
|
472
|
+
if last_tool_observation && type_matches?(last_tool_observation, output_field_type)
|
473
|
+
return last_tool_observation
|
474
|
+
end
|
475
|
+
|
476
|
+
# If final_answer already matches, use it
|
477
|
+
return final_answer if type_matches?(final_answer, output_field_type)
|
478
|
+
|
479
|
+
# Try to convert based on expected type
|
480
|
+
converted = convert_to_expected_type(final_answer, output_field_type)
|
481
|
+
return converted if type_matches?(converted, output_field_type)
|
482
|
+
|
483
|
+
# Type mismatch - raise error with helpful message
|
484
|
+
expected_type = type_name(output_field_type)
|
485
|
+
actual_type = final_answer.class.name
|
486
|
+
raise TypeMismatchError, "Cannot convert final answer from #{actual_type} to #{expected_type}. Value: #{final_answer.inspect}"
|
487
|
+
end
|
488
|
+
|
489
|
+
# Convert value to expected type
|
490
|
+
sig { params(value: T.untyped, type_object: T.untyped).returns(T.untyped) }
|
491
|
+
def convert_to_expected_type(value, type_object)
|
492
|
+
case type_object
|
493
|
+
when T::Types::TypedArray
|
494
|
+
return value unless value.is_a?(Array)
|
495
|
+
element_type = type_object.type
|
496
|
+
value.map { |item| convert_to_expected_type(item, element_type) }
|
497
|
+
when T::Types::Simple
|
498
|
+
struct_class = type_object.raw_type
|
499
|
+
if struct_class < T::Struct && value.is_a?(Hash)
|
500
|
+
# Convert string keys to symbol keys
|
501
|
+
symbolized = value.transform_keys(&:to_sym)
|
502
|
+
struct_class.new(**symbolized)
|
503
|
+
else
|
504
|
+
value
|
505
|
+
end
|
506
|
+
else
|
507
|
+
value
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
# Check if a value matches the expected type
|
512
|
+
sig { params(value: T.untyped, type_object: T.untyped).returns(T::Boolean) }
|
513
|
+
def type_matches?(value, type_object)
|
514
|
+
case type_object
|
515
|
+
when T::Types::TypedArray
|
516
|
+
value.is_a?(Array) && (value.empty? || value.first.is_a?(T::Struct))
|
517
|
+
when T::Types::TypedHash
|
518
|
+
value.is_a?(Hash)
|
519
|
+
when T::Types::Simple
|
520
|
+
value.is_a?(type_object.raw_type)
|
521
|
+
when T::Types::Union
|
522
|
+
# For union types, check if value matches any of the types
|
523
|
+
type_object.types.any? { |t| type_matches?(value, t) }
|
524
|
+
else
|
525
|
+
false
|
526
|
+
end
|
390
527
|
end
|
391
528
|
|
392
529
|
# Helper methods for ReAct logic
|
393
|
-
sig { params(iterations_count: Integer, final_answer: T.
|
530
|
+
sig { params(iterations_count: Integer, final_answer: T.untyped).returns(T::Boolean) }
|
394
531
|
def should_continue_iteration?(iterations_count, final_answer)
|
395
532
|
final_answer.nil? && (@max_iterations.nil? || iterations_count < @max_iterations)
|
396
533
|
end
|
@@ -409,29 +546,30 @@ module DSPy
|
|
409
546
|
!!@tools[action_str.downcase]
|
410
547
|
end
|
411
548
|
|
412
|
-
sig { params(action: T.nilable(T.any(String, T::Enum)), action_input: T.untyped, iteration: Integer).returns(
|
549
|
+
sig { params(action: T.nilable(T.any(String, T::Enum)), action_input: T.untyped, iteration: Integer).returns(T.untyped) }
|
413
550
|
def execute_tool_with_instrumentation(action, action_input, iteration)
|
414
|
-
|
415
|
-
|
551
|
+
raise InvalidActionError, "No action provided" unless action
|
552
|
+
|
416
553
|
action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
554
|
+
|
555
|
+
unless @tools[action_str.downcase]
|
556
|
+
available = @tools.keys.join(', ')
|
557
|
+
raise InvalidActionError, "Unknown action: #{action_str}. Available actions: #{available}, finish"
|
558
|
+
end
|
559
|
+
|
560
|
+
DSPy::Context.with_span(
|
561
|
+
operation: 'react.tool_call',
|
562
|
+
**DSPy::ObservationType::Tool.langfuse_attributes,
|
563
|
+
'dspy.module' => 'ReAct',
|
564
|
+
'react.iteration' => iteration,
|
565
|
+
'tool.name' => action_str.downcase,
|
566
|
+
'tool.input' => action_input
|
567
|
+
) do
|
568
|
+
execute_action(action_str, action_input)
|
431
569
|
end
|
432
570
|
end
|
433
571
|
|
434
|
-
sig { params(step: Integer, thought: String, action: String, action_input: T.untyped, observation:
|
572
|
+
sig { params(step: Integer, thought: String, action: String, action_input: T.untyped, observation: T.untyped).returns(HistoryEntry) }
|
435
573
|
def create_history_entry(step, thought, action, action_input, observation)
|
436
574
|
HistoryEntry.new(
|
437
575
|
step: step,
|
@@ -442,14 +580,12 @@ module DSPy
|
|
442
580
|
)
|
443
581
|
end
|
444
582
|
|
445
|
-
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation:
|
583
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: T.untyped, available_tools_desc: T::Array[AvailableTool], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
|
446
584
|
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
447
|
-
return { should_finish: false } if observation.include?("Unknown action")
|
448
|
-
|
449
585
|
observation_result = @observation_processor.forward(
|
450
586
|
input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
|
451
|
-
history: history,
|
452
|
-
observation: observation
|
587
|
+
history: serialize_history_for_llm(history),
|
588
|
+
observation: serialize_for_llm(observation)
|
453
589
|
)
|
454
590
|
|
455
591
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
@@ -461,20 +597,23 @@ module DSPy
|
|
461
597
|
{ should_finish: true, final_answer: final_answer }
|
462
598
|
end
|
463
599
|
|
464
|
-
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer).returns(
|
600
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer).returns(T.untyped) }
|
465
601
|
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
466
602
|
final_thought = @thought_generator.forward(
|
467
603
|
input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
|
468
|
-
history: history,
|
604
|
+
history: serialize_history_for_llm(history),
|
469
605
|
available_tools: available_tools_desc
|
470
606
|
)
|
471
607
|
|
472
608
|
action_str = final_thought.action.respond_to?(:serialize) ? final_thought.action.serialize : final_thought.action.to_s
|
473
609
|
if action_str.downcase != FINISH_ACTION
|
610
|
+
# Use interpretation if available, otherwise use last observation
|
474
611
|
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
475
612
|
observation_result.interpretation
|
613
|
+
elsif history.last&.observation
|
614
|
+
history.last.observation
|
476
615
|
else
|
477
|
-
|
616
|
+
raise MaxIterationsError, "Observation processor indicated finish but no answer is available"
|
478
617
|
end
|
479
618
|
handle_finish_action(forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history)
|
480
619
|
else
|
@@ -482,7 +621,7 @@ module DSPy
|
|
482
621
|
end
|
483
622
|
end
|
484
623
|
|
485
|
-
sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation:
|
624
|
+
sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation: T.untyped, tools_used: T::Array[String]).void }
|
486
625
|
def emit_iteration_complete_event(iteration, thought, action, action_input, observation, tools_used)
|
487
626
|
DSPy.event('react.iteration_complete', {
|
488
627
|
'react.iteration' => iteration,
|
@@ -494,7 +633,7 @@ module DSPy
|
|
494
633
|
})
|
495
634
|
end
|
496
635
|
|
497
|
-
sig { params(iterations_count: Integer, final_answer: T.
|
636
|
+
sig { params(iterations_count: Integer, final_answer: T.untyped, tools_used: T::Array[String], history: T::Array[HistoryEntry]).void }
|
498
637
|
def handle_max_iterations_if_needed(iterations_count, final_answer, tools_used, history)
|
499
638
|
if iterations_count >= @max_iterations && final_answer.nil?
|
500
639
|
DSPy.event('react.max_iterations', {
|
@@ -506,14 +645,44 @@ module DSPy
|
|
506
645
|
end
|
507
646
|
end
|
508
647
|
|
509
|
-
|
510
|
-
|
511
|
-
|
648
|
+
# Checks if a type is a scalar (primitives that don't need special serialization)
|
649
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
650
|
+
def scalar_type?(type_object)
|
651
|
+
case type_object
|
652
|
+
when T::Types::Simple
|
653
|
+
scalar_classes = [String, Integer, Float, Numeric, TrueClass, FalseClass]
|
654
|
+
scalar_classes.any? { |klass| type_object.raw_type == klass || type_object.raw_type <= klass }
|
655
|
+
when T::Types::Union
|
656
|
+
# Union is scalar if all its types are scalars
|
657
|
+
type_object.types.all? { |t| scalar_type?(t) }
|
658
|
+
else
|
659
|
+
false
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Checks if a type is structured (arrays, hashes, structs that need type preservation)
|
664
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
665
|
+
def structured_type?(type_object)
|
666
|
+
return true if type_object.is_a?(T::Types::TypedArray)
|
667
|
+
return true if type_object.is_a?(T::Types::TypedHash)
|
668
|
+
|
669
|
+
if type_object.is_a?(T::Types::Simple)
|
670
|
+
raw_type = type_object.raw_type
|
671
|
+
return true if raw_type.respond_to?(:<=) && raw_type <= T::Struct
|
672
|
+
end
|
673
|
+
|
674
|
+
# For union types (like T.nilable(T::Array[...])), check if any non-nil type is structured
|
675
|
+
if type_object.is_a?(T::Types::Union)
|
676
|
+
non_nil_types = type_object.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
677
|
+
return non_nil_types.any? { |t| structured_type?(t) }
|
678
|
+
end
|
679
|
+
|
680
|
+
false
|
512
681
|
end
|
513
682
|
|
514
683
|
# Checks if a type is String or compatible with String (e.g., T.any(String, ...) or T.nilable(String))
|
515
684
|
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
516
|
-
def
|
685
|
+
def string_type?(type_object)
|
517
686
|
case type_object
|
518
687
|
when T::Types::Simple
|
519
688
|
type_object.raw_type == String
|
@@ -525,6 +694,28 @@ module DSPy
|
|
525
694
|
end
|
526
695
|
end
|
527
696
|
|
697
|
+
# Alias for backward compatibility
|
698
|
+
alias string_compatible_type? string_type?
|
699
|
+
|
700
|
+
# Get a readable type name from a Sorbet type object
|
701
|
+
sig { params(type_object: T.untyped).returns(String) }
|
702
|
+
def type_name(type_object)
|
703
|
+
case type_object
|
704
|
+
when T::Types::TypedArray
|
705
|
+
element_type = type_object.type
|
706
|
+
"T::Array[#{type_name(element_type)}]"
|
707
|
+
when T::Types::TypedHash
|
708
|
+
"T::Hash"
|
709
|
+
when T::Types::Simple
|
710
|
+
type_object.raw_type.to_s
|
711
|
+
when T::Types::Union
|
712
|
+
types_str = type_object.types.map { |t| type_name(t) }.join(', ')
|
713
|
+
"T.any(#{types_str})"
|
714
|
+
else
|
715
|
+
type_object.to_s
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
528
719
|
# Returns an appropriate default value for a given Sorbet type
|
529
720
|
# This is used when max iterations is reached without a successful completion
|
530
721
|
sig { params(type_object: T.untyped).returns(T.untyped) }
|
@@ -562,25 +753,19 @@ module DSPy
|
|
562
753
|
end
|
563
754
|
|
564
755
|
# Tool execution method
|
565
|
-
sig { params(action: String, action_input: T.untyped).returns(
|
756
|
+
sig { params(action: String, action_input: T.untyped).returns(T.untyped) }
|
566
757
|
def execute_action(action, action_input)
|
567
758
|
tool_name = action.downcase
|
568
759
|
tool = @tools[tool_name]
|
569
|
-
return "Tool '#{action}' not found. Available tools: #{@tools.keys.join(', ')}" unless tool
|
570
760
|
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
tool.dynamic_call(action_input)
|
580
|
-
end
|
581
|
-
result.to_s
|
582
|
-
rescue => e
|
583
|
-
"Error executing tool '#{action}': #{e.message}"
|
761
|
+
# This should not happen since we check in execute_tool_with_instrumentation
|
762
|
+
raise InvalidActionError, "Tool '#{action}' not found" unless tool
|
763
|
+
|
764
|
+
# Execute tool - let errors propagate
|
765
|
+
if action_input.nil? || (action_input.is_a?(String) && action_input.strip.empty?)
|
766
|
+
tool.dynamic_call({})
|
767
|
+
else
|
768
|
+
tool.dynamic_call(action_input)
|
584
769
|
end
|
585
770
|
end
|
586
771
|
|
@@ -629,12 +814,12 @@ module DSPy
|
|
629
814
|
example
|
630
815
|
end
|
631
816
|
|
632
|
-
sig { params(action_input: T.untyped, last_observation: T.
|
817
|
+
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) }
|
633
818
|
def handle_finish_action(action_input, last_observation, step, thought, action, history)
|
634
|
-
final_answer = action_input
|
819
|
+
final_answer = action_input
|
635
820
|
|
636
|
-
# If final_answer is empty but we have a last observation, use it
|
637
|
-
if (final_answer.nil? || final_answer.empty?) && last_observation
|
821
|
+
# If final_answer is empty/nil but we have a last observation, use it
|
822
|
+
if (final_answer.nil? || (final_answer.is_a?(String) && final_answer.empty?)) && last_observation
|
638
823
|
final_answer = last_observation
|
639
824
|
end
|
640
825
|
|