dspy 0.27.0 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dspy/chain_of_thought.rb +29 -37
- data/lib/dspy/code_act.rb +2 -2
- data/lib/dspy/context.rb +96 -37
- data/lib/dspy/errors.rb +2 -0
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +37 -35
- data/lib/dspy/lm/adapters/gemini_adapter.rb +45 -21
- data/lib/dspy/lm/adapters/openai/schema_converter.rb +70 -40
- data/lib/dspy/lm/adapters/openai_adapter.rb +35 -8
- data/lib/dspy/lm/retry_handler.rb +15 -6
- data/lib/dspy/lm/strategies/gemini_structured_output_strategy.rb +21 -8
- data/lib/dspy/lm.rb +54 -11
- data/lib/dspy/memory/local_embedding_engine.rb +27 -11
- data/lib/dspy/memory/memory_manager.rb +26 -9
- data/lib/dspy/mixins/type_coercion.rb +30 -0
- data/lib/dspy/module.rb +20 -2
- data/lib/dspy/observability/observation_type.rb +65 -0
- data/lib/dspy/observability.rb +7 -0
- data/lib/dspy/predict.rb +22 -36
- data/lib/dspy/re_act.rb +5 -3
- data/lib/dspy/tools/base.rb +57 -85
- data/lib/dspy/tools/github_cli_toolset.rb +437 -0
- data/lib/dspy/tools/toolset.rb +33 -60
- data/lib/dspy/type_system/sorbet_json_schema.rb +263 -0
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +1 -0
- metadata +5 -3
- data/lib/dspy/lm/cache_manager.rb +0 -151
@@ -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
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.
|
4
|
+
version: 0.27.2
|
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-
|
10
|
+
date: 2025-09-16 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
|