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.
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; end
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
- return input_struct if toon_data_format?
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
- toon_data_format? ? history : serialize_history_for_llm(history)
258
+ history
212
259
  end
213
260
 
214
261
  sig { params(observation: T.untyped).returns(T.untyped) }
215
262
  def format_observation(observation)
216
- toon_data_format? ? observation : serialize_for_llm(observation)
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 = if data_format == :toon
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: data_format == :toon ? "All original input fields with their typed values" : "Serialized representation of all input fields"
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 = if data_format == :toon
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: data_format == :toon ? "All original input fields with their typed values" : "Serialized representation of all input fields"
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
- raise MaxIterationsError, "Agent reached maximum iterations (#{iterations}) without producing a final answer. Tools used: #{tools_used.join(', ')}"
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
- value.is_a?(Array) && (value.empty? || value.first.is_a?(T::Struct))
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
- handle_finish_action(forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history)
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
- final_answer = last_observation
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,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module DSPy
4
4
  module RubyLLM
5
- VERSION = '0.1.0'
5
+ VERSION = '0.1.1'
6
6
  end
7
7
  end
data/lib/dspy/ruby_llm.rb CHANGED
@@ -2,7 +2,4 @@
2
2
 
3
3
  require 'dspy/ruby_llm/version'
4
4
 
5
- require 'dspy/ruby_llm/guardrails'
6
- DSPy::RubyLLM::Guardrails.ensure_ruby_llm_installed!
7
-
8
5
  require 'dspy/ruby_llm/lm/adapters/ruby_llm_adapter'
@@ -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
- if descriptor.has_default
62
- const name, descriptor.type, default: descriptor.default_value
63
- else
64
- const name, descriptor.type
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(serialize_for_json(input_values))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.34.3"
4
+ VERSION = "1.0.0"
5
5
  end
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
- # Detect potential gem conflicts and warn users
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.34.3
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