dspy 0.30.0 → 0.31.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.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require 'sorbet/toon'
5
+
6
+ require_relative '../lm/errors'
7
+
8
+ module DSPy
9
+ module Schema
10
+ module SorbetToonAdapter
11
+ extend T::Sig
12
+
13
+ module_function
14
+
15
+ sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), values: T::Hash[Symbol, T.untyped]).returns(String) }
16
+ def render_input(signature_class, values)
17
+ Sorbet::Toon.encode(
18
+ values,
19
+ signature: signature_class,
20
+ role: :input
21
+ )
22
+ end
23
+
24
+ sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), values: T::Hash[Symbol, T.untyped]).returns(String) }
25
+ def render_expected_output(signature_class, values)
26
+ Sorbet::Toon.encode(
27
+ values,
28
+ signature: signature_class,
29
+ role: :output
30
+ )
31
+ end
32
+
33
+ sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), toon_string: String).returns(T.untyped) }
34
+ def parse_output(signature_class, toon_string)
35
+ payload = strip_code_fences(toon_string)
36
+
37
+ Sorbet::Toon.decode(
38
+ payload,
39
+ signature: signature_class,
40
+ role: :output
41
+ )
42
+ rescue Sorbet::Toon::DecodeError => e
43
+ log_decode_error(payload, e)
44
+ raise DSPy::LM::AdapterError,
45
+ "Failed to parse TOON response: #{e.message}. Ensure the model replies with a ```toon``` block using the schema described in the system prompt."
46
+ end
47
+
48
+ sig { params(text: T.nilable(String)).returns(String) }
49
+ def strip_code_fences(text)
50
+ return '' if text.nil?
51
+
52
+ match = text.match(/```(?:toon)?\s*(.*?)```/m)
53
+ return match[1].strip if match
54
+
55
+ text.strip
56
+ end
57
+
58
+ sig { params(payload: String, error: StandardError).void }
59
+ def log_decode_error(payload, error)
60
+ logger = DSPy.logger if DSPy.respond_to?(:logger)
61
+ return unless logger.respond_to?(:warn)
62
+
63
+ preview = payload.to_s.lines.first(5).join
64
+ logger.warn(
65
+ event: 'toon.decode_error',
66
+ error: error.message,
67
+ preview: preview,
68
+ length: payload.to_s.length
69
+ )
70
+ end
71
+
72
+ sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), role: Symbol).returns(String) }
73
+ def field_guidance(signature_class, role)
74
+ return '' unless signature_class
75
+
76
+ Sorbet::Toon::SignatureFormatter.describe_signature(signature_class, role)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -17,10 +17,11 @@ module DSPy
17
17
  few_shot_examples: T::Array[T.untyped],
18
18
  signature_class_name: T.nilable(String),
19
19
  schema_format: Symbol,
20
- signature_class: T.nilable(T.class_of(Signature))
20
+ signature_class: T.nilable(T.class_of(Signature)),
21
+ data_format: Symbol
21
22
  ).void
22
23
  end
23
- def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil)
24
+ def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil, data_format: :json)
24
25
  normalized_examples = few_shot_examples.map do |example|
25
26
  case example
26
27
  when FewShotExample
@@ -39,7 +40,8 @@ module DSPy
39
40
  few_shot_examples: normalized_examples,
40
41
  signature_class_name: signature_class_name,
41
42
  schema_format: schema_format,
42
- signature_class: signature_class
43
+ signature_class: signature_class,
44
+ data_format: data_format
43
45
  )
44
46
  end
45
47
 
@@ -17,6 +17,7 @@ module DSPy
17
17
  when Hash
18
18
  value.transform_values { |v| serialize(v) }
19
19
  else
20
+ return serialize(value.serialize) if value.respond_to?(:serialize)
20
21
  value
21
22
  end
22
23
  end
@@ -52,4 +53,4 @@ module DSPy
52
53
  result
53
54
  end
