dspy 0.27.1 → 0.27.3

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,263 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module DSPy
7
+ module TypeSystem
8
+ # Unified module for converting Sorbet types to JSON Schema
9
+ # Extracted from Signature class to ensure consistency across Tools, Toolsets, and Signatures
10
+ module SorbetJsonSchema
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ # Convert a Sorbet type to JSON Schema format
15
+ sig { params(type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
16
+ def self.type_to_json_schema(type)
17
+ # Handle T::Boolean type alias first
18
+ if type == T::Boolean
19
+ return { type: "boolean" }
20
+ end
21
+
22
+ # Handle type aliases by resolving to their underlying type
23
+ if type.is_a?(T::Private::Types::TypeAlias)
24
+ return self.type_to_json_schema(type.aliased_type)
25
+ end
26
+
27
+ # Handle raw class types first
28
+ if type.is_a?(Class)
29
+ if type < T::Enum
30
+ # Get all enum values
31
+ values = type.values.map(&:serialize)
32
+ { type: "string", enum: values }
33
+ elsif type == String
34
+ { type: "string" }
35
+ elsif type == Integer
36
+ { type: "integer" }
37
+ elsif type == Float
38
+ { type: "number" }
39
+ elsif type == Numeric
40
+ { type: "number" }
41
+ elsif [TrueClass, FalseClass].include?(type)
42
+ { type: "boolean" }
43
+ elsif type < T::Struct
44
+ # Handle custom T::Struct classes by generating nested object schema
45
+ self.generate_struct_schema(type)
46
+ else
47
+ { type: "string" } # Default fallback
48
+ end
49
+ elsif type.is_a?(T::Types::Simple)
50
+ case type.raw_type.to_s
51
+ when "String"
52
+ { type: "string" }
53
+ when "Integer"
54
+ { type: "integer" }
55
+ when "Float"
56
+ { type: "number" }
57
+ when "Numeric"
58
+ { type: "number" }
59
+ when "TrueClass", "FalseClass"
60
+ { type: "boolean" }
61
+ when "T::Boolean"
62
+ { type: "boolean" }
63
+ else
64
+ # Check if it's an enum
65
+ if type.raw_type < T::Enum
66
+ # Get all enum values
67
+ values = type.raw_type.values.map(&:serialize)
68
+ { type: "string", enum: values }
69
+ elsif type.raw_type < T::Struct
70
+ # Handle custom T::Struct classes
71
+ generate_struct_schema(type.raw_type)
72
+ else
73
+ { type: "string" } # Default fallback
74
+ end
75
+ end
76
+ elsif type.is_a?(T::Types::TypedArray)
77
+ # Handle arrays properly with nested item type
78
+ {
79
+ type: "array",
80
+ items: self.type_to_json_schema(type.type)
81
+ }
82
+ elsif type.is_a?(T::Types::TypedHash)
83
+ # Handle hashes as objects with additionalProperties
84
+ # TypedHash has keys and values methods to access its key and value types
85
+ key_schema = self.type_to_json_schema(type.keys)
86
+ value_schema = self.type_to_json_schema(type.values)
87
+
88
+ # Create a more descriptive schema for nested structures
89
+ {
90
+ type: "object",
91
+ propertyNames: key_schema, # Describe key constraints
92
+ additionalProperties: value_schema,
93
+ # Add a more explicit description of the expected structure
94
+ description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
95
+ }
96
+ elsif type.is_a?(T::Types::FixedHash)
97
+ # Handle fixed hashes (from type aliases like { "key" => Type })
98
+ properties = {}
99
+ required = []
100
+
101
+ type.types.each do |key, value_type|
102
+ properties[key] = self.type_to_json_schema(value_type)
103
+ required << key
104
+ end
105
+
106
+ {
107
+ type: "object",
108
+ properties: properties,
109
+ required: required,
110
+ additionalProperties: false
111
+ }
112
+ elsif type.class.name == "T::Private::Types::SimplePairUnion"
113
+ # Handle T.nilable types (T::Private::Types::SimplePairUnion)
114
+ # This is the actual implementation of T.nilable(SomeType)
115
+ has_nil = type.respond_to?(:types) && type.types.any? do |t|
116
+ (t.respond_to?(:raw_type) && t.raw_type == NilClass) ||
117
+ (t.respond_to?(:name) && t.name == "NilClass")
118
+ end
119
+
120
+ if has_nil
121
+ # Find the non-nil type
122
+ non_nil_type = type.types.find do |t|
123
+ !(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
124
+ !(t.respond_to?(:name) && t.name == "NilClass")
125
+ end
126
+
127
+ if non_nil_type
128
+ base_schema = self.type_to_json_schema(non_nil_type)
129
+ if base_schema[:type].is_a?(String)
130
+ # Convert single type to array with null
131
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
132
+ else
133
+ # For complex schemas, use anyOf to allow null
134
+ { anyOf: [base_schema, { type: "null" }] }
135
+ end
136
+ else
137
+ { type: "string" } # Fallback
138
+ end
139
+ else
140
+ # Not nilable SimplePairUnion - this is a regular T.any() union
141
+ # Generate oneOf schema for all types
142
+ if type.respond_to?(:types) && type.types.length > 1
143
+ {
144
+ oneOf: type.types.map { |t| self.type_to_json_schema(t) },
145
+ description: "Union of multiple types"
146
+ }
147
+ else
148
+ # Single type or fallback
149
+ first_type = type.respond_to?(:types) ? type.types.first : type
150
+ self.type_to_json_schema(first_type)
151
+ end
152
+ end
153
+ elsif type.is_a?(T::Types::Union)
154
+ # Check if this is a nilable type (contains NilClass)
155
+ is_nilable = type.types.any? { |t| t == T::Utils.coerce(NilClass) }
156
+ non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
157
+
158
+ # Special case: check if we have TrueClass + FalseClass (T.nilable(T::Boolean))
159
+ if non_nil_types.size == 2 && is_nilable
160
+ true_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == TrueClass }
161
+ false_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == FalseClass }
162
+
163
+ if true_class_type && false_class_type
164
+ # This is T.nilable(T::Boolean) - treat as nilable boolean
165
+ return { type: ["boolean", "null"] }
166
+ end
167
+ end
168
+
169
+ if non_nil_types.size == 1 && is_nilable
170
+ # This is T.nilable(SomeType) - generate proper schema with null allowed
171
+ base_schema = self.type_to_json_schema(non_nil_types.first)
172
+ if base_schema[:type].is_a?(String)
173
+ # Convert single type to array with null
174
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
175
+ else
176
+ # For complex schemas, use anyOf to allow null
177
+ { anyOf: [base_schema, { type: "null" }] }
178
+ end
179
+ elsif non_nil_types.size == 1
180
+ # Non-nilable single type union (shouldn't happen in practice)
181
+ self.type_to_json_schema(non_nil_types.first)
182
+ elsif non_nil_types.size > 1
183
+ # Handle complex unions with oneOf for better JSON schema compliance
184
+ base_schema = {
185
+ oneOf: non_nil_types.map { |t| self.type_to_json_schema(t) },
186
+ description: "Union of multiple types"
187
+ }
188
+ if is_nilable
189
+ # Add null as an option for complex nilable unions
190
+ base_schema[:oneOf] << { type: "null" }
191
+ end
192
+ base_schema
193
+ else
194
+ { type: "string" } # Fallback for complex unions
195
+ end
196
+ elsif type.is_a?(T::Types::ClassOf)
197
+ # Handle T.class_of() types
198
+ {
199
+ type: "string",
200
+ description: "Class name (T.class_of type)"
201
+ }
202
+ else
203
+ { type: "string" } # Default fallback
204
+ end
205
+ end
206
+
207
+ # Generate JSON schema for custom T::Struct classes
208
+ sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[Symbol, T.untyped]) }
209
+ def self.generate_struct_schema(struct_class)
210
+ return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
211
+
212
+ properties = {}
213
+ required = []
214
+
215
+ # Check if struct already has a _type field
216
+ if struct_class.props.key?(:_type)
217
+ raise DSPy::ValidationError, "_type field conflict: #{struct_class.name} already has a _type field defined. " \
218
+ "DSPy uses _type for automatic type detection in union types."
219
+ end
220
+
221
+ # Add automatic _type field for type detection
222
+ properties[:_type] = {
223
+ type: "string",
224
+ const: struct_class.name.split('::').last # Use the simple class name
225
+ }
226
+ required << "_type"
227
+
228
+ struct_class.props.each do |prop_name, prop_info|
229
+ prop_type = prop_info[:type_object] || prop_info[:type]
230
+ properties[prop_name] = self.type_to_json_schema(prop_type)
231
+
232
+ # A field is required if it's not fully optional
233
+ # fully_optional is true for nilable prop fields
234
+ # immutable const fields are required unless nilable
235
+ unless prop_info[:fully_optional]
236
+ required << prop_name.to_s
237
+ end
238
+ end
239
+
240
+ {
241
+ type: "object",
242
+ properties: properties,
243
+ required: required,
244
+ description: "#{struct_class.name} struct"
245
+ }
246
+ end
247
+
248
+ private
249
+
250
+ # Extensions to Hash for Rails-like except method if not available
251
+ # This ensures compatibility with the original code
252
+ unless Hash.method_defined?(:except)
253
+ Hash.class_eval do
254
+ def except(*keys)
255
+ dup.tap do |hash|
256
+ keys.each { |key| hash.delete(key) }
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ 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.27.1"
4
+ VERSION = "0.27.3"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'dspy/version'
8
8
  require_relative 'dspy/errors'
