dspy 0.27.1 → 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 +87 -34
- 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
data/lib/dspy/tools/toolset.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'sorbet-runtime'
|
4
4
|
require 'json'
|
5
|
+
require_relative '../type_system/sorbet_json_schema'
|
6
|
+
require_relative '../mixins/type_coercion'
|
5
7
|
|
6
8
|
module DSPy
|
7
9
|
module Tools
|
@@ -69,10 +71,8 @@ module DSPy
|
|
69
71
|
sig_info.kwarg_types.each do |param_name, param_type|
|
70
72
|
next if param_name == :block
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
description: "Parameter #{param_name}"
|
75
|
-
}
|
74
|
+
schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(param_type)
|
75
|
+
properties[param_name] = schema.merge({ description: "Parameter #{param_name}" })
|
76
76
|
|
77
77
|
# Check if parameter is required
|
78
78
|
if sig_info.req_kwarg_names.include?(param_name)
|
@@ -89,61 +89,12 @@ module DSPy
|
|
89
89
|
}
|
90
90
|
end
|
91
91
|
|
92
|
-
private
|
93
|
-
|
94
|
-
# Convert Sorbet types to JSON Schema types (extracted from Base)
|
95
|
-
sig { params(sorbet_type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
96
|
-
def sorbet_type_to_json_schema(sorbet_type)
|
97
|
-
# Check for boolean types first (SimplePairUnion of TrueClass | FalseClass)
|
98
|
-
if sorbet_type.respond_to?(:types) && sorbet_type.types.length == 2
|
99
|
-
raw_types = sorbet_type.types.map do |t|
|
100
|
-
t.is_a?(T::Types::Simple) ? t.raw_type : t
|
101
|
-
end
|
102
|
-
|
103
|
-
if raw_types.include?(TrueClass) && raw_types.include?(FalseClass)
|
104
|
-
return { type: :boolean }
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
if sorbet_type.is_a?(T::Types::Simple)
|
109
|
-
raw_type = sorbet_type.raw_type
|
110
|
-
|
111
|
-
case raw_type
|
112
|
-
when String
|
113
|
-
{ type: :string }
|
114
|
-
when Integer
|
115
|
-
{ type: :integer }
|
116
|
-
when Float, Numeric
|
117
|
-
{ type: :number }
|
118
|
-
when TrueClass, FalseClass, T::Boolean
|
119
|
-
{ type: :boolean }
|
120
|
-
else
|
121
|
-
{ type: :string, description: "#{raw_type} (converted to string)" }
|
122
|
-
end
|
123
|
-
elsif sorbet_type.is_a?(T::Types::Union)
|
124
|
-
# Handle nilable types
|
125
|
-
non_nil_types = sorbet_type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
126
|
-
if non_nil_types.length == 1
|
127
|
-
result = sorbet_type_to_json_schema(non_nil_types.first)
|
128
|
-
result[:description] = "#{result[:description] || ''} (optional)".strip
|
129
|
-
result
|
130
|
-
else
|
131
|
-
{ type: :string, description: "Union type (converted to string)" }
|
132
|
-
end
|
133
|
-
elsif sorbet_type.is_a?(T::Types::TypedArray)
|
134
|
-
{
|
135
|
-
type: :array,
|
136
|
-
items: sorbet_type_to_json_schema(sorbet_type.type)
|
137
|
-
}
|
138
|
-
else
|
139
|
-
{ type: :string, description: "#{sorbet_type} (converted to string)" }
|
140
|
-
end
|
141
|
-
end
|
142
92
|
end
|
143
93
|
|
144
94
|
# Inner class that wraps a method as a tool, compatible with DSPy::Tools::Base interface
|
145
95
|
class ToolProxy < Base
|
146
96
|
extend T::Sig
|
97
|
+
include DSPy::Mixins::TypeCoercion
|
147
98
|
|
148
99
|
sig { params(instance: Toolset, method_name: Symbol, tool_name: String, description: String).void }
|
149
100
|
def initialize(instance, method_name, tool_name, description)
|
@@ -203,12 +154,34 @@ module DSPy
|
|
203
154
|
|
204
155
|
# Convert string keys to symbols and validate types
|
205
156
|
kwargs = {}
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
157
|
+
|
158
|
+
# Get method signature for type information
|
159
|
+
method_obj = @instance.class.instance_method(@method_name)
|
160
|
+
sig_info = T::Utils.signature_for_method(method_obj)
|
161
|
+
|
162
|
+
if sig_info
|
163
|
+
# Handle kwargs using type signature information
|
164
|
+
sig_info.kwarg_types.each do |param_name, param_type|
|
165
|
+
next if param_name == :block
|
166
|
+
|
167
|
+
key = param_name.to_s
|
168
|
+
if args.key?(key)
|
169
|
+
kwargs[param_name] = coerce_value_to_type(args[key], param_type)
|
170
|
+
elsif schema[:required].include?(key)
|
171
|
+
return "Error: Missing required parameter: #{key}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Handle positional args if any
|
176
|
+
sig_info.arg_types.each do |param_name, param_type|
|
177
|
+
next if param_name == :block
|
178
|
+
|
179
|
+
key = param_name.to_s
|
180
|
+
if args.key?(key)
|
181
|
+
kwargs[param_name] = coerce_value_to_type(args[key], param_type)
|
182
|
+
elsif schema[:required].include?(key)
|
183
|
+
return "Error: Missing required parameter: #{key}"
|
184
|
+
end
|
212
185
|
end
|
213
186
|
end
|
214
187
|
|
@@ -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
|