54
55
  end
55
- end
56
+ 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.30.0"
4
+ VERSION = "0.31.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -75,6 +75,10 @@ module DSPy
75
75
  create_event_span(event_name, attributes)
76
76
  end
77
77
 
78
+ attributes = attributes.dup
79
+ module_metadata = DSPy::Context.module_context_attributes
80
+ attributes.merge!(module_metadata) unless module_metadata.empty?
81
+
78
82
  # Perform the actual logging (original DSPy.log behavior)
79
83
  # emit_log(event_name, attributes)
80
84
 
@@ -100,6 +104,8 @@ module DSPy
100
104
  # Merge context automatically (but don't include span_stack)
101
105
  context = Context.current.dup
102
106
  context.delete(:span_stack)
107
+ context.delete(:otel_span_stack)
108
+ context.delete(:module_stack)
103
109
  attributes = context.merge(attributes)
104
110
  attributes[:event] = event_name
105
111
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-10-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-configurable
@@ -67,77 +66,49 @@ dependencies:
67
66
  - !ruby/object:Gem::Version
68
67
  version: '1.3'
69
68
  - !ruby/object:Gem::Dependency
70
- name: openai
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: 0.22.0
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: 0.22.0
83
- - !ruby/object:Gem::Dependency
84
- name: anthropic
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: 1.5.0
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 1.5.0
97
- - !ruby/object:Gem::Dependency
98
- name: gemini-ai
69
+ name: sorbet-runtime
99
70
  requirement: !ruby/object:Gem::Requirement
100
71
  requirements:
101
72
  - - "~>"
102
73
  - !ruby/object:Gem::Version
103
- version: '4.3'
74
+ version: '0.5'
104
75
  type: :runtime
105
76
  prerelease: false
106
77
  version_requirements: !ruby/object:Gem::Requirement
107
78
  requirements:
108
79
  - - "~>"
109
80
  - !ruby/object:Gem::Version
110
- version: '4.3'
81
+ version: '0.5'
111
82
  - !ruby/object:Gem::Dependency
112
- name: sorbet-runtime
83
+ name: sorbet-schema
113
84
  requirement: !ruby/object:Gem::Requirement
114
85
  requirements:
115
86
  - - "~>"
116
87
  - !ruby/object:Gem::Version
117
- version: '0.5'
88
+ version: '0.3'
118
89
  type: :runtime
119
90
  prerelease: false
120
91
  version_requirements: !ruby/object:Gem::Requirement
121
92
  requirements:
122
93
  - - "~>"
123
94
  - !ruby/object:Gem::Version
124
- version: '0.5'
95
+ version: '0.3'
125
96
  - !ruby/object:Gem::Dependency
126
- name: sorbet-schema
97
+ name: sorbet-baml
127
98
  requirement: !ruby/object:Gem::Requirement
128
99
  requirements:
129
100
  - - "~>"
130
101
  - !ruby/object:Gem::Version
131
- version: '0.3'
102
+ version: '0.1'
132
103
  type: :runtime
133
104
  prerelease: false
134
105
  version_requirements: !ruby/object:Gem::Requirement
135
106
  requirements:
136
107
  - - "~>"
137
108
  - !ruby/object:Gem::Version
138
- version: '0.3'
109
+ version: '0.1'
139
110
  - !ruby/object:Gem::Dependency
140
- name: sorbet-baml
111
+ name: sorbet-toon
141
112
  requirement: !ruby/object:Gem::Requirement
142
113
  requirements:
143
114
  - - "~>"
@@ -209,13 +180,6 @@ files:
209
180
  - lib/dspy/lm.rb
210
181
  - lib/dspy/lm/adapter.rb
211
182
  - lib/dspy/lm/adapter_factory.rb
