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
data/lib/dspy/observability.rb
CHANGED
@@ -15,6 +15,13 @@ module DSPy
|
|
15
15
|
public_key = ENV['LANGFUSE_PUBLIC_KEY']
|
16
16
|
secret_key = ENV['LANGFUSE_SECRET_KEY']
|
17
17
|
|
18
|
+
# Skip OTLP configuration in test environment UNLESS Langfuse credentials are explicitly provided
|
19
|
+
# This allows observability tests to run while protecting general tests from network calls
|
20
|
+
if (ENV['RACK_ENV'] == 'test' || ENV['RAILS_ENV'] == 'test' || defined?(RSpec)) && !(public_key && secret_key)
|
21
|
+
DSPy.log('observability.disabled', reason: 'Test environment detected - OTLP disabled')
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
18
25
|
unless public_key && secret_key
|
19
26
|
return
|
20
27
|
end
|
data/lib/dspy/predict.rb
CHANGED
@@ -131,46 +131,32 @@ module DSPy
|
|
131
131
|
with_prompt(@prompt.add_examples(examples))
|
132
132
|
end
|
133
133
|
|
134
|
-
|
135
|
-
def forward(**kwargs)
|
136
|
-
@last_input_values = kwargs.clone
|
137
|
-
T.cast(forward_untyped(**kwargs), T.type_parameter(:O))
|
138
|
-
end
|
134
|
+
# Remove forward override to let Module#forward handle span creation
|
139
135
|
|
140
136
|
sig { params(input_values: T.untyped).returns(T.untyped) }
|
141
137
|
def forward_untyped(**input_values)
|
142
|
-
#
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
current_lm = lm
|
155
|
-
if current_lm.nil?
|
156
|
-
raise DSPy::ConfigurationError.missing_lm(self.class.name)
|
157
|
-
end
|
158
|
-
|
159
|
-
# Call LM and process response
|
160
|
-
output_attributes = current_lm.chat(self, input_values)
|
161
|
-
processed_output = process_lm_output(output_attributes)
|
162
|
-
|
163
|
-
# Create combined result struct
|
164
|
-
prediction_result = create_prediction_result(input_values, processed_output)
|
165
|
-
|
166
|
-
# Add output to span
|
167
|
-
if span && prediction_result
|
168
|
-
output_hash = prediction_result.respond_to?(:to_h) ? prediction_result.to_h : prediction_result.to_s
|
169
|
-
span.set_attribute('langfuse.observation.output', DSPy::Utils::Serialization.to_json(output_hash))
|
170
|
-
end
|
171
|
-
|
172
|
-
prediction_result
|
138
|
+
# Module#forward handles span creation, we just do the prediction logic
|
139
|
+
|
140
|
+
# Store input values for optimization
|
141
|
+
@last_input_values = input_values.clone
|
142
|
+
|
143
|
+
# Validate input
|
144
|
+
validate_input_struct(input_values)
|
145
|
+
|
146
|
+
# Check if LM is configured
|
147
|
+
current_lm = lm
|
148
|
+
if current_lm.nil?
|
149
|
+
raise DSPy::ConfigurationError.missing_lm(self.class.name)
|
173
150
|
end
|
151
|
+
|
152
|
+
# Call LM and process response
|
153
|
+
output_attributes = current_lm.chat(self, input_values)
|
154
|
+
processed_output = process_lm_output(output_attributes)
|
155
|
+
|
156
|
+
# Create combined result struct
|
157
|
+
prediction_result = create_prediction_result(input_values, processed_output)
|
158
|
+
|
159
|
+
prediction_result
|
174
160
|
end
|
175
161
|
|
176
162
|
private
|
data/lib/dspy/re_act.rb
CHANGED
@@ -241,9 +241,10 @@ module DSPy
|
|
241
241
|
# Executes a single iteration of the ReAct loop
|
242
242
|
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
|
243
243
|
def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
|
244
|
-
# Track each iteration with span
|
244
|
+
# Track each iteration with agent span
|
245
245
|
DSPy::Context.with_span(
|
246
246
|
operation: 'react.iteration',
|
247
|
+
**DSPy::ObservationType::Agent.langfuse_attributes,
|
247
248
|
'dspy.module' => 'ReAct',
|
248
249
|
'react.iteration' => iteration,
|
249
250
|
'react.max_iterations' => @max_iterations,
|
@@ -355,6 +356,7 @@ module DSPy
|
|
355
356
|
if action && @tools[action.downcase]
|
356
357
|
DSPy::Context.with_span(
|
357
358
|
operation: 'react.tool_call',
|
359
|
+
**DSPy::ObservationType::Tool.langfuse_attributes,
|
358
360
|
'dspy.module' => 'ReAct',
|
359
361
|
'react.iteration' => iteration,
|
360
362
|
'tool.name' => action.downcase,
|
@@ -419,7 +421,7 @@ module DSPy
|
|
419
421
|
|
420
422
|
sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation: String, tools_used: T::Array[String]).void }
|
421
423
|
def emit_iteration_complete_event(iteration, thought, action, action_input, observation, tools_used)
|
422
|
-
DSPy.
|
424
|
+
DSPy.event('react.iteration_complete', {
|
423
425
|
'react.iteration' => iteration,
|
424
426
|
'react.thought' => thought,
|
425
427
|
'react.action' => action,
|
@@ -432,7 +434,7 @@ module DSPy
|
|
432
434
|
sig { params(iterations_count: Integer, final_answer: T.nilable(String), tools_used: T::Array[String], history: T::Array[HistoryEntry]).void }
|
433
435
|
def handle_max_iterations_if_needed(iterations_count, final_answer, tools_used, history)
|
434
436
|
if iterations_count >= @max_iterations && final_answer.nil?
|
435
|
-
DSPy.
|
437
|
+
DSPy.event('react.max_iterations', {
|
436
438
|
'react.iteration_count' => iterations_count,
|
437
439
|
'react.max_iterations' => @max_iterations,
|
438
440
|
'react.tools_used' => tools_used.uniq,
|
data/lib/dspy/tools/base.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
|
@@ -9,6 +11,7 @@ module DSPy
|
|
9
11
|
class Base
|
10
12
|
extend T::Sig
|
11
13
|
extend T::Helpers
|
14
|
+
include DSPy::Mixins::TypeCoercion
|
12
15
|
|
13
16
|
class << self
|
14
17
|
extend T::Sig
|
@@ -30,14 +33,14 @@ module DSPy
|
|
30
33
|
|
31
34
|
# Get the JSON schema for the call method based on its Sorbet signature
|
32
35
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
33
|
-
def
|
36
|
+
def call_schema_object
|
34
37
|
method_obj = instance_method(:call)
|
35
38
|
sig_info = T::Utils.signature_for_method(method_obj)
|
36
39
|
|
37
40
|
if sig_info.nil?
|
38
41
|
# Fallback for methods without signatures
|
39
42
|
return {
|
40
|
-
type:
|
43
|
+
type: "object",
|
41
44
|
properties: {},
|
42
45
|
required: []
|
43
46
|
}
|
@@ -50,10 +53,8 @@ module DSPy
|
|
50
53
|
sig_info.arg_types.each do |param_name, param_type|
|
51
54
|
next if param_name == :block # Skip block parameters
|
52
55
|
|
53
|
-
|
54
|
-
|
55
|
-
description: "Parameter #{param_name}"
|
56
|
-
}
|
56
|
+
schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(param_type)
|
57
|
+
properties[param_name] = schema.merge({ description: "Parameter #{param_name}" })
|
57
58
|
|
58
59
|
# Check if parameter is required (not nilable)
|
59
60
|
unless param_type.class.name.include?('Union') && param_type.name.include?('NilClass')
|
@@ -65,10 +66,8 @@ module DSPy
|
|
65
66
|
sig_info.kwarg_types.each do |param_name, param_type|
|
66
67
|
next if param_name == :block # Skip block parameters
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
description: "Parameter #{param_name}"
|
71
|
-
}
|
69
|
+
schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(param_type)
|
70
|
+
properties[param_name] = schema.merge({ description: "Parameter #{param_name}" })
|
72
71
|
|
73
72
|
# Check if parameter is required by looking at required kwarg names
|
74
73
|
if sig_info.req_kwarg_names.include?(param_name)
|
@@ -79,54 +78,25 @@ module DSPy
|
|
79
78
|
end
|
80
79
|
|
81
80
|
{
|
82
|
-
type:
|
81
|
+
type: "object",
|
83
82
|
properties: properties,
|
84
83
|
required: required
|
85
84
|
}
|
86
85
|
end
|
87
86
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
{ type: :string }
|
98
|
-
elsif raw_type == Integer
|
99
|
-
{ type: :integer }
|
100
|
-
elsif raw_type == Float
|
101
|
-
{ type: :number }
|
102
|
-
elsif raw_type == Numeric
|
103
|
-
{ type: :number }
|
104
|
-
elsif raw_type == TrueClass || raw_type == FalseClass
|
105
|
-
{ type: :boolean }
|
106
|
-
elsif raw_type == T::Boolean
|
107
|
-
{ type: :boolean }
|
108
|
-
else
|
109
|
-
{ type: :string, description: "#{raw_type} (converted to string)" }
|
110
|
-
end
|
111
|
-
elsif sorbet_type.is_a?(T::Types::Union)
|
112
|
-
# Handle nilable types
|
113
|
-
non_nil_types = sorbet_type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
114
|
-
if non_nil_types.length == 1
|
115
|
-
result = sorbet_type_to_json_schema(non_nil_types.first)
|
116
|
-
result[:description] = "#{result[:description] || ''} (optional)".strip
|
117
|
-
result
|
118
|
-
else
|
119
|
-
{ type: :string, description: "Union type (converted to string)" }
|
120
|
-
end
|
121
|
-
elsif sorbet_type.is_a?(T::Types::TypedArray)
|
122
|
-
{
|
123
|
-
type: :array,
|
124
|
-
items: sorbet_type_to_json_schema(sorbet_type.type)
|
87
|
+
# Get the full tool schema for LLM tools format
|
88
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
89
|
+
def call_schema
|
90
|
+
{
|
91
|
+
type: 'function',
|
92
|
+
function: {
|
93
|
+
name: 'call',
|
94
|
+
description: "Call the #{self.name} tool",
|
95
|
+
parameters: call_schema_object
|
125
96
|
}
|
126
|
-
|
127
|
-
{ type: :string, description: "#{sorbet_type} (converted to string)" }
|
128
|
-
end
|
97
|
+
}
|
129
98
|
end
|
99
|
+
|
130
100
|
end
|
131
101
|
|
132
102
|
# Instance methods that tools can use
|
@@ -143,7 +113,7 @@ module DSPy
|
|
143
113
|
# Get the JSON schema string for the tool, formatted for LLM consumption
|
144
114
|
sig { returns(String) }
|
145
115
|
def schema
|
146
|
-
schema_obj = self.class.
|
116
|
+
schema_obj = self.class.call_schema_object
|
147
117
|
tool_info = {
|
148
118
|
name: name,
|
149
119
|
description: description,
|
@@ -152,11 +122,17 @@ module DSPy
|
|
152
122
|
JSON.generate(tool_info)
|
153
123
|
end
|
154
124
|
|
125
|
+
# Get the full call schema compatible with LLM tools format
|
126
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
127
|
+
def call_schema
|
128
|
+
self.class.call_schema
|
129
|
+
end
|
130
|
+
|
155
131
|
# Dynamic call method for ReAct agent - parses JSON arguments and calls the typed method
|
156
132
|
sig { params(args_json: T.untyped).returns(T.untyped) }
|
157
133
|
def dynamic_call(args_json)
|
158
134
|
# Parse arguments based on the call schema
|
159
|
-
schema = self.class.
|
135
|
+
schema = self.class.call_schema_object
|
160
136
|
|
161
137
|
if schema[:properties].empty?
|
162
138
|
# No parameters - call without arguments
|
@@ -178,12 +154,34 @@ module DSPy
|
|
178
154
|
|
179
155
|
# Convert string keys to symbols and validate types
|
180
156
|
kwargs = {}
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
157
|
+
|
158
|
+
# Get method signature for type information
|
159
|
+
method_obj = self.class.instance_method(:call)
|
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
|
187
185
|
end
|
188
186
|
end
|
189
187
|
|
@@ -195,32 +193,6 @@ module DSPy
|
|
195
193
|
|
196
194
|
# Subclasses must implement their own call method with their own signature
|
197
195
|
|
198
|
-
protected
|
199
|
-
|
200
|
-
# Convert argument to the expected type based on JSON schema
|
201
|
-
sig { params(value: T.untyped, schema: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
202
|
-
def convert_argument_type(value, schema)
|
203
|
-
case schema[:type]
|
204
|
-
when :integer
|
205
|
-
value.is_a?(Integer) ? value : value.to_i
|
206
|
-
when :number
|
207
|
-
# Always convert to Float for :number types to ensure compatibility with strict Float signatures
|
208
|
-
value.to_f
|
209
|
-
when :boolean
|
210
|
-
case value
|
211
|
-
when true, false
|
212
|
-
value
|
213
|
-
when "true", "1", 1
|
214
|
-
true
|
215
|
-
when "false", "0", 0
|
216
|
-
false
|
217
|
-
else
|
218
|
-
!!value
|
219
|
-
end
|
220
|
-
else
|
221
|
-
value.to_s
|
222
|
-
end
|
223
|
-
end
|
224
196
|
end
|
225
197
|
end
|
226
198
|
end
|