9
9
  require_relative 'dspy/type_serializer'
10
10
  require_relative 'dspy/observability'
11
+ require_relative 'dspy/observability/observation_type'
11
12
  require_relative 'dspy/context'
12
13
  require_relative 'dspy/events'
13
14
  require_relative 'dspy/events/types'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.1
4
+ version: 0.27.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-14 00:00:00.000000000 Z
10
+ date: 2025-09-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -212,7 +212,6 @@ files:
212
212
  - lib/dspy/lm/adapters/ollama_adapter.rb
213
213
  - lib/dspy/lm/adapters/openai/schema_converter.rb
214
214
  - lib/dspy/lm/adapters/openai_adapter.rb
215
- - lib/dspy/lm/cache_manager.rb
216
215
  - lib/dspy/lm/errors.rb
217
216
  - lib/dspy/lm/message.rb
218
217
  - lib/dspy/lm/message_builder.rb
@@ -241,6 +240,7 @@ files:
241
240
  - lib/dspy/module.rb
242
241
  - lib/dspy/observability.rb
243
242
  - lib/dspy/observability/async_span_processor.rb
243
+ - lib/dspy/observability/observation_type.rb
244
244
  - lib/dspy/optimizers/gaussian_process.rb
245
245
  - lib/dspy/predict.rb
246
246
  - lib/dspy/prediction.rb
@@ -262,10 +262,12 @@ files:
262
262
  - lib/dspy/teleprompt/utils.rb
263
263
  - lib/dspy/tools.rb
264
264
  - lib/dspy/tools/base.rb
265
+ - lib/dspy/tools/github_cli_toolset.rb
265
266
  - lib/dspy/tools/memory_toolset.rb
266
267
  - lib/dspy/tools/text_processing_toolset.rb
267
268
  - lib/dspy/tools/toolset.rb
268
269
  - lib/dspy/type_serializer.rb
270
+ - lib/dspy/type_system/sorbet_json_schema.rb
269
271
  - lib/dspy/utils/serialization.rb
270
272
  - lib/dspy/version.rb
271
273
  homepage: https://github.com/vicentereig/dspy.rb
@@ -1,151 +0,0 @@
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