212
- - lib/dspy/lm/adapters/anthropic_adapter.rb
213
- - lib/dspy/lm/adapters/gemini/schema_converter.rb
214
- - lib/dspy/lm/adapters/gemini_adapter.rb
215
- - lib/dspy/lm/adapters/ollama_adapter.rb
216
- - lib/dspy/lm/adapters/openai/schema_converter.rb
217
- - lib/dspy/lm/adapters/openai_adapter.rb
218
- - lib/dspy/lm/adapters/openrouter_adapter.rb
219
183
  - lib/dspy/lm/chat_strategy.rb
220
184
  - lib/dspy/lm/errors.rb
221
185
  - lib/dspy/lm/json_strategy.rb
@@ -248,6 +212,7 @@ files:
248
212
  - lib/dspy/registry/signature_registry.rb
249
213
  - lib/dspy/schema.rb
250
214
  - lib/dspy/schema/sorbet_json_schema.rb
215
+ - lib/dspy/schema/sorbet_toon_adapter.rb
251
216
  - lib/dspy/schema/version.rb
252
217
  - lib/dspy/schema_adapters.rb
253
218
  - lib/dspy/signature.rb
@@ -275,7 +240,6 @@ homepage: https://github.com/vicentereig/dspy.rb
275
240
  licenses:
276
241
  - MIT
277
242
  metadata: {}
278
- post_install_message:
279
243
  rdoc_options: []
280
244
  require_paths:
281
245
  - lib
@@ -290,8 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
290
254
  - !ruby/object:Gem::Version
291
255
  version: '0'
292
256
  requirements: []
293
- rubygems_version: 3.0.3.1
294
- signing_key:
257
+ rubygems_version: 3.6.9
295
258
  specification_version: 4
296
259
  summary: The Ruby framework for programming—rather than prompting—language models.
297
260
  test_files: []
