dspy 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44671af9a40e3f452983f616a3df0770babbe5d9cde85c18c9ace6fe618fcfe5
4
- data.tar.gz: e3ba107fed63ba58dfa9b3f707d682acc88a6e7a1c8a72a89a46fbb19c82347c
3
+ metadata.gz: cc32a4fe2ec14d442c2a0603b69992528ab81c67c4e61ceed6a6d337278d0220
4
+ data.tar.gz: f901d0336e0a4c912dfb37428bdd0e359b74de35b340eb769edcb5e811e257fe
5
5
  SHA512:
6
- metadata.gz: bda31c209ff68af39500de5b8f562fb173e08a5fc41863cd96b89f4f0e8be62cadbc73fc31fc61567d8d8e2e0afce86e4710028fbdeba8a4e6709811be73c754
7
- data.tar.gz: a4823fcc2341684b7c0b98b00e2c7dbe9847b61d3b3fde69e3cd31767478659e75fd8c432be6de44290a557f30e382836f671e7fe83edc0cab07f037ab2f6e7c
6
+ metadata.gz: db9477a7f7900559bb7c362104d9921d79c12b84f18ccfe0e51006adafd6e478624d20a930184e9e3dfacf9d1c88321ef54b6688f89ddf627fa0ecef3da12c53
7
+ data.tar.gz: 5ef621dfd8a8ef83f2bf0d3eabb32b07ae881636fe4c1826fddc3e544d3b5193448ff96bd28f970046d499dca69d23affa42596d41315e8c3245c4e7db5be0b3
data/README.md CHANGED
@@ -25,6 +25,9 @@ The result? LLM applications that actually scale and don't break when you sneeze
25
25
  - **Basic Optimization** - Simple prompt optimization techniques
26
26
 
27
27
  **Production Features:**
28
+ - **Reliable JSON Extraction** - Automatic strategy selection for OpenAI structured outputs, Anthropic patterns, and fallback modes
29
+ - **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
30
+ - **Performance Caching** - Schema and capability caching for faster repeated operations
28
31
  - **File-based Storage** - Basic optimization result persistence
29
32
  - **Multi-Platform Observability** - OpenTelemetry, New Relic, and Langfuse integration
30
33
  - **Basic Instrumentation** - Event tracking and logging
@@ -78,7 +81,9 @@ end
78
81
 
79
82
  # Configure DSPy with your LLM
80
83
  DSPy.configure do |c|
81
- c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
84
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini',
85
+ api_key: ENV['OPENAI_API_KEY'],
86
+ structured_outputs: true) # Enable OpenAI's native JSON mode
82
87
  end
83
88
 
84
89
  # Create the predictor and run inference
@@ -14,9 +14,10 @@ module DSPy
14
14
 
15
15
  # Chat interface that all adapters must implement
16
16
  # @param messages [Array<Hash>] Array of message hashes with :role and :content
17
+ # @param signature [DSPy::Signature, nil] Optional signature for structured outputs
17
18
  # @param block [Proc] Optional streaming block
18
19
  # @return [DSPy::LM::Response] Normalized response
19
- def chat(messages:, &block)
20
+ def chat(messages:, signature: nil, &block)
20
21
  raise NotImplementedError, "Subclasses must implement #chat method"
21
22
  end
22
23
 
@@ -14,12 +14,17 @@ module DSPy
14
14
  # Creates an adapter instance based on model_id
15
15
  # @param model_id [String] Full model identifier (e.g., "openai/gpt-4")
16
16
  # @param api_key [String] API key for the provider
17
+ # @param options [Hash] Additional adapter-specific options
17
18
  # @return [DSPy::LM::Adapter] Appropriate adapter instance
18
- def create(model_id, api_key:)
19
+ def create(model_id, api_key:, **options)
19
20
  provider, model = parse_model_id(model_id)
20
21
  adapter_class = get_adapter_class(provider)
21
22
 
22
- adapter_class.new(model: model, api_key: api_key)
23
+ # Pass provider-specific options
24
+ adapter_options = { model: model, api_key: api_key }
25
+ adapter_options.merge!(options) if provider == 'openai' # Only OpenAI accepts structured_outputs for now
26
+
27
+ adapter_class.new(**adapter_options)
23
28
  end
24
29
 
25
30
  private
@@ -11,7 +11,7 @@ module DSPy
11
11
  @client = Anthropic::Client.new(api_key: api_key)
12
12
  end
13
13
 
14
- def chat(messages:, &block)
14
+ def chat(messages:, signature: nil, **extra_params, &block)
15
15
  # Anthropic requires system message to be separate from messages
16
16
  system_message, user_messages = extract_system_message(normalize_messages(messages))
