dspy 0.34.2 → 0.34.4
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 +8 -16
- data/lib/dspy/chain_of_thought.rb +3 -2
- data/lib/dspy/context.rb +70 -21
- data/lib/dspy/evals/version.rb +1 -1
- data/lib/dspy/evals.rb +42 -31
- data/lib/dspy/events.rb +2 -3
- data/lib/dspy/example.rb +1 -1
- data/lib/dspy/lm/adapter.rb +39 -0
- data/lib/dspy/lm/json_strategy.rb +28 -67
- data/lib/dspy/lm/message.rb +1 -1
- data/lib/dspy/lm/response.rb +2 -2
- data/lib/dspy/lm/usage.rb +35 -10
- data/lib/dspy/lm.rb +22 -51
- data/lib/dspy/mixins/type_coercion.rb +256 -35
- data/lib/dspy/module.rb +203 -31
- data/lib/dspy/predict.rb +33 -6
- data/lib/dspy/prediction.rb +25 -58
- data/lib/dspy/prompt.rb +52 -76
- data/lib/dspy/propose/dataset_summary_generator.rb +1 -1
- data/lib/dspy/propose/grounded_proposer.rb +3 -3
- data/lib/dspy/re_act.rb +159 -196
- data/lib/dspy/registry/signature_registry.rb +3 -3
- data/lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb +1 -27
- data/lib/dspy/schema/sorbet_json_schema.rb +7 -6
- data/lib/dspy/schema/version.rb +1 -1
- data/lib/dspy/schema_adapters.rb +1 -1
- data/lib/dspy/signature.rb +4 -5
- data/lib/dspy/storage/program_storage.rb +2 -2
- data/lib/dspy/structured_outputs_prompt.rb +4 -4
- data/lib/dspy/teleprompt/utils.rb +2 -2
- data/lib/dspy/tools/github_cli_toolset.rb +7 -7
- data/lib/dspy/tools/text_processing_toolset.rb +2 -2
- data/lib/dspy/tools/toolset.rb +1 -1
- data/lib/dspy/utils/serialization.rb +2 -6
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +50 -5
- metadata +7 -26
- data/lib/dspy/events/subscriber_mixin.rb +0 -79
- data/lib/dspy/events/subscribers.rb +0 -43
- data/lib/dspy/memory/embedding_engine.rb +0 -68
- data/lib/dspy/memory/in_memory_store.rb +0 -216
- data/lib/dspy/memory/local_embedding_engine.rb +0 -244
- data/lib/dspy/memory/memory_compactor.rb +0 -298
- data/lib/dspy/memory/memory_manager.rb +0 -266
- data/lib/dspy/memory/memory_record.rb +0 -163
- data/lib/dspy/memory/memory_store.rb +0 -90
- data/lib/dspy/memory.rb +0 -30
- data/lib/dspy/tools/memory_toolset.rb +0 -117
data/lib/dspy/re_act.rb
CHANGED
|
@@ -33,21 +33,6 @@ module DSPy
|
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
-
# Base class for ReAct thought generation - will be customized per input type
|
|
37
|
-
class ThoughtBase < DSPy::Signature
|
|
38
|
-
description "Generate a thought about what to do next to process the given inputs."
|
|
39
|
-
|
|
40
|
-
output do
|
|
41
|
-
const :thought, String,
|
|
42
|
-
description: "Reasoning about what to do next, considering the history and observations."
|
|
43
|
-
const :action, String,
|
|
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."
|
|
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."
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
36
|
|
|
52
37
|
class NextStep < T::Enum
|
|
53
38
|
enums do
|
|
@@ -56,25 +41,62 @@ module DSPy
|
|
|
56
41
|
end
|
|
57
42
|
end
|
|
58
43
|
|
|
59
|
-
# Base class for observation processing - will be customized per input type
|
|
60
|
-
class ReActObservationBase < DSPy::Signature
|
|
61
|
-
description "Process the observation from a tool and decide what to do next."
|
|
62
|
-
|
|
63
|
-
output do
|
|
64
|
-
const :interpretation, String,
|
|
65
|
-
description: "Interpretation of the observation"
|
|
66
|
-
const :next_step, NextStep,
|
|
67
|
-
description: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
44
|
# ReAct Agent using Sorbet signatures
|
|
72
45
|
class ReAct < Predict
|
|
73
46
|
extend T::Sig
|
|
74
47
|
include Mixins::StructBuilder
|
|
75
48
|
|
|
76
49
|
# Custom error classes
|
|
77
|
-
class MaxIterationsError < StandardError
|
|
50
|
+
class MaxIterationsError < StandardError
|
|
51
|
+
extend T::Sig
|
|
52
|
+
|
|
53
|
+
sig { returns(T.nilable(Integer)) }
|
|
54
|
+
attr_reader :iterations
|
|
55
|
+
|
|
56
|
+
sig { returns(T.nilable(Integer)) }
|
|
57
|
+
attr_reader :max_iterations
|
|
58
|
+
|
|
59
|
+
sig { returns(T::Array[String]) }
|
|
60
|
+
attr_reader :tools_used
|
|
61
|
+
|
|
62
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
63
|
+
attr_reader :history
|
|
64
|
+
|
|
65
|
+
sig { returns(T.untyped) }
|
|
66
|
+
attr_reader :last_observation
|
|
67
|
+
|
|
68
|
+
sig { returns(T.untyped) }
|
|
69
|
+
attr_reader :partial_final_answer
|
|
70
|
+
|
|
71
|
+
sig do
|
|
72
|
+
params(
|
|
73
|
+
message: String,
|
|
74
|
+
iterations: T.nilable(Integer),
|
|
75
|
+
max_iterations: T.nilable(Integer),
|
|
76
|
+
tools_used: T::Array[String],
|
|
77
|
+
history: T::Array[T::Hash[Symbol, T.untyped]],
|
|
78
|
+
last_observation: T.untyped,
|
|
79
|
+
partial_final_answer: T.untyped
|
|
80
|
+
).void
|
|
81
|
+
end
|
|
82
|
+
def initialize(
|
|
83
|
+
message = "Agent reached maximum iterations without producing a final answer",
|
|
84
|
+
iterations: nil,
|
|
85
|
+
max_iterations: nil,
|
|
86
|
+
tools_used: [],
|
|
87
|
+
history: [],
|
|
88
|
+
last_observation: nil,
|
|
89
|
+
partial_final_answer: nil
|
|
90
|
+
)
|
|
91
|
+
@iterations = T.let(iterations, T.nilable(Integer))
|
|
92
|
+
@max_iterations = T.let(max_iterations, T.nilable(Integer))
|
|
93
|
+
@tools_used = T.let(tools_used, T::Array[String])
|
|
94
|
+
@history = T.let(history, T::Array[T::Hash[Symbol, T.untyped]])
|
|
95
|
+
@last_observation = last_observation
|
|
96
|
+
@partial_final_answer = partial_final_answer
|
|
97
|
+
super(message)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
78
100
|
class InvalidActionError < StandardError; end
|
|
79
101
|
class TypeMismatchError < StandardError; end
|
|
80
102
|
|
|
@@ -228,19 +250,17 @@ module DSPy
|
|
|
228
250
|
|
|
229
251
|
sig { params(input_struct: T.untyped).returns(T.untyped) }
|
|
230
252
|
def format_input_context(input_struct)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
DSPy::TypeSerializer.serialize(input_struct).to_json
|
|
253
|
+
input_struct
|
|
234
254
|
end
|
|
235
255
|
|
|
236
256
|
sig { params(history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
237
257
|
def format_history(history)
|
|
238
|
-
|
|
258
|
+
history
|
|
239
259
|
end
|
|
240
260
|
|
|
241
261
|
sig { params(observation: T.untyped).returns(T.untyped) }
|
|
242
262
|
def format_observation(observation)
|
|
243
|
-
|
|
263
|
+
observation
|
|
244
264
|
end
|
|
245
265
|
|
|
246
266
|
sig { returns(T::Boolean) }
|
|
@@ -280,11 +300,7 @@ module DSPy
|
|
|
280
300
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
281
301
|
def create_thought_signature(signature_class, data_format)
|
|
282
302
|
action_enum_class = @action_enum_class
|
|
283
|
-
input_context_type =
|
|
284
|
-
signature_class.input_struct_class || String
|
|
285
|
-
else
|
|
286
|
-
String
|
|
287
|
-
end
|
|
303
|
+
input_context_type = signature_class.input_struct_class || String
|
|
288
304
|
|
|
289
305
|
# Get the output field type for the final_answer field
|
|
290
306
|
output_field_name = signature_class.output_struct_class.props.keys.first
|
|
@@ -298,7 +314,7 @@ module DSPy
|
|
|
298
314
|
# Define input fields
|
|
299
315
|
input do
|
|
300
316
|
const :input_context, input_context_type,
|
|
301
|
-
description:
|
|
317
|
+
description: "All original input fields with their typed values"
|
|
302
318
|
const :history, T::Array[HistoryEntry],
|
|
303
319
|
description: "Previous thoughts and actions, including observations from tools."
|
|
304
320
|
const :available_tools, T::Array[AvailableTool],
|
|
@@ -322,11 +338,7 @@ module DSPy
|
|
|
322
338
|
# Creates a dynamic observation signature that includes the original input fields
|
|
323
339
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
324
340
|
def create_observation_signature(signature_class, data_format)
|
|
325
|
-
input_context_type =
|
|
326
|
-
signature_class.input_struct_class || String
|
|
327
|
-
else
|
|
328
|
-
String
|
|
329
|
-
end
|
|
341
|
+
input_context_type = signature_class.input_struct_class || String
|
|
330
342
|
# Create new class that inherits from DSPy::Signature
|
|
331
343
|
Class.new(DSPy::Signature) do
|
|
332
344
|
# Set description
|
|
@@ -335,7 +347,7 @@ module DSPy
|
|
|
335
347
|
# Define input fields
|
|
336
348
|
input do
|
|
337
349
|
const :input_context, input_context_type,
|
|
338
|
-
description:
|
|
350
|
+
description: "All original input fields with their typed values"
|
|
339
351
|
const :history, T::Array[HistoryEntry],
|
|
340
352
|
description: "Previous thoughts, actions, and observations."
|
|
341
353
|
const :observation, T.untyped,
|
|
@@ -419,7 +431,7 @@ module DSPy
|
|
|
419
431
|
if finish_action?(thought_obj.action)
|
|
420
432
|
final_answer = handle_finish_action(
|
|
421
433
|
thought_obj.final_answer, last_observation, iteration,
|
|
422
|
-
thought_obj.thought, thought_obj.action, history
|
|
434
|
+
thought_obj.thought, thought_obj.action, history, expected_output_field_type
|
|
423
435
|
)
|
|
424
436
|
return { should_finish: true, final_answer: final_answer }
|
|
425
437
|
end
|
|
@@ -443,7 +455,7 @@ module DSPy
|
|
|
443
455
|
|
|
444
456
|
# Process observation and decide next step
|
|
445
457
|
observation_decision = process_observation_and_decide_next_step(
|
|
446
|
-
input_struct, history, observation, available_tools_desc, iteration
|
|
458
|
+
input_struct, history, observation, available_tools_desc, iteration, tools_used
|
|
447
459
|
)
|
|
448
460
|
|
|
449
461
|
if observation_decision[:should_finish]
|
|
@@ -490,7 +502,8 @@ module DSPy
|
|
|
490
502
|
if final_answer.nil?
|
|
491
503
|
iterations = reasoning_result[:iterations]
|
|
492
504
|
tools_used = reasoning_result[:tools_used]
|
|
493
|
-
|
|
505
|
+
history = T.cast(reasoning_result[:history], T::Array[HistoryEntry])
|
|
506
|
+
raise build_max_iterations_error(iterations: iterations, tools_used: tools_used, history: history)
|
|
494
507
|
end
|
|
495
508
|
|
|
496
509
|
output_data = input_kwargs.merge({
|
|
@@ -516,6 +529,19 @@ module DSPy
|
|
|
516
529
|
history.reverse.find { |entry| !entry.observation.nil? }&.observation
|
|
517
530
|
end
|
|
518
531
|
|
|
532
|
+
sig { params(iterations: Integer, tools_used: T::Array[String], history: T::Array[HistoryEntry], partial_final_answer: T.untyped).returns(MaxIterationsError) }
|
|
533
|
+
def build_max_iterations_error(iterations:, tools_used:, history:, partial_final_answer: nil)
|
|
534
|
+
MaxIterationsError.new(
|
|
535
|
+
"Agent reached maximum iterations (#{iterations}) without producing a final answer. Tools used: #{tools_used.join(', ')}",
|
|
536
|
+
iterations: iterations,
|
|
537
|
+
max_iterations: @max_iterations,
|
|
538
|
+
tools_used: tools_used.uniq,
|
|
539
|
+
history: history.map(&:to_h),
|
|
540
|
+
last_observation: find_last_tool_observation(history),
|
|
541
|
+
partial_final_answer: partial_final_answer
|
|
542
|
+
)
|
|
543
|
+
end
|
|
544
|
+
|
|
519
545
|
# Deserialize final answer to match expected output type
|
|
520
546
|
# Routes to appropriate deserialization based on type classification
|
|
521
547
|
sig { params(final_answer: T.untyped, output_field_type: T.untyped, history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
@@ -559,6 +585,12 @@ module DSPy
|
|
|
559
585
|
return last_tool_observation
|
|
560
586
|
end
|
|
561
587
|
|
|
588
|
+
# Structured outputs frequently arrive as JSON strings; parse before coercion.
|
|
589
|
+
if final_answer.is_a?(String)
|
|
590
|
+
parsed = parse_json_string(final_answer)
|
|
591
|
+
final_answer = parsed unless parsed.nil?
|
|
592
|
+
end
|
|
593
|
+
|
|
562
594
|
# If final_answer already matches, use it
|
|
563
595
|
return final_answer if type_matches?(final_answer, output_field_type)
|
|
564
596
|
|
|
@@ -599,7 +631,11 @@ module DSPy
|
|
|
599
631
|
def type_matches?(value, type_object)
|
|
600
632
|
case type_object
|
|
601
633
|
when T::Types::TypedArray
|
|
602
|
-
|
|
634
|
+
return false unless value.is_a?(Array)
|
|
635
|
+
return true if value.empty?
|
|
636
|
+
|
|
637
|
+
element_type = type_object.type
|
|
638
|
+
value.all? { |item| type_matches?(item, element_type) }
|
|
603
639
|
when T::Types::TypedHash
|
|
604
640
|
value.is_a?(Hash)
|
|
605
641
|
when T::Types::Simple
|
|
@@ -649,12 +685,25 @@ module DSPy
|
|
|
649
685
|
'dspy.module' => 'ReAct',
|
|
650
686
|
'react.iteration' => iteration,
|
|
651
687
|
'tool.name' => action_str.downcase,
|
|
688
|
+
'langfuse.observation.input' => serialize_tool_payload(tool_input),
|
|
652
689
|
'tool.input' => tool_input
|
|
653
|
-
) do
|
|
654
|
-
execute_action(action_str, tool_input)
|
|
690
|
+
) do |span|
|
|
691
|
+
result = execute_action(action_str, tool_input)
|
|
692
|
+
if span
|
|
693
|
+
span.set_attribute('langfuse.observation.output', serialize_tool_payload(result))
|
|
694
|
+
end
|
|
695
|
+
result
|
|
655
696
|
end
|
|
656
697
|
end
|
|
657
698
|
|
|
699
|
+
sig { params(payload: T.untyped).returns(String) }
|
|
700
|
+
def serialize_tool_payload(payload)
|
|
701
|
+
serialized = DSPy::TypeSerializer.serialize(payload)
|
|
702
|
+
JSON.generate(serialized)
|
|
703
|
+
rescue StandardError
|
|
704
|
+
payload.to_s
|
|
705
|
+
end
|
|
706
|
+
|
|
658
707
|
sig { params(step: Integer, thought: String, action: String, tool_input: ToolInput, observation: T.untyped).returns(HistoryEntry) }
|
|
659
708
|
def create_history_entry(step, thought, action, tool_input, observation)
|
|
660
709
|
HistoryEntry.new(
|
|
@@ -666,8 +715,8 @@ module DSPy
|
|
|
666
715
|
)
|
|
667
716
|
end
|
|
668
717
|
|
|
669
|
-
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]) }
|
|
670
|
-
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
|
718
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: T.untyped, available_tools_desc: T::Array[AvailableTool], iteration: Integer, tools_used: T::Array[String]).returns(T::Hash[Symbol, T.untyped]) }
|
|
719
|
+
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration, tools_used)
|
|
671
720
|
observation_result = @observation_processor.forward(
|
|
672
721
|
input_context: format_input_context(input_struct),
|
|
673
722
|
history: format_history(history),
|
|
@@ -677,14 +726,14 @@ module DSPy
|
|
|
677
726
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
|
678
727
|
|
|
679
728
|
final_answer = generate_forced_final_answer(
|
|
680
|
-
input_struct, history, available_tools_desc, observation_result, iteration
|
|
729
|
+
input_struct, history, available_tools_desc, observation_result, iteration, tools_used
|
|
681
730
|
)
|
|
682
731
|
|
|
683
732
|
{ should_finish: true, final_answer: final_answer }
|
|
684
733
|
end
|
|
685
734
|
|
|
686
|
-
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) }
|
|
687
|
-
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
|
735
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer, tools_used: T::Array[String]).returns(T.untyped) }
|
|
736
|
+
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration, tools_used)
|
|
688
737
|
final_thought = @thought_generator.forward(
|
|
689
738
|
input_context: format_input_context(input_struct),
|
|
690
739
|
history: format_history(history),
|
|
@@ -692,6 +741,8 @@ module DSPy
|
|
|
692
741
|
)
|
|
693
742
|
|
|
694
743
|
action_str = final_thought.action.respond_to?(:serialize) ? final_thought.action.serialize : final_thought.action.to_s
|
|
744
|
+
output_field_type = expected_output_field_type
|
|
745
|
+
|
|
695
746
|
if action_str.downcase != FINISH_ACTION
|
|
696
747
|
# Use interpretation if available, otherwise use last observation
|
|
697
748
|
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
|
@@ -701,9 +752,23 @@ module DSPy
|
|
|
701
752
|
else
|
|
702
753
|
raise MaxIterationsError, "Observation processor indicated finish but no answer is available"
|
|
703
754
|
end
|
|
704
|
-
|
|
755
|
+
|
|
756
|
+
if structured_type?(output_field_type)
|
|
757
|
+
coerced_forced_answer = coerce_candidate_for_output(forced_answer, output_field_type)
|
|
758
|
+
if coerced_forced_answer.nil?
|
|
759
|
+
raise build_max_iterations_error(
|
|
760
|
+
iterations: iteration,
|
|
761
|
+
tools_used: tools_used,
|
|
762
|
+
history: history,
|
|
763
|
+
partial_final_answer: forced_answer
|
|
764
|
+
)
|
|
765
|
+
end
|
|
766
|
+
handle_finish_action(coerced_forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history, output_field_type)
|
|
767
|
+
else
|
|
768
|
+
handle_finish_action(forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history, output_field_type)
|
|
769
|
+
end
|
|
705
770
|
else
|
|
706
|
-
handle_finish_action(final_thought.final_answer, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history)
|
|
771
|
+
handle_finish_action(final_thought.final_answer, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history, output_field_type)
|
|
707
772
|
end
|
|
708
773
|
end
|
|
709
774
|
|
|
@@ -731,113 +796,6 @@ module DSPy
|
|
|
731
796
|
end
|
|
732
797
|
end
|
|
733
798
|
|
|
734
|
-
# Checks if a type is a scalar (primitives that don't need special serialization)
|
|
735
|
-
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
736
|
-
def scalar_type?(type_object)
|
|
737
|
-
case type_object
|
|
738
|
-
when T::Types::Simple
|
|
739
|
-
scalar_classes = [String, Integer, Float, Numeric, TrueClass, FalseClass]
|
|
740
|
-
scalar_classes.any? { |klass| type_object.raw_type == klass || type_object.raw_type <= klass }
|
|
741
|
-
when T::Types::Union
|
|
742
|
-
# Union is scalar if all its types are scalars
|
|
743
|
-
type_object.types.all? { |t| scalar_type?(t) }
|
|
744
|
-
else
|
|
745
|
-
false
|
|
746
|
-
end
|
|
747
|
-
end
|
|
748
|
-
|
|
749
|
-
# Checks if a type is structured (arrays, hashes, structs that need type preservation)
|
|
750
|
-
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
751
|
-
def structured_type?(type_object)
|
|
752
|
-
return true if type_object.is_a?(T::Types::TypedArray)
|
|
753
|
-
return true if type_object.is_a?(T::Types::TypedHash)
|
|
754
|
-
|
|
755
|
-
if type_object.is_a?(T::Types::Simple)
|
|
756
|
-
raw_type = type_object.raw_type
|
|
757
|
-
return true if raw_type.respond_to?(:<=) && raw_type <= T::Struct
|
|
758
|
-
end
|
|
759
|
-
|
|
760
|
-
# For union types (like T.nilable(T::Array[...])), check if any non-nil type is structured
|
|
761
|
-
if type_object.is_a?(T::Types::Union)
|
|
762
|
-
non_nil_types = type_object.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
763
|
-
return non_nil_types.any? { |t| structured_type?(t) }
|
|
764
|
-
end
|
|
765
|
-
|
|
766
|
-
false
|
|
767
|
-
end
|
|
768
|
-
|
|
769
|
-
# Checks if a type is String or compatible with String (e.g., T.any(String, ...) or T.nilable(String))
|
|
770
|
-
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
771
|
-
def string_type?(type_object)
|
|
772
|
-
case type_object
|
|
773
|
-
when T::Types::Simple
|
|
774
|
-
type_object.raw_type == String
|
|
775
|
-
when T::Types::Union
|
|
776
|
-
# Check if any of the union types is String
|
|
777
|
-
type_object.types.any? { |t| t.is_a?(T::Types::Simple) && t.raw_type == String }
|
|
778
|
-
else
|
|
779
|
-
false
|
|
780
|
-
end
|
|
781
|
-
end
|
|
782
|
-
|
|
783
|
-
# Alias for backward compatibility
|
|
784
|
-
alias string_compatible_type? string_type?
|
|
785
|
-
|
|
786
|
-
# Get a readable type name from a Sorbet type object
|
|
787
|
-
sig { params(type_object: T.untyped).returns(String) }
|
|
788
|
-
def type_name(type_object)
|
|
789
|
-
case type_object
|
|
790
|
-
when T::Types::TypedArray
|
|
791
|
-
element_type = type_object.type
|
|
792
|
-
"T::Array[#{type_name(element_type)}]"
|
|
793
|
-
when T::Types::TypedHash
|
|
794
|
-
"T::Hash"
|
|
795
|
-
when T::Types::Simple
|
|
796
|
-
type_object.raw_type.to_s
|
|
797
|
-
when T::Types::Union
|
|
798
|
-
types_str = type_object.types.map { |t| type_name(t) }.join(', ')
|
|
799
|
-
"T.any(#{types_str})"
|
|
800
|
-
else
|
|
801
|
-
type_object.to_s
|
|
802
|
-
end
|
|
803
|
-
end
|
|
804
|
-
|
|
805
|
-
# Returns an appropriate default value for a given Sorbet type
|
|
806
|
-
# This is used when max iterations is reached without a successful completion
|
|
807
|
-
sig { params(type_object: T.untyped).returns(T.untyped) }
|
|
808
|
-
def default_value_for_type(type_object)
|
|
809
|
-
# Handle TypedArray (T::Array[...])
|
|
810
|
-
if type_object.is_a?(T::Types::TypedArray)
|
|
811
|
-
return []
|
|
812
|
-
end
|
|
813
|
-
|
|
814
|
-
# Handle TypedHash (T::Hash[...])
|
|
815
|
-
if type_object.is_a?(T::Types::TypedHash)
|
|
816
|
-
return {}
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
# Handle simple types
|
|
820
|
-
case type_object
|
|
821
|
-
when T::Types::Simple
|
|
822
|
-
raw_type = type_object.raw_type
|
|
823
|
-
case raw_type.to_s
|
|
824
|
-
when 'String' then ''
|
|
825
|
-
when 'Integer' then 0
|
|
826
|
-
when 'Float' then 0.0
|
|
827
|
-
when 'TrueClass', 'FalseClass' then false
|
|
828
|
-
else
|
|
829
|
-
# For T::Struct types, return nil as fallback
|
|
830
|
-
nil
|
|
831
|
-
end
|
|
832
|
-
when T::Types::Union
|
|
833
|
-
# For unions, return nil (assuming it's nilable) or first non-nil default
|
|
834
|
-
nil
|
|
835
|
-
else
|
|
836
|
-
# Default fallback for unknown types
|
|
837
|
-
nil
|
|
838
|
-
end
|
|
839
|
-
end
|
|
840
|
-
|
|
841
799
|
# Tool execution method
|
|
842
800
|
sig { params(action: String, tool_input: ToolInput).returns(T.untyped) }
|
|
843
801
|
def execute_action(action, tool_input)
|
|
@@ -855,34 +813,6 @@ module DSPy
|
|
|
855
813
|
end
|
|
856
814
|
end
|
|
857
815
|
|
|
858
|
-
sig { params(output: T.untyped).void }
|
|
859
|
-
def validate_output_schema!(output)
|
|
860
|
-
# Validate that output is an instance of the enhanced output struct
|
|
861
|
-
unless output.is_a?(@enhanced_output_struct)
|
|
862
|
-
raise "Output must be an instance of #{@enhanced_output_struct}, got #{output.class}"
|
|
863
|
-
end
|
|
864
|
-
|
|
865
|
-
# Validate original signature output fields are present
|
|
866
|
-
@original_signature_class.output_struct_class.props.each do |field_name, _prop|
|
|
867
|
-
unless output.respond_to?(field_name)
|
|
868
|
-
raise "Missing required field: #{field_name}"
|
|
869
|
-
end
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
# Validate ReAct-specific fields
|
|
873
|
-
unless output.respond_to?(:history) && output.history.is_a?(Array)
|
|
874
|
-
raise "Missing or invalid history field"
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
unless output.respond_to?(:iterations) && output.iterations.is_a?(Integer)
|
|
878
|
-
raise "Missing or invalid iterations field"
|
|
879
|
-
end
|
|
880
|
-
|
|
881
|
-
unless output.respond_to?(:tools_used) && output.tools_used.is_a?(Array)
|
|
882
|
-
raise "Missing or invalid tools_used field"
|
|
883
|
-
end
|
|
884
|
-
end
|
|
885
|
-
|
|
886
816
|
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
887
817
|
def generate_example_output
|
|
888
818
|
example = super
|
|
@@ -900,13 +830,14 @@ module DSPy
|
|
|
900
830
|
example
|
|
901
831
|
end
|
|
902
832
|
|
|
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)
|
|
833
|
+
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], output_field_type: T.untyped).returns(T.untyped) }
|
|
834
|
+
def handle_finish_action(final_answer_value, last_observation, step, thought, action, history, output_field_type)
|
|
905
835
|
final_answer = final_answer_value
|
|
906
836
|
|
|
907
837
|
# If final_answer is empty/nil but we have a last observation, use it
|
|
908
838
|
if (final_answer.nil? || (final_answer.is_a?(String) && final_answer.empty?)) && last_observation
|
|
909
|
-
|
|
839
|
+
fallback_value = coerce_candidate_for_output(last_observation, output_field_type)
|
|
840
|
+
final_answer = fallback_value unless fallback_value.nil?
|
|
910
841
|
end
|
|
911
842
|
|
|
912
843
|
# Convert action enum to string for storage in history
|
|
@@ -923,5 +854,37 @@ module DSPy
|
|
|
923
854
|
|
|
924
855
|
final_answer
|
|
925
856
|
end
|
|
857
|
+
|
|
858
|
+
sig { returns(T.untyped) }
|
|
859
|
+
def expected_output_field_type
|
|
860
|
+
output_field_name = @original_signature_class.output_struct_class.props.keys.first
|
|
861
|
+
@original_signature_class.output_struct_class.props[output_field_name][:type_object]
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
sig { params(value: T.untyped, output_field_type: T.untyped).returns(T.untyped) }
|
|
865
|
+
def coerce_candidate_for_output(value, output_field_type)
|
|
866
|
+
return value if type_matches?(value, output_field_type)
|
|
867
|
+
|
|
868
|
+
candidate = value
|
|
869
|
+
if candidate.is_a?(String) && structured_type?(output_field_type)
|
|
870
|
+
parsed = parse_json_string(candidate)
|
|
871
|
+
candidate = parsed unless parsed.nil?
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
return candidate if type_matches?(candidate, output_field_type)
|
|
875
|
+
|
|
876
|
+
converted = convert_to_expected_type(candidate, output_field_type)
|
|
877
|
+
return converted if type_matches?(converted, output_field_type)
|
|
878
|
+
|
|
879
|
+
nil
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
sig { params(value: String).returns(T.nilable(T.untyped)) }
|
|
883
|
+
def parse_json_string(value)
|
|
884
|
+
parsed = JSON.parse(value)
|
|
885
|
+
parsed
|
|
886
|
+
rescue JSON::ParserError
|
|
887
|
+
nil
|
|
888
|
+
end
|
|
926
889
|
end
|
|
927
890
|
end
|
|
@@ -259,7 +259,7 @@ module DSPy
|
|
|
259
259
|
emit_register_complete_event(signature_version)
|
|
260
260
|
signature_version
|
|
261
261
|
|
|
262
|
-
rescue => error
|
|
262
|
+
rescue StandardError => error
|
|
263
263
|
emit_register_error_event(signature_name, version, error)
|
|
264
264
|
raise
|
|
265
265
|
end
|
|
@@ -313,7 +313,7 @@ module DSPy
|
|
|
313
313
|
emit_deploy_complete_event(deployed_version)
|
|
314
314
|
deployed_version
|
|
315
315
|
|
|
316
|
-
rescue => error
|
|
316
|
+
rescue StandardError => error
|
|
317
317
|
emit_deploy_error_event(signature_name, version, error)
|
|
318
318
|
nil
|
|
319
319
|
end
|
|
@@ -363,7 +363,7 @@ module DSPy
|
|
|
363
363
|
emit_rollback_error_event(signature_name, "No previous version to rollback to")
|
|
364
364
|
nil
|
|
365
365
|
|
|
366
|
-
rescue => error
|
|
366
|
+
rescue StandardError => error
|
|
367
367
|
emit_rollback_error_event(signature_name, error.message)
|
|
368
368
|
nil
|
|
369
369
|
end
|
|
@@ -52,7 +52,7 @@ module DSPy
|
|
|
52
52
|
# Validate vision support if images are present
|
|
53
53
|
if contains_images?(normalized_messages)
|
|
54
54
|
validate_vision_support!
|
|
55
|
-
normalized_messages = format_multimodal_messages(normalized_messages)
|
|
55
|
+
normalized_messages = format_multimodal_messages(normalized_messages, provider)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
chat_instance = create_chat_instance
|
|
@@ -358,32 +358,6 @@ module DSPy
|
|
|
358
358
|
# If DSPy doesn't know about the model, let RubyLLM handle it
|
|
359
359
|
# RubyLLM has its own model registry with capability detection
|
|
360
360
|
end
|
|
361
|
-
|
|
362
|
-
def format_multimodal_messages(messages)
|
|
363
|
-
messages.map do |msg|
|
|
364
|
-
if msg[:content].is_a?(Array)
|
|
365
|
-
formatted_content = msg[:content].map do |item|
|
|
366
|
-
case item[:type]
|
|
367
|
-
when 'text'
|
|
368
|
-
{ type: 'text', text: item[:text] }
|
|
369
|
-
when 'image'
|
|
370
|
-
# Validate and format image for provider
|
|
371
|
-
image = item[:image]
|
|
372
|
-
if image.respond_to?(:validate_for_provider!)
|
|
373
|
-
image.validate_for_provider!(provider)
|
|
374
|
-
end
|
|
375
|
-
item
|
|
376
|
-
else
|
|
377
|
-
item
|
|
378
|
-
end
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
{ role: msg[:role], content: formatted_content }
|
|
382
|
-
else
|
|
383
|
-
msg
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
361
|
end
|
|
388
362
|
end
|
|
389
363
|
end
|
|
@@ -195,10 +195,10 @@ module DSPy
|
|
|
195
195
|
end
|
|
196
196
|
else
|
|
197
197
|
# Not nilable SimplePairUnion - this is a regular T.any() union
|
|
198
|
-
# Generate
|
|
198
|
+
# Generate anyOf schema for all types (oneOf not supported by Anthropic strict mode)
|
|
199
199
|
if type.respond_to?(:types) && type.types.length > 1
|
|
200
200
|
{
|
|
201
|
-
|
|
201
|
+
anyOf: type.types.map { |t| type_to_json_schema_internal(t, visited, definitions) },
|
|
202
202
|
description: "Union of multiple types"
|
|
203
203
|
}
|
|
204
204
|
else
|
|
@@ -237,14 +237,14 @@ module DSPy
|
|
|
237
237
|
# Non-nilable single type union (shouldn't happen in practice)
|
|
238
238
|
type_to_json_schema_internal(non_nil_types.first, visited, definitions)
|
|
239
239
|
elsif non_nil_types.size > 1
|
|
240
|
-
# Handle complex unions with oneOf
|
|
240
|
+
# Handle complex unions with anyOf (oneOf not supported by Anthropic strict mode)
|
|
241
241
|
base_schema = {
|
|
242
|
-
|
|
242
|
+
anyOf: non_nil_types.map { |t| type_to_json_schema_internal(t, visited, definitions) },
|
|
243
243
|
description: "Union of multiple types"
|
|
244
244
|
}
|
|
245
245
|
if is_nilable
|
|
246
246
|
# Add null as an option for complex nilable unions
|
|
247
|
-
base_schema[:
|
|
247
|
+
base_schema[:anyOf] << { type: "null" }
|
|
248
248
|
end
|
|
249
249
|
base_schema
|
|
250
250
|
else
|
|
@@ -335,7 +335,8 @@ module DSPy
|
|
|
335
335
|
type: "object",
|
|
336
336
|
properties: properties,
|
|
337
337
|
required: required,
|
|
338
|
-
description: "#{struct_name} struct"
|
|
338
|
+
description: "#{struct_name} struct",
|
|
339
|
+
additionalProperties: false
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
# Add this struct's schema to definitions for $defs
|
data/lib/dspy/schema/version.rb
CHANGED
data/lib/dspy/schema_adapters.rb
CHANGED
data/lib/dspy/signature.rb
CHANGED
|
@@ -58,11 +58,10 @@ module DSPy
|
|
|
58
58
|
Class.new(T::Struct) do
|
|
59
59
|
extend T::Sig
|
|
60
60
|
descriptors.each do |name, descriptor|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
61
|
+
opts = {}
|
|
62
|
+
opts[:default] = descriptor.default_value if descriptor.has_default
|
|
63
|
+
opts[:description] = descriptor.description if descriptor.description
|
|
64
|
+
const name, descriptor.type, **opts
|
|
66
65
|
end
|
|
67
66
|
end
|
|
68
67
|
end
|