@@ -1,291 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'anthropic'
4
- require_relative '../vision_models'
5
-
6
- module DSPy
7
- class LM
8
- class AnthropicAdapter < Adapter
9
- def initialize(model:, api_key:, structured_outputs: true)
10
- super(model: model, api_key: api_key)
11
- validate_api_key!(api_key, 'anthropic')
12
- @client = Anthropic::Client.new(api_key: api_key)
13
- @structured_outputs_enabled = structured_outputs
14
- end
15
-
16
- def chat(messages:, signature: nil, **extra_params, &block)
17
- normalized_messages = normalize_messages(messages)
18
-
19
- # Validate vision support if images are present
20
- if contains_images?(normalized_messages)
21
- VisionModels.validate_vision_support!('anthropic', model)
22
- # Convert messages to Anthropic format with proper image handling
23
- normalized_messages = format_multimodal_messages(normalized_messages)
24
- end
25
-
26
- # Anthropic requires system message to be separate from messages
27
- system_message, user_messages = extract_system_message(normalized_messages)
28
-
29
- # Check if this is a tool use request
30
- has_tools = extra_params.key?(:tools) && !extra_params[:tools].empty?
31
-
32
- # Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use)
33
- unless has_tools || contains_images?(normalized_messages)
34
- user_messages = prepare_messages_for_json(user_messages, system_message)
35
- end
36
-
37
- request_params = {
38
- model: model,
39
- messages: user_messages,
40
- max_tokens: 4096, # Required for Anthropic
41
- temperature: 0.0 # DSPy default for deterministic responses
42
- }.merge(extra_params)
43
-
44
- # Add system message if present
45
- request_params[:system] = system_message if system_message
46
-
47
- # Add streaming if block provided
48
- if block_given?
49
- request_params[:stream] = true
50
- end
51
-
52
- begin
53
- if block_given?
54
- content = ""
55
- @client.messages.stream(**request_params) do |chunk|
56
- if chunk.respond_to?(:delta) && chunk.delta.respond_to?(:text)
57
- chunk_text = chunk.delta.text
58
- content += chunk_text
59
- block.call(chunk)
60
- end
61
- end
62
-
63
- # Create typed metadata for streaming response
64
- metadata = ResponseMetadataFactory.create('anthropic', {
65
- model: model,
66
- streaming: true
67
- })
68
-
69
- Response.new(
70
- content: content,
71
- usage: nil, # Usage not available in streaming
72
- metadata: metadata
73
- )
74
- else
75
- response = @client.messages.create(**request_params)
76
-
77
- if response.respond_to?(:error) && response.error
78
- raise AdapterError, "Anthropic API error: #{response.error}"
79
- end
80
-
81
- # Handle both text content and tool use
82
- content = ""
83
- tool_calls = []
84
-
85
- if response.content.is_a?(Array)
86
- response.content.each do |content_block|
87
- case content_block.type.to_s
88
- when "text"
89
- content += content_block.text
90
- when "tool_use"
91
- tool_calls << {
92
- id: content_block.id,
93
- name: content_block.name,
94
- input: content_block.input
95
- }
96
- end
97
- end
98
- end
99
-
100
- usage = response.usage
101
-
102
- # Convert usage data to typed struct
103
- usage_struct = UsageFactory.create('anthropic', usage)
104
-
105
- metadata = {
106
- provider: 'anthropic',
107
- model: model,
108
- response_id: response.id,
109
- role: response.role
110
- }
111
-
112
- # Add tool calls to metadata if present
113
- metadata[:tool_calls] = tool_calls unless tool_calls.empty?
114
-
115
- # Create typed metadata
116
- typed_metadata = ResponseMetadataFactory.create('anthropic', metadata)
117
-
118
- Response.new(
119
- content: content,
120
- usage: usage_struct,
121
- metadata: typed_metadata
122
- )
123
- end
124
- rescue => e
125
- # Check for specific image-related errors in the message
126
- error_msg = e.message.to_s
127
-
128
- if error_msg.include?('Could not process image')
129
- raise AdapterError, "Image processing failed: #{error_msg}. Ensure your image is a valid PNG, JPEG, GIF, or WebP format, properly base64-encoded, and under 5MB."
130
- elsif error_msg.include?('image')
131
- raise AdapterError, "Image error: #{error_msg}. Anthropic requires base64-encoded images (URLs are not supported)."
132
- elsif error_msg.include?('rate')
133
- raise AdapterError, "Anthropic rate limit exceeded: #{error_msg}. Please wait and try again."
134
- elsif error_msg.include?('authentication') || error_msg.include?('API key')
135
- raise AdapterError, "Anthropic authentication failed: #{error_msg}. Check your API key."
136
- else
137
- # Generic error handling
138
- raise AdapterError, "Anthropic adapter error: #{e.message}"
139
- end
140
- end
141
- end
142
-
143
- private
144
-
145
- # Enhanced JSON extraction specifically for Claude models
146
- # Handles multiple patterns of markdown-wrapped JSON responses
147
- def extract_json_from_response(content)
148
- return content if content.nil? || content.empty?
149
-
150
- # Pattern 1: ```json blocks
151
- if content.include?('```json')
152
- extracted = content[/```json\s*\n(.*?)\n```/m, 1]
153
- return extracted.strip if extracted
154
- end
155
-
156
- # Pattern 2: ## Output values header
157
- if content.include?('## Output values')
158
- extracted = content.split('## Output values').last
159
- .gsub(/```json\s*\n/, '')
160
- .gsub(/\n```.*/, '')
161
- .strip
162
- return extracted if extracted && !extracted.empty?
163
- end
164
-
165
- # Pattern 3: Generic code blocks (check if it looks like JSON)
166
- if content.include?('```')
167
- extracted = content[/```\s*\n(.*?)\n```/m, 1]
168
- return extracted.strip if extracted && looks_like_json?(extracted)
169
- end
170
-
171
- # Pattern 4: Already valid JSON or fallback
172
- content.strip
173
- end
174
-
175
- # Simple heuristic to check if content looks like JSON
176
- def looks_like_json?(str)
177
- return false if str.nil? || str.empty?
178
- trimmed = str.strip
179
- (trimmed.start_with?('{') && trimmed.end_with?('}')) ||
180
- (trimmed.start_with?('[') && trimmed.end_with?(']'))
181
- end
182
-
183
- # Prepare messages for JSON output by adding prefilling and strong instructions
184
- def prepare_messages_for_json(user_messages, system_message)
185
- return user_messages unless requires_json_output?(user_messages, system_message)
186
- return user_messages unless tends_to_wrap_json?
187
-
188
- # Add strong JSON instruction to the last user message if not already present
189
- enhanced_messages = enhance_json_instructions(user_messages)
190
-
191
- # Only add prefill for models that support it and temporarily disable for testing
192
- if false # supports_prefilling? - temporarily disabled
193
- add_json_prefill(enhanced_messages)
194
- else
195
- enhanced_messages
196
- end
197
- end
198
-
199
- # Detect if the conversation requires JSON output
200
- def requires_json_output?(user_messages, system_message)
201
- # Check for JSON-related keywords in messages
202
- all_content = [system_message] + user_messages.map { |m| m[:content] }
203
- all_content.compact.any? do |content|
204
- content.downcase.include?('json') ||
205
- content.include?('```') ||
206
- content.include?('{') ||
207
- content.include?('output')
208
- end
209
- end
210
-
211
- # Check if this is a Claude model that benefits from prefilling
212
- def supports_prefilling?
213
- # Claude models that work well with JSON prefilling
214
- model.downcase.include?('claude')
215
- end
216
-
217
- # Check if this is a Claude model that tends to wrap JSON in markdown
218
- def tends_to_wrap_json?
219
- # All Claude models have this tendency, especially Opus variants
220
- model.downcase.include?('claude')
221
- end
222
-
223
- # Enhance the last user message with strong JSON instructions
224
- def enhance_json_instructions(user_messages)
225
- return user_messages if user_messages.empty?
226
-
227
- enhanced_messages = user_messages.dup
228
- last_message = enhanced_messages.last
229
-
230
- # Only add instruction if not already present
231
- unless last_message[:content].include?('ONLY valid JSON')
232
- # Use smart default instruction for Claude models
233
- json_instruction = "\n\nIMPORTANT: Respond with ONLY valid JSON. No markdown formatting, no code blocks, no explanations. Start your response with '{' and end with '}'."
234
-
235
- last_message = last_message.dup
236
- last_message[:content] = last_message[:content] + json_instruction
237
- enhanced_messages[-1] = last_message
238
- end
239
-
240
- enhanced_messages
241
- end
242
-
243
- # Add assistant message prefill to guide Claude
244
- def add_json_prefill(user_messages)
245
- user_messages + [{ role: "assistant", content: "{" }]
246
- end
247
-
248
- def extract_system_message(messages)
249
- system_message = nil
250
- user_messages = []
251
-
252
- messages.each do |msg|
253
- if msg[:role] == 'system'
254
- system_message = msg[:content]
255
- else
256
- user_messages << msg
257
- end
258
- end
259
-
260
- [system_message, user_messages]
261
- end
262
-
263
- def format_multimodal_messages(messages)
264
- messages.map do |msg|
265
- if msg[:content].is_a?(Array)
266
- # Convert multimodal content to Anthropic format
267
- formatted_content = msg[:content].map do |item|
268
- case item[:type]
269
- when 'text'
270
- { type: 'text', text: item[:text] }
271
- when 'image'
272
- # Validate image compatibility before formatting
273
- item[:image].validate_for_provider!('anthropic')
274
- item[:image].to_anthropic_format
275
- else
276
- item
277
- end
278
- end
279
-
280
- {
281
- role: msg[:role],
282
- content: formatted_content
283
- }
284
- else
285
- msg
286
- end
287
- end
288
- end
289
- end
290
- end
291
- end