17
17
 
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+ require_relative "../../cache_manager"
5
+
6
+ module DSPy
7
+ class LM
8
+ module Adapters
9
+ module OpenAI
10
+ # Converts DSPy signatures to OpenAI structured output format
11
+ class SchemaConverter
12
+ extend T::Sig
13
+
14
+ # Models that support structured outputs as of July 2025
15
+ STRUCTURED_OUTPUT_MODELS = T.let([
16
+ "gpt-4o-mini",
17
+ "gpt-4o-2024-08-06",
18
+ "gpt-4o",
19
+ "gpt-4-turbo",
20
+ "gpt-4-turbo-2024-04-09"
21
+ ].freeze, T::Array[String])
22
+
23
+ sig { params(signature_class: T.class_of(DSPy::Signature), name: T.nilable(String), strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
24
+ def self.to_openai_format(signature_class, name: nil, strict: true)
25
+ # Build cache params from the method parameters
26
+ cache_params = { strict: strict }
27
+ cache_params[:name] = name if name
28
+
29
+ # Check cache first
30
+ cache_manager = DSPy::LM.cache_manager
31
+ cached_schema = cache_manager.get_schema(signature_class, "openai", cache_params)
32
+
33
+ if cached_schema
34
+ DSPy.logger.debug("Using cached schema for #{signature_class.name}")
35
+ return cached_schema
36
+ end
37
+
38
+ # Get the output JSON schema from the signature class
39
+ output_schema = signature_class.output_json_schema
40
+
41
+ # Build the complete schema
42
+ dspy_schema = {
43
+ "$schema": "http://json-schema.org/draft-06/schema#",
44
+ type: "object",
45
+ properties: output_schema[:properties] || {},
46
+ required: output_schema[:required] || []
47
+ }
48
+
49
+ # Generate a schema name if not provided
50
+ schema_name = name || generate_schema_name(signature_class)
51
+
52
+ # Remove the $schema field as OpenAI doesn't use it
53
+ openai_schema = dspy_schema.except(:$schema)
54
+
55
+ # Add additionalProperties: false for strict mode
56
+ if strict
57
+ openai_schema = add_additional_properties_recursively(openai_schema)
58
+ end
59
+
60
+ # Wrap in OpenAI's required format
61
+ result = {
62
+ type: "json_schema",
63
+ json_schema: {
64
+ name: schema_name,
65
+ strict: strict,
66
+ schema: openai_schema
67
+ }
68
+ }
69
+
70
+ # Cache the result with same params
71
+ cache_manager.cache_schema(signature_class, "openai", result, cache_params)
72
+
73
+ result
74
+ end
75
+
76
+ sig { params(model: String).returns(T::Boolean) }
77
+ def self.supports_structured_outputs?(model)
78
+ # Check cache first
79
+ cache_manager = DSPy::LM.cache_manager
80
+ cached_result = cache_manager.get_capability(model, "structured_outputs")
81
+
82
+ if !cached_result.nil?
83
+ DSPy.logger.debug("Using cached capability check for #{model}")
84
+ return cached_result
85
+ end
86
+
87
+ # Extract base model name without provider prefix
88
+ base_model = model.sub(/^openai\//, "")
89
+
90
+ # Check if it's a supported model or a newer version
91
+ result = STRUCTURED_OUTPUT_MODELS.any? { |supported| base_model.start_with?(supported) }
92
+
93
+ # Cache the result
94
+ cache_manager.cache_capability(model, "structured_outputs", result)
95
+
96
+ result
97
+ end
98
+
99
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Array[String]) }
100
+ def self.validate_compatibility(schema)
101
+ issues = []
102
+
103
+ # Check for deeply nested objects (OpenAI has depth limits)
104
+ depth = calculate_depth(schema)
105
+ if depth > 5
106
+ issues << "Schema depth (#{depth}) exceeds recommended limit of 5 levels"
107
+ end
108
+
109
+ # Check for unsupported JSON Schema features
110
+ if contains_pattern_properties?(schema)
111
+ issues << "Pattern properties are not supported in OpenAI structured outputs"
112
+ end
113
+
114
+ if contains_conditional_schemas?(schema)
115
+ issues << "Conditional schemas (if/then/else) are not supported"
116
+ end
117
+
118
+ issues
119
+ end
120
+
121
+ private
122
+
123
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
124
+ def self.add_additional_properties_recursively(schema)
125
+ return schema unless schema.is_a?(Hash)
126
+
127
+ result = schema.dup
128
+
129
+ # Add additionalProperties: false if this is an object
130
+ if result[:type] == "object"
131
+ result[:additionalProperties] = false
132
+ end
133
+
134
+ # Process properties recursively
135
+ if result[:properties].is_a?(Hash)
136
+ result[:properties] = result[:properties].transform_values do |prop|
137
+ if prop.is_a?(Hash)
138
+ processed = add_additional_properties_recursively(prop)
139
+ # Special handling for arrays - ensure their items have additionalProperties if they're objects
140
+ if processed[:type] == "array" && processed[:items].is_a?(Hash)
141
+ processed[:items] = add_additional_properties_recursively(processed[:items])
142
+ end
143
+ processed
144
+ else
145
+ prop
146
+ end
147
+ end
148
+ end
149
+
150
+ # Process array items
151
+ if result[:items].is_a?(Hash)
152
+ processed_items = add_additional_properties_recursively(result[:items])
153
+ # OpenAI requires additionalProperties on all objects, even in array items
154
+ if processed_items.is_a?(Hash) && processed_items[:type] == "object" && !processed_items.key?(:additionalProperties)
155
+ processed_items[:additionalProperties] = false
156
+ end
157
+ result[:items] = processed_items
158
+ elsif result[:items].is_a?(Array)
159
+ # Handle tuple validation
160
+ result[:items] = result[:items].map do |item|
161
+ processed = item.is_a?(Hash) ? add_additional_properties_recursively(item) : item
162
+ if processed.is_a?(Hash) && processed[:type] == "object" && !processed.key?(:additionalProperties)
163
+ processed[:additionalProperties] = false
164
+ end
165
+ processed
166
+ end
167
+ end
168
+
169
+ # Process oneOf/anyOf/allOf
170
+ [:oneOf, :anyOf, :allOf].each do |key|
171
+ if result[key].is_a?(Array)
172
+ result[key] = result[key].map do |sub_schema|
173
+ sub_schema.is_a?(Hash) ? add_additional_properties_recursively(sub_schema) : sub_schema
174
+ end
175
+ end
176
+ end
177
+
178
+ result
179
+ end
180
+
181
+ sig { params(signature_class: T.class_of(DSPy::Signature)).returns(String) }
182
+ def self.generate_schema_name(signature_class)
183
+ # Use the signature class name
184
+ class_name = signature_class.name&.split("::")&.last
185
+ if class_name
186
+ class_name.gsub(/[^a-zA-Z0-9_]/, "_").downcase
187
+ else
188
+ # Fallback to a generic name
189
+ "dspy_output_#{Time.now.to_i}"
190
+ end
191
+ end
192
+
193
+ sig { params(schema: T::Hash[Symbol, T.untyped], current_depth: Integer).returns(Integer) }
194
+ def self.calculate_depth(schema, current_depth = 0)
195
+ return current_depth unless schema.is_a?(Hash)
196
+
197
+ max_depth = current_depth
198
+
199
+ # Check properties
200
+ if schema[:properties].is_a?(Hash)
201
+ schema[:properties].each_value do |prop|
202
+ if prop.is_a?(Hash)
203
+ prop_depth = calculate_depth(prop, current_depth + 1)
204
+ max_depth = [max_depth, prop_depth].max
205
+ end
206
+ end
207
+ end
208
+
209
+ # Check array items
210
+ if schema[:items].is_a?(Hash)
211
+ items_depth = calculate_depth(schema[:items], current_depth + 1)
212
+ max_depth = [max_depth, items_depth].max
213
+ end
214
+
215
+ # Check oneOf/anyOf/allOf
216
+ [:oneOf, :anyOf, :allOf].each do |key|
217
+ if schema[key].is_a?(Array)
218
+ schema[key].each do |sub_schema|
219
+ if sub_schema.is_a?(Hash)
220
+ sub_depth = calculate_depth(sub_schema, current_depth + 1)
221
+ max_depth = [max_depth, sub_depth].max
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ max_depth
228
+ end
229
+
230
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
231
+ def self.contains_pattern_properties?(schema)
232
+ return true if schema[:patternProperties]
233
+
234
+ # Recursively check nested schemas
235
+ [:properties, :items, :oneOf, :anyOf, :allOf].each do |key|
236
+ value = schema[key]
237
+ case value
238
+ when Hash
239
+ return true if contains_pattern_properties?(value)
240
+ when Array
241
+ return true if value.any? { |v| v.is_a?(Hash) && contains_pattern_properties?(v) }
242
+ end
243
+ end
244
+
245
+ false
246
+ end
247
+
248
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
249
+ def self.contains_conditional_schemas?(schema)
250
+ return true if schema[:if] || schema[:then] || schema[:else]
251
+
252
+ # Recursively check nested schemas
253
+ [:properties, :items, :oneOf, :anyOf, :allOf].each do |key|
254
+ value = schema[key]
255
+ case value
256
+ when Hash
257
+ return true if contains_conditional_schemas?(value)
258
+ when Array
259
+ return true if value.any? { |v| v.is_a?(Hash) && contains_conditional_schemas?(v) }
260
+ end
261
+ end
262
+
263
+ false
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -1,23 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openai'
4
+ require_relative 'openai/schema_converter'
4
5
 
5
6
  module DSPy
6
7
  class LM
7
8
  class OpenAIAdapter < Adapter
8
- def initialize(model:, api_key:)
9
- super
9
+ def initialize(model:, api_key:, structured_outputs: false)
10
+ super(model: model, api_key: api_key)
10
11
  validate_api_key!(api_key, 'openai')
11
12
  @client = OpenAI::Client.new(api_key: api_key)
13
+ @structured_outputs_enabled = structured_outputs
12
14
  end
13
15
 
14
- def chat(messages:, &block)
16
+ def chat(messages:, signature: nil, response_format: nil, &block)
15
17
  request_params = {
16
18
  model: model,
17
19
  messages: normalize_messages(messages),
18
20
  temperature: 0.0 # DSPy default for deterministic responses
19
21
  }
20
22
 
23
+ # Add response format if provided by strategy
24
+ if response_format
25
+ request_params[:response_format] = response_format
26
+ elsif @structured_outputs_enabled && signature && supports_structured_outputs?
27
+ # Legacy behavior for backward compatibility
28
+ response_format = DSPy::LM::Adapters::OpenAI::SchemaConverter.to_openai_format(signature)
29
+ request_params[:response_format] = response_format
30
+ end
31
+
21
32
  # Add streaming if block provided
22
33
  if block_given?
23
34
  request_params[:stream] = proc do |chunk, _bytesize|
@@ -32,9 +43,15 @@ module DSPy
32
43
  raise AdapterError, "OpenAI API error: #{response.error}"
33
44
  end
34
45
 
35
- content = response.choices.first.message.content
46
+ message = response.choices.first.message
47
+ content = message.content
36
48
  usage = response.usage
37
49
 
50
+ # Handle structured output refusals
51
+ if message.respond_to?(:refusal) && message.refusal
52
+ raise AdapterError, "OpenAI refused to generate output: #{message.refusal}"
53
+ end
54
+
38
55
  Response.new(
39
56
  content: content,
40
57
  usage: usage.respond_to?(:to_h) ? usage.to_h : usage,
@@ -42,13 +59,20 @@ module DSPy
42
59
  provider: 'openai',
43
60
  model: model,
44
61
  response_id: response.id,
45
- created: response.created
62
+ created: response.created,
63
+ structured_output: @structured_outputs_enabled && signature && supports_structured_outputs?
46
64
  }
47
65
  )
