dspy 0.34.3 → 1.0.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 +9 -16
- data/lib/dspy/context.rb +53 -20
- data/lib/dspy/document.rb +153 -0
- data/lib/dspy/lm/adapter.rb +23 -0
- data/lib/dspy/lm/errors.rb +7 -2
- data/lib/dspy/lm/json_strategy.rb +87 -124
- data/lib/dspy/lm/message.rb +5 -1
- data/lib/dspy/lm/message_builder.rb +15 -1
- data/lib/dspy/lm/response.rb +1 -1
- data/lib/dspy/lm/usage.rb +31 -6
- data/lib/dspy/lm.rb +81 -8
- data/lib/dspy/mixins/type_coercion.rb +76 -14
- data/lib/dspy/module.rb +133 -6
- data/lib/dspy/predict.rb +1 -1
- data/lib/dspy/prediction.rb +10 -1
- data/lib/dspy/prompt.rb +2 -46
- data/lib/dspy/re_act.rb +159 -34
- data/lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb +13 -3
- data/lib/dspy/ruby_llm/version.rb +1 -1
- data/lib/dspy/ruby_llm.rb +0 -3
- data/lib/dspy/signature.rb +4 -5
- data/lib/dspy/structured_outputs_prompt.rb +1 -1
- data/lib/dspy/support/openai_sdk_warning.rb +32 -0
- data/lib/dspy/utils/serialization.rb +2 -6
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +52 -17
- metadata +9 -2
- data/lib/dspy/ruby_llm/guardrails.rb +0 -24
data/lib/dspy/re_act.rb
CHANGED
|
@@ -47,7 +47,56 @@ module DSPy
|
|
|
47
47
|
include Mixins::StructBuilder
|
|
48
48
|
|
|
49
49
|
# Custom error classes
|
|
50
|
-
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
|
|
51
100
|
class InvalidActionError < StandardError; end
|
|
52
101
|
class TypeMismatchError < StandardError; end
|
|
53
102
|
|
|
@@ -201,19 +250,17 @@ module DSPy
|
|
|
201
250
|
|
|
202
251
|
sig { params(input_struct: T.untyped).returns(T.untyped) }
|
|
203
252
|
def format_input_context(input_struct)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
DSPy::TypeSerializer.serialize(input_struct).to_json
|
|
253
|
+
input_struct
|
|
207
254
|
end
|
|
208
255
|
|
|
209
256
|
sig { params(history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
210
257
|
def format_history(history)
|
|
211
|
-
|
|
258
|
+
history
|
|
212
259
|
end
|
|
213
260
|
|
|
214
261
|
sig { params(observation: T.untyped).returns(T.untyped) }
|
|
215
262
|
def format_observation(observation)
|
|
216
|
-
|
|
263
|
+
observation
|
|
217
264
|
end
|
|
218
265
|
|
|
219
266
|
sig { returns(T::Boolean) }
|
|
@@ -253,11 +300,7 @@ module DSPy
|
|
|
253
300
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
254
301
|
def create_thought_signature(signature_class, data_format)
|
|
255
302
|
action_enum_class = @action_enum_class
|
|
256
|
-
input_context_type =
|
|
257
|
-
signature_class.input_struct_class || String
|
|
258
|
-
else
|
|
259
|
-
String
|
|
260
|
-
end
|
|
303
|
+
input_context_type = signature_class.input_struct_class || String
|
|
261
304
|
|
|
262
305
|
# Get the output field type for the final_answer field
|
|
263
306
|
output_field_name = signature_class.output_struct_class.props.keys.first
|
|
@@ -271,7 +314,7 @@ module DSPy
|
|
|
271
314
|
# Define input fields
|
|
272
315
|
input do
|
|
273
316
|
const :input_context, input_context_type,
|
|
274
|
-
description:
|
|
317
|
+
description: "All original input fields with their typed values"
|
|
275
318
|
const :history, T::Array[HistoryEntry],
|
|
276
319
|
description: "Previous thoughts and actions, including observations from tools."
|
|
277
320
|
const :available_tools, T::Array[AvailableTool],
|
|
@@ -295,11 +338,7 @@ module DSPy
|
|
|
295
338
|
# Creates a dynamic observation signature that includes the original input fields
|
|
296
339
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
297
340
|
def create_observation_signature(signature_class, data_format)
|
|
298
|
-
input_context_type =
|
|
299
|
-
signature_class.input_struct_class || String
|
|
300
|
-
else
|
|
301
|
-
String
|
|
302
|
-
end
|
|
341
|
+
input_context_type = signature_class.input_struct_class || String
|
|
303
342
|
# Create new class that inherits from DSPy::Signature
|
|
304
343
|
Class.new(DSPy::Signature) do
|
|
305
344
|
# Set description
|
|
@@ -308,7 +347,7 @@ module DSPy
|
|
|
308
347
|
# Define input fields
|
|
309
348
|
input do
|
|
310
349
|
const :input_context, input_context_type,
|
|
311
|
-
description:
|
|
350
|
+
description: "All original input fields with their typed values"
|
|
312
351
|
const :history, T::Array[HistoryEntry],
|
|
313
352
|
description: "Previous thoughts, actions, and observations."
|
|
314
353
|
const :observation, T.untyped,
|
|
@@ -392,7 +431,7 @@ module DSPy
|
|
|
392
431
|
if finish_action?(thought_obj.action)
|
|
393
432
|
final_answer = handle_finish_action(
|
|
394
433
|
thought_obj.final_answer, last_observation, iteration,
|
|
395
|
-
thought_obj.thought, thought_obj.action, history
|
|
434
|
+
thought_obj.thought, thought_obj.action, history, expected_output_field_type
|
|
396
435
|
)
|
|
397
436
|
return { should_finish: true, final_answer: final_answer }
|
|
398
437
|
end
|
|
@@ -416,7 +455,7 @@ module DSPy
|
|
|
416
455
|
|
|
417
456
|
# Process observation and decide next step
|
|
418
457
|
observation_decision = process_observation_and_decide_next_step(
|
|
419
|
-
input_struct, history, observation, available_tools_desc, iteration
|
|
458
|
+
input_struct, history, observation, available_tools_desc, iteration, tools_used
|
|
420
459
|
)
|
|
421
460
|
|
|
422
461
|
if observation_decision[:should_finish]
|
|
@@ -463,7 +502,8 @@ module DSPy
|
|
|
463
502
|
if final_answer.nil?
|
|
464
503
|
iterations = reasoning_result[:iterations]
|
|
465
504
|
tools_used = reasoning_result[:tools_used]
|
|
466
|
-
|
|
505
|
+
history = T.cast(reasoning_result[:history], T::Array[HistoryEntry])
|
|
506
|
+
raise build_max_iterations_error(iterations: iterations, tools_used: tools_used, history: history)
|
|
467
507
|
end
|
|
468
508
|
|
|
469
509
|
output_data = input_kwargs.merge({
|
|
@@ -489,6 +529,19 @@ module DSPy
|
|
|
489
529
|
history.reverse.find { |entry| !entry.observation.nil? }&.observation
|
|
490
530
|
end
|
|
491
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
|
+
|
|
492
545
|
# Deserialize final answer to match expected output type
|
|
493
546
|
# Routes to appropriate deserialization based on type classification
|
|
494
547
|
sig { params(final_answer: T.untyped, output_field_type: T.untyped, history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
@@ -532,6 +585,12 @@ module DSPy
|
|
|
532
585
|
return last_tool_observation
|
|
533
586
|
end
|
|
534
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
|
+
|
|
535
594
|
# If final_answer already matches, use it
|
|
536
595
|
return final_answer if type_matches?(final_answer, output_field_type)
|
|
537
596
|
|
|
@@ -572,7 +631,11 @@ module DSPy
|
|
|
572
631
|
def type_matches?(value, type_object)
|
|
573
632
|
case type_object
|
|
574
633
|
when T::Types::TypedArray
|
|
575
|
-
|
|
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) }
|
|
576
639
|
when T::Types::TypedHash
|
|
577
640
|
value.is_a?(Hash)
|
|
578
641
|
when T::Types::Simple
|
|
@@ -622,12 +685,25 @@ module DSPy
|
|
|
622
685
|
'dspy.module' => 'ReAct',
|
|
623
686
|
'react.iteration' => iteration,
|
|
624
687
|
'tool.name' => action_str.downcase,
|
|
688
|
+
'langfuse.observation.input' => serialize_tool_payload(tool_input),
|
|
625
689
|
'tool.input' => tool_input
|
|
626
|
-
) do
|
|
627
|
-
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
|
|
628
696
|
end
|
|
629
697
|
end
|
|
630
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
|
+
|
|
631
707
|
sig { params(step: Integer, thought: String, action: String, tool_input: ToolInput, observation: T.untyped).returns(HistoryEntry) }
|
|
632
708
|
def create_history_entry(step, thought, action, tool_input, observation)
|
|
633
709
|
HistoryEntry.new(
|
|
@@ -639,8 +715,8 @@ module DSPy
|
|
|
639
715
|
)
|
|
640
716
|
end
|
|
641
717
|
|
|
642
|
-
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]) }
|
|
643
|
-
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)
|
|
644
720
|
observation_result = @observation_processor.forward(
|
|
645
721
|
input_context: format_input_context(input_struct),
|
|
646
722
|
history: format_history(history),
|
|
@@ -650,14 +726,14 @@ module DSPy
|
|
|
650
726
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
|
651
727
|
|
|
652
728
|
final_answer = generate_forced_final_answer(
|
|
653
|
-
input_struct, history, available_tools_desc, observation_result, iteration
|
|
729
|
+
input_struct, history, available_tools_desc, observation_result, iteration, tools_used
|
|
654
730
|
)
|
|
655
731
|
|
|
656
732
|
{ should_finish: true, final_answer: final_answer }
|
|
657
733
|
end
|
|
658
734
|
|
|
659
|
-
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) }
|
|
660
|
-
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)
|
|
661
737
|
final_thought = @thought_generator.forward(
|
|
662
738
|
input_context: format_input_context(input_struct),
|
|
663
739
|
history: format_history(history),
|
|
@@ -665,6 +741,8 @@ module DSPy
|
|
|
665
741
|
)
|
|
666
742
|
|
|
667
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
|
+
|
|
668
746
|
if action_str.downcase != FINISH_ACTION
|
|
669
747
|
# Use interpretation if available, otherwise use last observation
|
|
670
748
|
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
|
@@ -674,9 +752,23 @@ module DSPy
|
|
|
674
752
|
else
|
|
675
753
|
raise MaxIterationsError, "Observation processor indicated finish but no answer is available"
|
|
676
754
|
end
|
|
677
|
-
|
|
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
|
|
678
770
|
else
|
|
679
|
-
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)
|
|
680
772
|
end
|
|
681
773
|
end
|
|
682
774
|
|
|
@@ -738,13 +830,14 @@ module DSPy
|
|
|
738
830
|
example
|
|
739
831
|
end
|
|
740
832
|
|
|
741
|
-
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) }
|
|
742
|
-
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)
|
|
743
835
|
final_answer = final_answer_value
|
|
744
836
|
|
|
745
837
|
# If final_answer is empty/nil but we have a last observation, use it
|
|
746
838
|
if (final_answer.nil? || (final_answer.is_a?(String) && final_answer.empty?)) && last_observation
|
|
747
|
-
|
|
839
|
+
fallback_value = coerce_candidate_for_output(last_observation, output_field_type)
|
|
840
|
+
final_answer = fallback_value unless fallback_value.nil?
|
|
748
841
|
end
|
|
749
842
|
|
|
750
843
|
# Convert action enum to string for storage in history
|
|
@@ -761,5 +854,37 @@ module DSPy
|
|
|
761
854
|
|
|
762
855
|
final_answer
|
|
763
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
|
|
764
889
|
end
|
|
765
890
|
end
|
|
@@ -5,9 +5,6 @@ require 'ruby_llm'
|
|
|
5
5
|
require 'dspy/lm/adapter'
|
|
6
6
|
require 'dspy/lm/vision_models'
|
|
7
7
|
|
|
8
|
-
require 'dspy/ruby_llm/guardrails'
|
|
9
|
-
DSPy::RubyLLM::Guardrails.ensure_ruby_llm_installed!
|
|
10
|
-
|
|
11
8
|
module DSPy
|
|
12
9
|
module RubyLLM
|
|
13
10
|
module LM
|
|
@@ -49,6 +46,8 @@ module DSPy
|
|
|
49
46
|
def chat(messages:, signature: nil, &block)
|
|
50
47
|
normalized_messages = normalize_messages(messages)
|
|
51
48
|
|
|
49
|
+
validate_document_support!(normalized_messages)
|
|
50
|
+
|
|
52
51
|
# Validate vision support if images are present
|
|
53
52
|
if contains_images?(normalized_messages)
|
|
54
53
|
validate_vision_support!
|
|
@@ -255,6 +254,9 @@ module DSPy
|
|
|
255
254
|
elsif item[:image_url]
|
|
256
255
|
attachments << item[:image_url][:url]
|
|
257
256
|
end
|
|
257
|
+
when 'document'
|
|
258
|
+
document = item[:document]
|
|
259
|
+
attachments << document.to_ruby_llm_attachment if document
|
|
258
260
|
end
|
|
259
261
|
end
|
|
260
262
|
content = text_parts.join("\n")
|
|
@@ -263,6 +265,14 @@ module DSPy
|
|
|
263
265
|
[content.to_s, attachments]
|
|
264
266
|
end
|
|
265
267
|
|
|
268
|
+
def validate_document_support!(messages)
|
|
269
|
+
return unless contains_documents?(messages)
|
|
270
|
+
return if provider == 'anthropic'
|
|
271
|
+
|
|
272
|
+
raise DSPy::LM::IncompatibleDocumentFeatureError,
|
|
273
|
+
"RubyLLM document inputs are currently supported only when the underlying provider is Anthropic."
|
|
274
|
+
end
|
|
275
|
+
|
|
266
276
|
def map_response(ruby_llm_response)
|
|
267
277
|
DSPy::LM::Response.new(
|
|
268
278
|
content: ruby_llm_response.content.to_s,
|
data/lib/dspy/ruby_llm.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
|
|
@@ -80,7 +80,7 @@ module DSPy
|
|
|
80
80
|
|
|
81
81
|
sections << "## Input Values"
|
|
82
82
|
sections << "```json"
|
|
83
|
-
sections << JSON.pretty_generate(
|
|
83
|
+
sections << JSON.pretty_generate(DSPy::Utils::Serialization.deep_serialize(input_values))
|
|
84
84
|
sections << "```"
|
|
85
85
|
|
|
86
86
|
sections.join("\n")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DSPy
|
|
4
|
+
module Support
|
|
5
|
+
module OpenAISDKWarning
|
|
6
|
+
WARNING_MESSAGE = <<~WARNING.freeze
|
|
7
|
+
WARNING: ruby-openai gem detected. This may cause conflicts with DSPy's OpenAI integration.
|
|
8
|
+
|
|
9
|
+
DSPy uses the official 'openai' gem. The community 'ruby-openai' gem uses the same
|
|
10
|
+
OpenAI namespace and will cause conflicts.
|
|
11
|
+
|
|
12
|
+
To fix this, remove 'ruby-openai' from your Gemfile and use the official gem instead:
|
|
13
|
+
- Remove: gem 'ruby-openai'
|
|
14
|
+
- Keep: gem 'openai' (official SDK that DSPy uses)
|
|
15
|
+
|
|
16
|
+
The official gem provides better compatibility and is actively maintained by OpenAI.
|
|
17
|
+
WARNING
|
|
18
|
+
|
|
19
|
+
def self.warn_if_community_gem_loaded!
|
|
20
|
+
return if @warned
|
|
21
|
+
return unless community_gem_loaded?
|
|
22
|
+
|
|
23
|
+
Kernel.warn WARNING_MESSAGE
|
|
24
|
+
@warned = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.community_gem_loaded?
|
|
28
|
+
defined?(::OpenAI) && defined?(::OpenAI::Client) && !defined?(::OpenAI::Internal)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -13,6 +13,8 @@ module DSPy
|
|
|
13
13
|
when T::Struct
|
|
14
14
|
# Use the serialize method to convert to a plain hash
|
|
15
15
|
deep_serialize(obj.serialize)
|
|
16
|
+
when T::Enum
|
|
17
|
+
obj.serialize
|
|
16
18
|
when Hash
|
|
17
19
|
# Recursively serialize hash values
|
|
18
20
|
obj.transform_values { |v| deep_serialize(v) }
|
|
@@ -24,12 +26,6 @@ module DSPy
|
|
|
24
26
|
obj
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
|
-
|
|
28
|
-
# Serializes an object to JSON with proper T::Struct handling
|
|
29
|
-
sig { params(obj: T.untyped).returns(String) }
|
|
30
|
-
def self.to_json(obj)
|
|
31
|
-
deep_serialize(obj).to_json
|
|
32
|
-
end
|
|
33
29
|
end
|
|
34
30
|
end
|
|
35
31
|
end
|
data/lib/dspy/version.rb
CHANGED
data/lib/dspy.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require_relative 'dspy/support/warning_filters'
|
|
3
|
+
require_relative 'dspy/support/openai_sdk_warning'
|
|
3
4
|
require 'sorbet-runtime'
|
|
4
5
|
require 'dry-configurable'
|
|
5
6
|
require 'dry/logger'
|
|
@@ -159,13 +160,61 @@ module DSPy
|
|
|
159
160
|
if value.is_a?(Hash)
|
|
160
161
|
flatten_attributes(value, new_key, result)
|
|
161
162
|
else
|
|
162
|
-
result[new_key] = value
|
|
163
|
+
result[new_key] = sanitize_event_attribute_value(value)
|
|
163
164
|
end
|
|
164
165
|
end
|
|
165
166
|
|
|
166
167
|
result
|
|
167
168
|
end
|
|
168
169
|
|
|
170
|
+
def self.sanitize_event_attribute_value(value)
|
|
171
|
+
return value if primitive_event_attribute_value?(value)
|
|
172
|
+
|
|
173
|
+
if value.is_a?(Array)
|
|
174
|
+
return value if homogeneous_primitive_array?(value)
|
|
175
|
+
return JSON.generate(value.map { |item| normalize_event_json_value(item) })
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if value.is_a?(Hash)
|
|
179
|
+
return JSON.generate(normalize_event_json_value(value))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if value.respond_to?(:to_h)
|
|
183
|
+
return JSON.generate(normalize_event_json_value(value.to_h))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
value.respond_to?(:to_json) ? value.to_json : value.to_s
|
|
187
|
+
rescue StandardError
|
|
188
|
+
value.to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.primitive_event_attribute_value?(value)
|
|
192
|
+
value.nil? || value.is_a?(String) || value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.homogeneous_primitive_array?(value)
|
|
196
|
+
return true if value.empty?
|
|
197
|
+
return false unless value.all? { |item| primitive_event_attribute_value?(item) }
|
|
198
|
+
|
|
199
|
+
value.map(&:class).uniq.size == 1
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.normalize_event_json_value(value)
|
|
203
|
+
if primitive_event_attribute_value?(value)
|
|
204
|
+
value
|
|
205
|
+
elsif value.is_a?(Array)
|
|
206
|
+
value.map { |item| normalize_event_json_value(item) }
|
|
207
|
+
elsif value.is_a?(Hash)
|
|
208
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
209
|
+
acc[k.to_s] = normalize_event_json_value(v)
|
|
210
|
+
end
|
|
211
|
+
elsif value.respond_to?(:to_h)
|
|
212
|
+
normalize_event_json_value(value.to_h)
|
|
213
|
+
else
|
|
214
|
+
value.to_s
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
169
218
|
def self.create_logger
|
|
170
219
|
env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
|
171
220
|
log_output = ENV['DSPY_LOG'] # Allow override
|
|
@@ -219,6 +268,7 @@ require_relative 'dspy/prompt'
|
|
|
219
268
|
require_relative 'dspy/example'
|
|
220
269
|
require_relative 'dspy/lm'
|
|
221
270
|
require_relative 'dspy/image'
|
|
271
|
+
require_relative 'dspy/document'
|
|
222
272
|
require_relative 'dspy/prediction'
|
|
223
273
|
require_relative 'dspy/predict'
|
|
224
274
|
require_relative 'dspy/chain_of_thought'
|
|
@@ -261,19 +311,4 @@ DSPy::Observability.configure!
|
|
|
261
311
|
|
|
262
312
|
# LoggerSubscriber will be lazy-initialized when first accessed
|
|
263
313
|
|
|
264
|
-
|
|
265
|
-
# DSPy uses the official openai gem, warn if ruby-openai (community version) is detected
|
|
266
|
-
if defined?(OpenAI) && defined?(OpenAI::Client) && !defined?(OpenAI::Internal)
|
|
267
|
-
warn <<~WARNING
|
|
268
|
-
WARNING: ruby-openai gem detected. This may cause conflicts with DSPy's OpenAI integration.
|
|
269
|
-
|
|
270
|
-
DSPy uses the official 'openai' gem. The community 'ruby-openai' gem uses the same
|
|
271
|
-
OpenAI namespace and will cause conflicts.
|
|
272
|
-
|
|
273
|
-
To fix this, remove 'ruby-openai' from your Gemfile and use the official gem instead:
|
|
274
|
-
- Remove: gem 'ruby-openai'
|
|
275
|
-
- Keep: gem 'openai' (official SDK that DSPy uses)
|
|
276
|
-
|
|
277
|
-
The official gem provides better compatibility and is actively maintained by OpenAI.
|
|
278
|
-
WARNING
|
|
279
|
-
end
|
|
314
|
+
DSPy::Support::OpenAISDKWarning.warn_if_community_gem_loaded!
|
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: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincón de Arellano
|
|
@@ -100,6 +100,9 @@ dependencies:
|
|
|
100
100
|
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
102
|
version: '0.5'
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 0.5.1
|
|
103
106
|
type: :runtime
|
|
104
107
|
prerelease: false
|
|
105
108
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +110,9 @@ dependencies:
|
|
|
107
110
|
- - "~>"
|
|
108
111
|
- !ruby/object:Gem::Version
|
|
109
112
|
version: '0.5'
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 0.5.1
|
|
110
116
|
- !ruby/object:Gem::Dependency
|
|
111
117
|
name: sorbet-toon
|
|
112
118
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -151,6 +157,7 @@ files:
|
|
|
151
157
|
- lib/dspy/callbacks.rb
|
|
152
158
|
- lib/dspy/chain_of_thought.rb
|
|
153
159
|
- lib/dspy/context.rb
|
|
160
|
+
- lib/dspy/document.rb
|
|
154
161
|
- lib/dspy/error_formatter.rb
|
|
155
162
|
- lib/dspy/errors.rb
|
|
156
163
|
- lib/dspy/evals.rb
|
|
@@ -188,7 +195,6 @@ files:
|
|
|
188
195
|
- lib/dspy/registry/registry_manager.rb
|
|
189
196
|
- lib/dspy/registry/signature_registry.rb
|
|
190
197
|
- lib/dspy/ruby_llm.rb
|
|
191
|
-
- lib/dspy/ruby_llm/guardrails.rb
|
|
192
198
|
- lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb
|
|
193
199
|
- lib/dspy/ruby_llm/version.rb
|
|
194
200
|
- lib/dspy/schema.rb
|
|
@@ -204,6 +210,7 @@ files:
|
|
|
204
210
|
- lib/dspy/storage/program_storage.rb
|
|
205
211
|
- lib/dspy/storage/storage_manager.rb
|
|
206
212
|
- lib/dspy/structured_outputs_prompt.rb
|
|
213
|
+
- lib/dspy/support/openai_sdk_warning.rb
|
|
207
214
|
- lib/dspy/support/warning_filters.rb
|
|
208
215
|
- lib/dspy/teleprompt/bootstrap_strategy.rb
|
|
209
216
|
- lib/dspy/teleprompt/data_handler.rb
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'dspy/lm/errors'
|
|
4
|
-
|
|
5
|
-
module DSPy
|
|
6
|
-
module RubyLLM
|
|
7
|
-
class Guardrails
|
|
8
|
-
SUPPORTED_RUBY_LLM_VERSIONS = "~> 1.3".freeze
|
|
9
|
-
|
|
10
|
-
def self.ensure_ruby_llm_installed!
|
|
11
|
-
require 'ruby_llm'
|
|
12
|
-
|
|
13
|
-
spec = Gem.loaded_specs["ruby_llm"]
|
|
14
|
-
unless spec && Gem::Requirement.new(SUPPORTED_RUBY_LLM_VERSIONS).satisfied_by?(spec.version)
|
|
15
|
-
msg = <<~MSG
|
|
16
|
-
DSPy requires the `ruby_llm` gem #{SUPPORTED_RUBY_LLM_VERSIONS}.
|
|
17
|
-
Please install or upgrade it with `bundle add ruby_llm --version "#{SUPPORTED_RUBY_LLM_VERSIONS}"`.
|
|
18
|
-
MSG
|
|
19
|
-
raise DSPy::LM::UnsupportedVersionError, msg
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|