48
66
  rescue => e
49
67
  raise AdapterError, "OpenAI adapter error: #{e.message}"
50
68
  end
51
69
  end
70
+
71
+ private
72
+
73
+ def supports_structured_outputs?
74
+ DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(model)
75
+ end
52
76
  end
53
77
  end
54
78
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+
5
+ module DSPy
6
+ class LM
7
+ # Manages caching for schemas and capability detection
8
+ class CacheManager
9
+ extend T::Sig
10
+
11
+ # Cache entry with TTL
12
+ class CacheEntry < T::Struct
13
+ extend T::Sig
14
+
15
+ const :value, T.untyped
16
+ const :expires_at, Time
17
+
18
+ sig { returns(T::Boolean) }
19
+ def expired?
20
+ Time.now > expires_at
21
+ end
22
+ end
23
+
24
+ DEFAULT_TTL = 3600 # 1 hour
25
+
26
+ sig { void }
27
+ def initialize
28
+ @schema_cache = {}
29
+ @capability_cache = {}
30
+ @mutex = Mutex.new
31
+ end
32
+
33
+ # Cache a schema for a signature class
34
+ sig { params(signature_class: T.class_of(DSPy::Signature), provider: String, schema: T.untyped, cache_params: T::Hash[Symbol, T.untyped]).void }
35
+ def cache_schema(signature_class, provider, schema, cache_params = {})
36
+ key = schema_key(signature_class, provider, cache_params)
37
+
38
+ @mutex.synchronize do
39
+ @schema_cache[key] = CacheEntry.new(
40
+ value: schema,
41
+ expires_at: Time.now + DEFAULT_TTL
42
+ )
43
+ end
44
+
45
+ DSPy.logger.debug("Cached schema for #{signature_class.name} (#{provider})")
46
+ end
47
+
48
+ # Get cached schema if available
49
+ sig { params(signature_class: T.class_of(DSPy::Signature), provider: String, cache_params: T::Hash[Symbol, T.untyped]).returns(T.nilable(T.untyped)) }
50
+ def get_schema(signature_class, provider, cache_params = {})
51
+ key = schema_key(signature_class, provider, cache_params)
52
+
53
+ @mutex.synchronize do
54
+ entry = @schema_cache[key]
55
+
56
+ if entry.nil?
57
+ nil
58
+ elsif entry.expired?
59
+ @schema_cache.delete(key)
60
+ nil
61
+ else
62
+ entry.value
63
+ end
64
+ end
65
+ end
66
+
67
+ # Cache capability detection result
68
+ sig { params(model: String, capability: String, result: T::Boolean).void }
69
+ def cache_capability(model, capability, result)
70
+ key = capability_key(model, capability)
71
+
72
+ @mutex.synchronize do
73
+ @capability_cache[key] = CacheEntry.new(
74
+ value: result,
75
+ expires_at: Time.now + DEFAULT_TTL * 24 # Capabilities change less frequently
76
+ )
77
+ end
78
+
79
+ DSPy.logger.debug("Cached capability #{capability} for #{model}: #{result}")
80
+ end
81
+
82
+ # Get cached capability if available
83
+ sig { params(model: String, capability: String).returns(T.nilable(T::Boolean)) }
84
+ def get_capability(model, capability)
85
+ key = capability_key(model, capability)
86
+
87
+ @mutex.synchronize do
88
+ entry = @capability_cache[key]
89
+
90
+ if entry.nil?
91
+ nil
92
+ elsif entry.expired?
93
+ @capability_cache.delete(key)
94
+ nil
95
+ else
96
+ entry.value
97
+ end
98
+ end
99
+ end
100
+
101
+ # Clear all caches
102
+ sig { void }
103
+ def clear!
104
+ @mutex.synchronize do
105
+ @schema_cache.clear
106
+ @capability_cache.clear
107
+ end
108
+
109
+ DSPy.logger.debug("Cleared all caches")
110
+ end
111
+
112
+ # Get cache statistics
113
+ sig { returns(T::Hash[Symbol, Integer]) }
114
+ def stats
115
+ @mutex.synchronize do
116
+ {
117
+ schema_entries: @schema_cache.size,
118
+ capability_entries: @capability_cache.size,
119
+ total_entries: @schema_cache.size + @capability_cache.size
120
+ }
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ sig { params(signature_class: T.class_of(DSPy::Signature), provider: String, cache_params: T::Hash[Symbol, T.untyped]).returns(String) }
127
+ def schema_key(signature_class, provider, cache_params = {})
128
+ params_str = cache_params.sort.map { |k, v| "#{k}:#{v}" }.join(":")
129
+ base_key = "schema:#{provider}:#{signature_class.name}"
130
+ params_str.empty? ? base_key : "#{base_key}:#{params_str}"
131
+ end
132
+
133
+ sig { params(model: String, capability: String).returns(String) }
134
+ def capability_key(model, capability)
135
+ "capability:#{model}:#{capability}"
136
+ end
137
+ end
138
+
139
+ # Global cache instance
140
+ @cache_manager = T.let(nil, T.nilable(CacheManager))
141
+
142
+ class << self
143
+ extend T::Sig
144
+
145
+ sig { returns(CacheManager) }
146
+ def cache_manager
147
+ @cache_manager ||= CacheManager.new
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+
5
+ module DSPy
6
+ class LM
7
+ # Handles retry logic with progressive fallback strategies
8
+ class RetryHandler
9
+ extend T::Sig
10
+
11
+ MAX_RETRIES = 3
12
+ BACKOFF_BASE = 0.5 # seconds
13
+
14
+ sig { params(adapter: DSPy::LM::Adapter, signature_class: T.class_of(DSPy::Signature)).void }
15
+ def initialize(adapter, signature_class)
16
+ @adapter = adapter
17
+ @signature_class = signature_class
18
+ @attempt = 0
19
+ end
20
+
21
+ # Execute a block with retry logic and progressive fallback
22
+ sig do
23
+ type_parameters(:T)
24
+ .params(
25
+ initial_strategy: Strategies::BaseStrategy,
26
+ block: T.proc.params(strategy: Strategies::BaseStrategy).returns(T.type_parameter(:T))
27
+ )
28
+ .returns(T.type_parameter(:T))
29
+ end
30
+ def with_retry(initial_strategy, &block)
31
+ strategies = build_fallback_chain(initial_strategy)
32
+ last_error = nil
33
+
34
+ strategies.each do |strategy|
35
+ retry_count = 0
36
+
37
+ begin
38
+ @attempt += 1
39
+ DSPy.logger.debug("Attempting with strategy: #{strategy.name} (attempt #{@attempt})")
40
+
41
+ result = yield(strategy)
42
+
43
+ # Success! Reset attempt counter for next time
44
+ @attempt = 0
45
+ return result
46
+
47
+ rescue JSON::ParserError, StandardError => e
48
+ last_error = e
49
+
50
+ # Let strategy handle the error first
51
+ if strategy.handle_error(e)
52
+ DSPy.logger.info("Strategy #{strategy.name} handled error, will try next strategy")
53
+ next # Try next strategy
54
+ end
55
+
56
+ # Try retrying with the same strategy
57
+ if retry_count < max_retries_for_strategy(strategy)
58
+ retry_count += 1
59
+ backoff_time = calculate_backoff(retry_count)
60
+
61
+ DSPy.logger.warn(
62
+ "Retrying #{strategy.name} after error (attempt #{retry_count}/#{max_retries_for_strategy(strategy)}): #{e.message}"
63
+ )
64
+
65
+ sleep(backoff_time) if backoff_time > 0
66
+ retry
67
+ else
68
+ DSPy.logger.info("Max retries reached for #{strategy.name}, trying next strategy")
69
+ next # Try next strategy
70
+ end
71
+ end
72
+ end
73
+
74
+ # All strategies exhausted
75
+ DSPy.logger.error("All strategies exhausted after #{@attempt} total attempts")
76
+ raise last_error || StandardError.new("All JSON extraction strategies failed")
77
+ end
78
+
79
+ private
80
+
81
+ # Build a chain of strategies to try in order
82
+ sig { params(initial_strategy: Strategies::BaseStrategy).returns(T::Array[Strategies::BaseStrategy]) }
83
+ def build_fallback_chain(initial_strategy)
84
+ selector = StrategySelector.new(@adapter, @signature_class)
85
+ all_strategies = selector.available_strategies.sort_by(&:priority).reverse
86
+
87
+ # Start with the requested strategy, then try others
88
+ chain = [initial_strategy]
89
+ chain.concat(all_strategies.reject { |s| s.name == initial_strategy.name })
90
+
91
+ chain
92
+ end
93
+
94
+ # Different strategies get different retry counts
95
+ sig { params(strategy: Strategies::BaseStrategy).returns(Integer) }
96
+ def max_retries_for_strategy(strategy)
97
+ case strategy.name
98
+ when "openai_structured_output"
99
+ 1 # Structured outputs rarely benefit from retries
100
+ when "anthropic_extraction"
101
+ 2 # Anthropic can be a bit more variable
102
+ else
103
+ MAX_RETRIES # Enhanced prompting might need more attempts
104
+ end
105
+ end
106
+
107
+ # Calculate exponential backoff with jitter
108
+ sig { params(attempt: Integer).returns(Float) }
109
+ def calculate_backoff(attempt)
110
+ return 0.0 if DSPy.config.test_mode # No sleep in tests
111
+
112
+ base_delay = BACKOFF_BASE * (2 ** (attempt - 1))
113
+ jitter = rand * 0.1 * base_delay
114
+
115
+ [base_delay + jitter, 10.0].min # Cap at 10 seconds
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+
5
+ module DSPy
6
+ class LM
7
+ module Strategies
8
+ # Strategy for using Anthropic's enhanced JSON extraction patterns
9
+ class AnthropicExtractionStrategy < BaseStrategy
10
+ extend T::Sig
11
+
12
+ sig { override.returns(T::Boolean) }
13
+ def available?
14
+ adapter.is_a?(DSPy::LM::AnthropicAdapter)
15
+ end
16
+
17
+ sig { override.returns(Integer) }
18
+ def priority
19
+ 90 # High priority - Anthropic's extraction is very reliable
20
+ end
21
+
22
+ sig { override.returns(String) }
23
+ def name
24
+ "anthropic_extraction"
25
+ end
26
+
27
+ sig { override.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
28
+ def prepare_request(messages, request_params)
29
+ # Anthropic adapter already handles JSON optimization in prepare_messages_for_json
30
+ # No additional preparation needed here
31
+ end
32
+
33
+ sig { override.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
34
+ def extract_json(response)
35
+ # Use Anthropic's specialized extraction method if available
36
+ if adapter.respond_to?(:extract_json_from_response)
37
+ adapter.extract_json_from_response(response.content)
38
+ else
39
+ # Fallback to basic extraction
40
+ extract_json_fallback(response.content)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ sig { params(content: T.nilable(String)).returns(T.nilable(String)) }
47
+ def extract_json_fallback(content)
48
+ return nil if content.nil?
49
+
50
+ # Try the 4 patterns Anthropic adapter uses
51
+ # Pattern 1: ```json blocks
52
+ if content.include?('```json')
53
+ return content.split('```json').last.split('```').first.strip
54
+ end
55
+
56
+ # Pattern 2: ## Output values header
57
+ if content.include?('## Output values')
58
+ json_part = content.split('## Output values').last
59
+ if json_part.include?('```')
60
+ return json_part.split('```')[1].strip
61
+ end
62
+ end
63
+
64
+ # Pattern 3: Generic code blocks
65
+ if content.include?('```')
66
+ code_block = content.split('```')[1]
67
+ if code_block && (code_block.strip.start_with?('{') || code_block.strip.start_with?('['))
68
+ return code_block.strip
69
+ end
70
+ end
71
+
72
+ # Pattern 4: Already valid JSON
73
+ content.strip if content.strip.start_with?('{') || content.strip.start_with?('[')
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+
5
+ module DSPy
6
+ class LM
7
+ module Strategies
8
+ # Base class for JSON extraction strategies
9
+ class BaseStrategy
10
+ extend T::Sig
11
+ extend T::Helpers
12
+ abstract!
13
+
14
+ sig { params(adapter: DSPy::LM::Adapter, signature_class: T.class_of(DSPy::Signature)).void }
15
+ def initialize(adapter, signature_class)
16
+ @adapter = adapter
17
+ @signature_class = signature_class
18
+ end
19
+
20
+ # Check if this strategy is available for the given adapter/model
21
+ sig { abstract.returns(T::Boolean) }
22
+ def available?; end
23
+
24
+ # Priority for this strategy (higher = preferred)
25
+ sig { abstract.returns(Integer) }
26
+ def priority; end
27
+
28
+ # Name of the strategy for logging/debugging
29
+ sig { abstract.returns(String) }
30
+ def name; end
31
+
32
+ # Prepare the request for JSON extraction
33
+ sig { abstract.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
34
+ def prepare_request(messages, request_params); end
35
+
36
+ # Extract JSON from the response
37
+ sig { abstract.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
38
+ def extract_json(response); end
39
+
40
+ # Handle errors specific to this strategy
41
+ sig { params(error: StandardError).returns(T::Boolean) }
42
+ def handle_error(error)
43
+ # By default, don't handle errors - let them propagate
44
+ false
45
+ end
46
+
47
+ protected
48
+
49
+ attr_reader :adapter, :signature_class
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+
5
+ module DSPy
6
+ class LM
7
+ module Strategies
8
+ # Enhanced prompting strategy that works with any LLM
9
+ # Adds explicit JSON formatting instructions to improve reliability
10
+ class EnhancedPromptingStrategy < BaseStrategy
11
+ extend T::Sig
12
+
13
+ sig { override.returns(T::Boolean) }
14
+ def available?
15
+ # This strategy is always available as a fallback
16
+ true
17
+ end
18
+
19
+ sig { override.returns(Integer) }
20
+ def priority
21
+ 50 # Medium priority - use when native methods aren't available
22
+ end
23
+
24
+ sig { override.returns(String) }
25
+ def name
26
+ "enhanced_prompting"
27
+ end
28
+
29
+ sig { override.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
30
+ def prepare_request(messages, request_params)
31
+ # Enhance the user message with explicit JSON instructions
32
+ return if messages.empty?
33
+
34
+ # Get the output schema
35
+ output_schema = signature_class.output_json_schema
36
+
37
+ # Find the last user message
38
+ last_user_idx = messages.rindex { |msg| msg[:role] == "user" }
39
+ return unless last_user_idx
40
+
41
+ # Add JSON formatting instructions
42
+ original_content = messages[last_user_idx][:content]
43
+ enhanced_content = enhance_prompt_with_json_instructions(original_content, output_schema)
44
+ messages[last_user_idx][:content] = enhanced_content
45
+
46
+ # Add system instructions if no system message exists
47
+ if messages.none? { |msg| msg[:role] == "system" }
48
+ messages.unshift({
49
+ role: "system",
50
+ content: "You are a helpful assistant that always responds with valid JSON when requested."
51
+ })
52
+ end
53
+ end
54
+
55
+ sig { override.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
56
+ def extract_json(response)
57
+ return nil if response.content.nil?
58
+
59
+ content = response.content.strip
60
+
61
+ # Try multiple extraction patterns
62
+ # 1. Check for markdown code blocks
63
+ if content.include?('```json')
64
+ json_content = content.split('```json').last.split('```').first.strip
65
+ return json_content if valid_json?(json_content)
66
+ elsif content.include?('```')
67
+ code_block = content.split('```')[1]
68
+ if code_block
69
+ json_content = code_block.strip
70
+ return json_content if valid_json?(json_content)
71
+ end
72
+ end
73
+
74
+ # 2. Check if the entire response is JSON
75
+ return content if valid_json?(content)
76
+
77
+ # 3. Look for JSON-like structures in the content
78
+ json_match = content.match(/\{[\s\S]*\}|\[[\s\S]*\]/)
79
+ if json_match
80
+ json_content = json_match[0]
81
+ return json_content if valid_json?(json_content)
82
+ end
83
+
84
+ nil
85
+ end
86
+
87
+ private
88
+
89
+ sig { params(prompt: String, schema: T::Hash[Symbol, T.untyped]).returns(String) }
90
+ def enhance_prompt_with_json_instructions(prompt, schema)
91
+ json_example = generate_example_from_schema(schema)
92
+
93
+ <<~ENHANCED
94
+ #{prompt}
95
+
96
+ IMPORTANT: You must respond with valid JSON that matches this structure:
97
+ ```json
98
+ #{JSON.pretty_generate(json_example)}
99
+ ```
100
+
101
+ Required fields: #{schema[:required]&.join(', ') || 'none'}
102
+
103
+ Ensure your response:
104
+ 1. Is valid JSON (properly quoted strings, no trailing commas)
105
+ 2. Includes all required fields
106
+ 3. Uses the correct data types for each field
107
+ 4. Is wrapped in ```json``` markdown code blocks
108
+ ENHANCED
109
+ end
110
+
111
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
112
+ def generate_example_from_schema(schema)
113
+ return {} unless schema[:properties]
114
+
115
+ example = {}
116
+ schema[:properties].each do |field_name, field_schema|
117
+ example[field_name.to_s] = case field_schema[:type]
118
+ when "string"
119
+ field_schema[:description] || "example string"
120
+ when "integer"
121
+ 42
122
+ when "number"
123
+ 3.14
124
+ when "boolean"
125
+ true
126
+ when "array"
127
+ ["example item"]
128
+ when "object"
129
+ { "nested" => "object" }
130
+ else
131
+ "example value"
132
+ end
133
+ end
134
+ example
135
+ end
136
+
137
+ sig { params(content: String).returns(T::Boolean) }
138
+ def valid_json?(content)
139
+ JSON.parse(content)
140
+ true
141
+ rescue JSON::ParserError
142
+ false
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+
5
+ module DSPy
6
+ class LM
7
+ module Strategies
8
+ # Strategy for using OpenAI's native structured output feature
9
+ class OpenAIStructuredOutputStrategy < BaseStrategy
10
+ extend T::Sig
11
+
12
+ sig { override.returns(T::Boolean) }
13
+ def available?
14
+ # Check if adapter is OpenAI and supports structured outputs
15
+ return false unless adapter.is_a?(DSPy::LM::OpenAIAdapter)
16
+ return false unless adapter.instance_variable_get(:@structured_outputs_enabled)
17
+
18
+ DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(adapter.model)
19
+ end
20
+
21
+ sig { override.returns(Integer) }
22
+ def priority
23
+ 100 # Highest priority - native structured outputs are most reliable
24
+ end
25
+
26
+ sig { override.returns(String) }
27
+ def name
28
+ "openai_structured_output"
29
+ end
30
+
31
+ sig { override.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
32
+ def prepare_request(messages, request_params)
33
+ # Add structured output format to request
34
+ response_format = DSPy::LM::Adapters::OpenAI::SchemaConverter.to_openai_format(signature_class)
35
+ request_params[:response_format] = response_format
36
+ end
37
+
38
+ sig { override.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
39
+ def extract_json(response)
40
+ # With structured outputs, the response should already be valid JSON
41
+ # Just return the content as-is
42
+ response.content
43
+ end
44
+
45
+ sig { override.params(error: StandardError).returns(T::Boolean) }
46
+ def handle_error(error)
47
+ # Handle OpenAI-specific structured output errors
48
+ if error.message.include?("response_format") || error.message.include?("Invalid schema")
49
+ # Log the error and return true to indicate we handled it
50
+ # This allows fallback to another strategy
51
+ DSPy.logger.warn("OpenAI structured output failed: #{error.message}")
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+ require_relative "strategies/base_strategy"
5
+ require_relative "strategies/openai_structured_output_strategy"
6
+ require_relative "strategies/anthropic_extraction_strategy"
7
+ require_relative "strategies/enhanced_prompting_strategy"
8
+
9
+ module DSPy
10
+ class LM
11
+ # Selects the best JSON extraction strategy based on the adapter and capabilities
12
+ class StrategySelector
13
+ extend T::Sig
14
+
15
+ # Available strategies in order of registration
16
+ STRATEGIES = [
17
+ Strategies::OpenAIStructuredOutputStrategy,
18
+ Strategies::AnthropicExtractionStrategy,
19
+ Strategies::EnhancedPromptingStrategy
20
+ ].freeze
21
+
22
+ sig { params(adapter: DSPy::LM::Adapter, signature_class: T.class_of(DSPy::Signature)).void }
23
+ def initialize(adapter, signature_class)
24
+ @adapter = adapter
25
+ @signature_class = signature_class
26
+ @strategies = build_strategies
27
+ end
28
+
29
+ # Select the best available strategy
30
+ sig { returns(Strategies::BaseStrategy) }
31
+ def select
32
+ # Allow manual override via configuration
33
+ if DSPy.config.structured_outputs.strategy
34
+ strategy = find_strategy_by_name(DSPy.config.structured_outputs.strategy)
35
+ return strategy if strategy&.available?
36
+
37
+ DSPy.logger.warn("Requested strategy '#{DSPy.config.structured_outputs.strategy}' is not available")
38
+ end
39
+
40
+ # Select the highest priority available strategy
41
+ available_strategies = @strategies.select(&:available?)
42
+
43
+ if available_strategies.empty?
44
+ raise "No JSON extraction strategies available for #{@adapter.class}"
45
+ end
46
+
47
+ selected = available_strategies.max_by(&:priority)
48
+
49
+ DSPy.logger.debug("Selected JSON extraction strategy: #{selected.name}")
50
+ selected
51
+ end
52
+
53
+ # Get all available strategies
54
+ sig { returns(T::Array[Strategies::BaseStrategy]) }
55
+ def available_strategies
56
+ @strategies.select(&:available?)
57
+ end
58
+
59
+ # Check if a specific strategy is available
60
+ sig { params(strategy_name: String).returns(T::Boolean) }
61
+ def strategy_available?(strategy_name)
62
+ strategy = find_strategy_by_name(strategy_name)
63
+ strategy&.available? || false
64
+ end
65
+
66
+ private
67
+
68
+ sig { returns(T::Array[Strategies::BaseStrategy]) }
69
+ def build_strategies
70
+ STRATEGIES.map { |klass| klass.new(@adapter, @signature_class) }
71
+ end
72
+
73
+ sig { params(name: String).returns(T.nilable(Strategies::BaseStrategy)) }
74
+ def find_strategy_by_name(name)
75
+ @strategies.find { |s| s.name == name }
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/dspy/lm.rb CHANGED
@@ -14,19 +14,23 @@ require_relative 'instrumentation/token_tracker'
14
14
  require_relative 'lm/adapters/openai_adapter'
15
15
  require_relative 'lm/adapters/anthropic_adapter'
16
16
 
17
+ # Load strategy system
18
+ require_relative 'lm/strategy_selector'
19
+ require_relative 'lm/retry_handler'
20
+
17
21
  module DSPy
18
22
  class LM
19
23
  attr_reader :model_id, :api_key, :model, :provider, :adapter
20
24
 
21
- def initialize(model_id, api_key: nil)
25
+ def initialize(model_id, api_key: nil, **options)
22
26
  @model_id = model_id
23
27
  @api_key = api_key
24
28
 
25
29
  # Parse provider and model from model_id
26
30
  @provider, @model = parse_model_id(model_id)
27
31
 
28
- # Create appropriate adapter
29
- @adapter = AdapterFactory.create(model_id, api_key: api_key)
32
+ # Create appropriate adapter with options
33
+ @adapter = AdapterFactory.create(model_id, api_key: api_key, **options)
30
34
  end
31
35
 
32
36
  def chat(inference_module, input_values, &block)
@@ -54,7 +58,7 @@ module DSPy
54
58
  adapter_class: adapter.class.name,
55
59
  input_size: input_size
56
60
  }) do
57
- adapter.chat(messages: messages, &block)
61
+ chat_with_strategy(messages, signature_class, &block)
58
62
  end
59
63
 
60
64
  # Extract actual token usage from response (more accurate than estimation)
@@ -79,7 +83,7 @@ module DSPy
79
83
  end
80
84
  else
81
85
  # Consolidated mode: execute without nested instrumentation
82
- response = adapter.chat(messages: messages, &block)
86
+ response = chat_with_strategy(messages, signature_class, &block)
83
87
  token_usage = Instrumentation::TokenTracker.extract_token_usage(response, provider)
84
88
  parsed_result = parse_response(response, input_values, signature_class)
85
89
  end
@@ -89,6 +93,53 @@ module DSPy
89
93
 
90
94
  private
91
95
 
96
+ def chat_with_strategy(messages, signature_class, &block)
97
+ # Select the best strategy for JSON extraction
98
+ strategy_selector = StrategySelector.new(adapter, signature_class)
99
+ initial_strategy = strategy_selector.select
100
+
101
+ if DSPy.config.structured_outputs.retry_enabled && signature_class
102
+ # Use retry handler for JSON responses
103
+ retry_handler = RetryHandler.new(adapter, signature_class)
104
+
105
+ retry_handler.with_retry(initial_strategy) do |strategy|
106
+ execute_chat_with_strategy(messages, signature_class, strategy, &block)
107
+ end
108
+ else
109
+ # No retry logic, just execute once
110
+ execute_chat_with_strategy(messages, signature_class, initial_strategy, &block)
111
+ end
112
+ end
113
+
114
+ def execute_chat_with_strategy(messages, signature_class, strategy, &block)
115
+ # Prepare request with strategy-specific modifications
116
+ request_params = {}
117
+ strategy.prepare_request(messages.dup, request_params)
118
+
119
+ # Make the request
120
+ response = if request_params.any?
121
+ # Pass additional parameters if strategy added them
122
+ adapter.chat(messages: messages, signature: signature_class, **request_params, &block)
123
+ else
124
+ adapter.chat(messages: messages, signature: signature_class, &block)
125
+ end
126
+
127
+ # Let strategy handle JSON extraction if needed
128
+ if signature_class && response.content
129
+ extracted_json = strategy.extract_json(response)
130
+ if extracted_json && extracted_json != response.content
131
+ # Create a new response with extracted JSON
132
+ response = Response.new(
133
+ content: extracted_json,
134
+ usage: response.usage,
135
+ metadata: response.metadata
136
+ )
137
+ end
138
+ end
139
+
140
+ response
141
+ end
142
+
92
143
  # Determines if LM-level events should be emitted using smart consolidation
93
144
  def should_emit_lm_events?
94
145
  # Emit LM events only if we're not in a nested context (smart consolidation)
@@ -139,18 +190,6 @@ module DSPy
139
190
  # Try to parse the response as JSON
140
191
  content = response.content
141
192
 
142
- # Let adapters handle their own extraction logic if available
143
- if adapter && adapter.respond_to?(:extract_json_from_response, true)
144
- content = adapter.send(:extract_json_from_response, content)
145
- else
146
- # Fallback: Extract JSON if it's in a code block (legacy behavior)
147
- if content.include?('```json')
148
- content = content.split('```json').last.split('```').first.strip
149
- elsif content.include?('```')
150
- content = content.split('```').last.split('```').first.strip
151
- end
152
- end
153
-
154
193
  begin
155
194
  json_payload = JSON.parse(content)
156
195
 
@@ -161,7 +200,6 @@ module DSPy
161
200
  # Enhanced error message with debugging information
162
201
  error_details = {
163
202
  original_content: response.content,
164
- extracted_content: content,
165
203
  provider: provider,
166
204
  model: model
167
205
  }
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.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -21,6 +21,19 @@ module DSPy
21
21
  setting :lm
22
22
  setting :logger, default: Dry.Logger(:dspy, formatter: :string)
23
23
 
24
+ # Structured output configuration for LLM providers
25
+ setting :structured_outputs do
26
+ setting :openai, default: false
27
+ setting :anthropic, default: false # Reserved for future use
28
+ setting :strategy, default: nil # Can be 'openai_structured_output', 'anthropic_extraction', 'enhanced_prompting', or nil for auto
29
+ setting :retry_enabled, default: true
30
+ setting :max_retries, default: 3
31
+ setting :fallback_enabled, default: true
32
+ end
33
+
34
+ # Test mode disables sleeps in retry logic
35
+ setting :test_mode, default: false
36
+
24
37
  # Nested instrumentation configuration using proper dry-configurable syntax
25
38
  setting :instrumentation do
26
39
  # Core settings
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 0.12.0
74
+ version: 0.13.0
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: 0.12.0
81
+ version: 0.13.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: anthropic
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -171,9 +171,17 @@ files:
171
171
  - lib/dspy/lm/adapter.rb
172
172
  - lib/dspy/lm/adapter_factory.rb
173
173
  - lib/dspy/lm/adapters/anthropic_adapter.rb
174
+ - lib/dspy/lm/adapters/openai/schema_converter.rb
174
175
  - lib/dspy/lm/adapters/openai_adapter.rb
176
+ - lib/dspy/lm/cache_manager.rb
175
177
  - lib/dspy/lm/errors.rb
176
178
  - lib/dspy/lm/response.rb
179
+ - lib/dspy/lm/retry_handler.rb
180
+ - lib/dspy/lm/strategies/anthropic_extraction_strategy.rb
181
+ - lib/dspy/lm/strategies/base_strategy.rb
182
+ - lib/dspy/lm/strategies/enhanced_prompting_strategy.rb
183
+ - lib/dspy/lm/strategies/openai_structured_output_strategy.rb
184
+ - lib/dspy/lm/strategy_selector.rb
177
185
  - lib/dspy/memory.rb
178
186
  - lib/dspy/memory/embedding_engine.rb
179
187
  - lib/dspy/memory/in_memory_store.rb