dspy 0.30.0 → 0.31.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 +4 -4
- data/README.md +68 -37
- data/lib/dspy/callbacks.rb +21 -2
- data/lib/dspy/context.rb +52 -1
- data/lib/dspy/evals.rb +21 -2
- data/lib/dspy/lm/adapter_factory.rb +40 -17
- data/lib/dspy/lm/errors.rb +3 -0
- data/lib/dspy/lm/json_strategy.rb +24 -8
- data/lib/dspy/lm.rb +62 -19
- data/lib/dspy/module.rb +213 -17
- data/lib/dspy/prompt.rb +94 -36
- data/lib/dspy/re_act.rb +50 -17
- data/lib/dspy/schema/sorbet_json_schema.rb +5 -2
- data/lib/dspy/schema/sorbet_toon_adapter.rb +80 -0
- data/lib/dspy/structured_outputs_prompt.rb +5 -3
- data/lib/dspy/type_serializer.rb +2 -1
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +6 -0
- metadata +14 -51
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +0 -291
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +0 -186
- data/lib/dspy/lm/adapters/gemini_adapter.rb +0 -220
- data/lib/dspy/lm/adapters/ollama_adapter.rb +0 -73
- data/lib/dspy/lm/adapters/openai/schema_converter.rb +0 -359
- data/lib/dspy/lm/adapters/openai_adapter.rb +0 -188
- data/lib/dspy/lm/adapters/openrouter_adapter.rb +0 -68
data/lib/dspy/module.rb
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'sorbet-runtime'
|
|
4
4
|
require 'dry-configurable'
|
|
5
|
+
require 'securerandom'
|
|
5
6
|
require_relative 'context'
|
|
6
7
|
require_relative 'callbacks'
|
|
8
|
+
require_relative 'type_serializer'
|
|
9
|
+
require 'json'
|
|
7
10
|
|
|
8
11
|
module DSPy
|
|
9
12
|
class Module
|
|
@@ -12,10 +15,84 @@ module DSPy
|
|
|
12
15
|
include Dry::Configurable
|
|
13
16
|
include DSPy::Callbacks
|
|
14
17
|
|
|
18
|
+
class SubcriptionScope < T::Enum
|
|
19
|
+
enums do
|
|
20
|
+
Descendants = new('descendants')
|
|
21
|
+
SelfOnly = new('self')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
DEFAULT_MODULE_SUBSCRIPTION_SCOPE = SubcriptionScope::Descendants
|
|
26
|
+
|
|
27
|
+
module ForwardOverrideHooks
|
|
28
|
+
def method_added(method_name)
|
|
29
|
+
super
|
|
30
|
+
|
|
31
|
+
return unless method_name == :forward
|
|
32
|
+
return if self == DSPy::Module
|
|
33
|
+
return if @_wrapping_forward
|
|
34
|
+
|
|
35
|
+
@_wrapping_forward = true
|
|
36
|
+
|
|
37
|
+
original = instance_method(:forward)
|
|
38
|
+
define_method(:forward) do |*args, **kwargs, &block|
|
|
39
|
+
instrument_forward_call(args, kwargs) do
|
|
40
|
+
original.bind(self).call(*args, **kwargs, &block)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
ensure
|
|
44
|
+
@_wrapping_forward = false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def inherited(subclass)
|
|
50
|
+
super
|
|
51
|
+
specs_copy = module_subscription_specs.map(&:dup)
|
|
52
|
+
subclass.instance_variable_set(:@module_subscription_specs, specs_copy)
|
|
53
|
+
subclass.extend(ForwardOverrideHooks)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def subscribe(pattern, handler = nil, scope: DEFAULT_MODULE_SUBSCRIPTION_SCOPE, &block)
|
|
57
|
+
scope = normalize_scope(scope)
|
|
58
|
+
raise ArgumentError, 'Provide a handler method or block' if handler.nil? && block.nil?
|
|
59
|
+
|
|
60
|
+
module_subscription_specs << {
|
|
61
|
+
pattern: pattern,
|
|
62
|
+
handler: handler,
|
|
63
|
+
block: block,
|
|
64
|
+
scope: scope
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def module_subscription_specs
|
|
69
|
+
@module_subscription_specs ||= []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def validate_subscription_scope!(scope)
|
|
75
|
+
T.must(scope)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_scope(scope)
|
|
79
|
+
return scope if scope.is_a?(SubcriptionScope)
|
|
80
|
+
|
|
81
|
+
case scope
|
|
82
|
+
when :descendants
|
|
83
|
+
SubcriptionScope::Descendants
|
|
84
|
+
when :self
|
|
85
|
+
SubcriptionScope::SelfOnly
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "Unsupported subscription scope: #{scope.inspect}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
15
92
|
# Per-instance LM configuration
|
|
16
93
|
setting :lm, default: nil
|
|
17
94
|
|
|
18
|
-
#
|
|
95
|
+
# Enable callback hooks for forward method
|
|
19
96
|
create_before_callback :forward
|
|
20
97
|
create_after_callback :forward
|
|
21
98
|
create_around_callback :forward
|
|
@@ -29,23 +106,8 @@ module DSPy
|
|
|
29
106
|
.returns(T.type_parameter(:O))
|
|
30
107
|
end
|
|
31
108
|
def forward(**input_values)
|
|
32
|
-
|
|
33
|
-
observation_type = DSPy::ObservationType.for_module_class(self.class)
|
|
34
|
-
DSPy::Context.with_span(
|
|
35
|
-
operation: "#{self.class.name}.forward",
|
|
36
|
-
**observation_type.langfuse_attributes,
|
|
37
|
-
'langfuse.observation.input' => input_values.to_json,
|
|
38
|
-
'dspy.module' => self.class.name
|
|
39
|
-
) do |span|
|
|
109
|
+
instrument_forward_call([], input_values) do
|
|
40
110
|
result = forward_untyped(**input_values)
|
|
41
|
-
|
|
42
|
-
# Add output to span
|
|
43
|
-
if span && result
|
|
44
|
-
output_json = result.respond_to?(:to_h) ? result.to_h.to_json : result.to_json rescue result.to_s
|
|
45
|
-
span.set_attribute('langfuse.observation.output', output_json)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Cast the result of forward_untyped to the expected output type
|
|
49
111
|
T.cast(result, T.type_parameter(:O))
|
|
50
112
|
end
|
|
51
113
|
end
|
|
@@ -116,5 +178,139 @@ module DSPy
|
|
|
116
178
|
def predictors
|
|
117
179
|
named_predictors.map { |(_, predictor)| predictor }
|
|
118
180
|
end
|
|
181
|
+
|
|
182
|
+
def instrument_forward_call(call_args, call_kwargs)
|
|
183
|
+
ensure_module_subscriptions!
|
|
184
|
+
|
|
185
|
+
DSPy::Context.with_module(self) do
|
|
186
|
+
observation_type = DSPy::ObservationType.for_module_class(self.class)
|
|
187
|
+
span_attributes = observation_type.langfuse_attributes.merge(
|
|
188
|
+
'langfuse.observation.input' => serialize_module_input(call_args, call_kwargs),
|
|
189
|
+
'dspy.module' => self.class.name
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
DSPy::Context.with_span(
|
|
193
|
+
operation: "#{self.class.name}.forward",
|
|
194
|
+
**span_attributes
|
|
195
|
+
) do |span|
|
|
196
|
+
yield.tap do |result|
|
|
197
|
+
if span && result
|
|
198
|
+
span.set_attribute('langfuse.observation.output', serialize_module_output(result))
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def serialize_module_input(call_args, call_kwargs)
|
|
206
|
+
payload = if call_kwargs && !call_kwargs.empty?
|
|
207
|
+
call_kwargs
|
|
208
|
+
elsif call_args && !call_args.empty?
|
|
209
|
+
call_args
|
|
210
|
+
else
|
|
211
|
+
{}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
serialized = DSPy::TypeSerializer.serialize(payload)
|
|
215
|
+
JSON.generate(serialized)
|
|
216
|
+
rescue StandardError
|
|
217
|
+
payload.to_s
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def serialize_module_output(result)
|
|
221
|
+
serialized = DSPy::TypeSerializer.serialize(result)
|
|
222
|
+
JSON.generate(serialized)
|
|
223
|
+
rescue StandardError
|
|
224
|
+
result.to_s
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private :instrument_forward_call, :serialize_module_input, :serialize_module_output
|
|
228
|
+
|
|
229
|
+
sig { returns(String) }
|
|
230
|
+
def module_scope_id
|
|
231
|
+
@module_scope_id ||= SecureRandom.uuid
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
sig { returns(T.nilable(String)) }
|
|
235
|
+
def module_scope_label
|
|
236
|
+
@module_scope_label
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { params(label: T.nilable(String)).void }
|
|
240
|
+
def module_scope_label=(label)
|
|
241
|
+
@module_scope_label = label
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
sig { returns(T::Array[String]) }
|
|
245
|
+
def registered_module_subscriptions
|
|
246
|
+
Array(@module_subscription_ids).dup
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { void }
|
|
250
|
+
def unsubscribe_module_events
|
|
251
|
+
Array(@module_subscription_ids).each { |id| DSPy.events.unsubscribe(id) }
|
|
252
|
+
@module_subscription_ids = []
|
|
253
|
+
@module_subscriptions_registered = false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def ensure_module_subscriptions!
|
|
259
|
+
return if @module_subscriptions_registered
|
|
260
|
+
|
|
261
|
+
specs = self.class.module_subscription_specs
|
|
262
|
+
if specs.empty?
|
|
263
|
+
@module_subscriptions_registered = true
|
|
264
|
+
return
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
@module_subscription_ids ||= []
|
|
268
|
+
specs.each do |spec|
|
|
269
|
+
callback = build_subscription_callback(spec)
|
|
270
|
+
subscription_id = DSPy.events.subscribe(spec[:pattern], &callback)
|
|
271
|
+
@module_subscription_ids << subscription_id
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
@module_subscriptions_registered = true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def build_subscription_callback(spec)
|
|
278
|
+
scope = spec[:scope] || DEFAULT_MODULE_SUBSCRIPTION_SCOPE
|
|
279
|
+
handler = spec[:handler]
|
|
280
|
+
block = spec[:block]
|
|
281
|
+
|
|
282
|
+
proc do |event_name, attributes|
|
|
283
|
+
next unless module_event_within_scope?(attributes, scope)
|
|
284
|
+
|
|
285
|
+
if handler
|
|
286
|
+
send(handler, event_name, attributes)
|
|
287
|
+
else
|
|
288
|
+
instance_exec(event_name, attributes, &block)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def module_event_within_scope?(attributes, scope)
|
|
294
|
+
metadata = extract_module_metadata(attributes)
|
|
295
|
+
return false unless metadata
|
|
296
|
+
|
|
297
|
+
case scope
|
|
298
|
+
when SubcriptionScope::SelfOnly
|
|
299
|
+
metadata[:leaf_id] == module_scope_id
|
|
300
|
+
else
|
|
301
|
+
metadata[:path_ids].include?(module_scope_id)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def extract_module_metadata(attributes)
|
|
306
|
+
path = attributes[:module_path] || attributes['module_path']
|
|
307
|
+
leaf = attributes[:module_leaf] || attributes['module_leaf']
|
|
308
|
+
return nil unless path.is_a?(Array)
|
|
309
|
+
|
|
310
|
+
{
|
|
311
|
+
path_ids: path.map { |entry| entry[:id] || entry['id'] }.compact,
|
|
312
|
+
leaf_id: leaf&.dig(:id) || leaf&.dig('id')
|
|
313
|
+
}
|
|
314
|
+
end
|
|
119
315
|
end
|
|
120
316
|
end
|
data/lib/dspy/prompt.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require 'sorbet-runtime'
|
|
5
|
+
require 'sorbet/toon'
|
|
6
|
+
|
|
4
7
|
require_relative 'few_shot_example'
|
|
8
|
+
require_relative 'schema/sorbet_toon_adapter'
|
|
5
9
|
|
|
6
10
|
module DSPy
|
|
7
11
|
class Prompt
|
|
@@ -33,6 +37,15 @@ module DSPy
|
|
|
33
37
|
DSPy.config.lm&.schema_format || @schema_format || :json
|
|
34
38
|
end
|
|
35
39
|
|
|
40
|
+
sig { returns(Symbol) }
|
|
41
|
+
def data_format
|
|
42
|
+
return @data_format if @data_format && @data_format != :json
|
|
43
|
+
|
|
44
|
+
lm = DSPy.config.lm
|
|
45
|
+
lm_format = lm&.respond_to?(:data_format) ? lm.data_format : nil
|
|
46
|
+
lm_format || @data_format || :json
|
|
47
|
+
end
|
|
48
|
+
|
|
36
49
|
sig { returns(T.nilable(T.class_of(Signature))) }
|
|
37
50
|
attr_reader :signature_class
|
|
38
51
|
|
|
@@ -44,10 +57,11 @@ module DSPy
|
|
|
44
57
|
few_shot_examples: T::Array[FewShotExample],
|
|
45
58
|
signature_class_name: T.nilable(String),
|
|
46
59
|
schema_format: Symbol,
|
|
47
|
-
signature_class: T.nilable(T.class_of(Signature))
|
|
60
|
+
signature_class: T.nilable(T.class_of(Signature)),
|
|
61
|
+
data_format: Symbol
|
|
48
62
|
).void
|
|
49
63
|
end
|
|
50
|
-
def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil)
|
|
64
|
+
def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil, data_format: :json)
|
|
51
65
|
@instruction = instruction
|
|
52
66
|
@few_shot_examples = few_shot_examples.freeze
|
|
53
67
|
@input_schema = input_schema.freeze
|
|
@@ -55,6 +69,7 @@ module DSPy
|
|
|
55
69
|
@signature_class_name = signature_class_name
|
|
56
70
|
@schema_format = schema_format
|
|
57
71
|
@signature_class = signature_class
|
|
72
|
+
@data_format = data_format
|
|
58
73
|
end
|
|
59
74
|
|
|
60
75
|
# Immutable update methods for optimization
|
|
@@ -67,7 +82,8 @@ module DSPy
|
|
|
67
82
|
few_shot_examples: @few_shot_examples,
|
|
68
83
|
signature_class_name: @signature_class_name,
|
|
69
84
|
schema_format: @schema_format,
|
|
70
|
-
signature_class: @signature_class
|
|
85
|
+
signature_class: @signature_class,
|
|
86
|
+
data_format: @data_format
|
|
71
87
|
)
|
|
72
88
|
end
|
|
73
89
|
|
|
@@ -80,7 +96,8 @@ module DSPy
|
|
|
80
96
|
few_shot_examples: new_examples,
|
|
81
97
|
signature_class_name: @signature_class_name,
|
|
82
98
|
schema_format: @schema_format,
|
|
83
|
-
signature_class: @signature_class
|
|
99
|
+
signature_class: @signature_class,
|
|
100
|
+
data_format: @data_format
|
|
84
101
|
)
|
|
85
102
|
end
|
|
86
103
|
|
|
@@ -106,7 +123,13 @@ module DSPy
|
|
|
106
123
|
sections << "```baml"
|
|
107
124
|
sections << render_baml_schema(@output_schema, :output)
|
|
108
125
|
sections << "```"
|
|
109
|
-
|
|
126
|
+
when :toon
|
|
127
|
+
sections << "Your input schema fields (TOON order) are:"
|
|
128
|
+
sections << Sorbet::Toon::SignatureFormatter.describe_signature(@signature_class, :input)
|
|
129
|
+
sections << ""
|
|
130
|
+
sections << "Your output schema fields (TOON order) are:"
|
|
131
|
+
sections << Sorbet::Toon::SignatureFormatter.describe_signature(@signature_class, :output)
|
|
132
|
+
else
|
|
110
133
|
sections << "Your input schema fields are:"
|
|
111
134
|
sections << "```json"
|
|
112
135
|
sections << JSON.pretty_generate(@input_schema)
|
|
@@ -117,11 +140,10 @@ module DSPy
|
|
|
117
140
|
sections << JSON.pretty_generate(@output_schema)
|
|
118
141
|
sections << "```"
|
|
119
142
|
end
|
|
120
|
-
|
|
143
|
+
|
|
121
144
|
sections << ""
|
|
122
145
|
sections << "All interactions will be structured in the following way, with the appropriate values filled in."
|
|
123
|
-
|
|
124
|
-
# Add few-shot examples if present
|
|
146
|
+
|
|
125
147
|
if @few_shot_examples.any?
|
|
126
148
|
sections << ""
|
|
127
149
|
sections << "Here are some examples:"
|
|
@@ -133,36 +155,59 @@ module DSPy
|
|
|
133
155
|
end
|
|
134
156
|
end
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
if data_format == :toon && @signature_class
|
|
159
|
+
sections << "## Input values"
|
|
160
|
+
sections << "```toon"
|
|
161
|
+
sections << "{input_values}"
|
|
162
|
+
sections << "```"
|
|
163
|
+
sections << ""
|
|
164
|
+
sections << "## Output values"
|
|
165
|
+
sections << "Respond exclusively with a ```toon``` block containing only the output fields defined above, in the same order."
|
|
166
|
+
sections << "```toon"
|
|
167
|
+
sections << "{output_values}"
|
|
168
|
+
sections << "```"
|
|
169
|
+
else
|
|
170
|
+
sections << "## Input values"
|
|
171
|
+
sections << "```json"
|
|
172
|
+
sections << "{input_values}"
|
|
173
|
+
sections << "```"
|
|
174
|
+
|
|
175
|
+
sections << "## Output values"
|
|
176
|
+
sections << "Respond exclusively with the output schema fields in the json block below."
|
|
177
|
+
sections << "```json"
|
|
178
|
+
sections << "{output_values}"
|
|
179
|
+
sections << "```"
|
|
180
|
+
end
|
|
181
|
+
|
|
147
182
|
sections << ""
|
|
148
183
|
sections << "In adhering to this structure, your objective is: #{@instruction}"
|
|
149
|
-
|
|
184
|
+
|
|
150
185
|
sections.join("\n")
|
|
151
186
|
end
|
|
152
187
|
|
|
153
188
|
sig { params(input_values: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
154
189
|
def render_user_prompt(input_values)
|
|
155
190
|
sections = []
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
|
|
192
|
+
if data_format == :toon && @signature_class
|
|
193
|
+
toon_payload = DSPy::Schema::SorbetToonAdapter.render_input(@signature_class, input_values)
|
|
194
|
+
|
|
195
|
+
sections << "## Input Values"
|
|
196
|
+
sections << "```toon"
|
|
197
|
+
sections << toon_payload
|
|
198
|
+
sections << "```"
|
|
199
|
+
sections << ""
|
|
200
|
+
sections << "Respond with the corresponding output schema fields encoded as TOON inside a ```toon``` block starting with the heading `## Output values`."
|
|
201
|
+
else
|
|
202
|
+
sections << "## Input Values"
|
|
203
|
+
sections << "```json"
|
|
204
|
+
sections << JSON.pretty_generate(serialize_for_json(input_values))
|
|
205
|
+
sections << "```"
|
|
206
|
+
sections << ""
|
|
207
|
+
sections << "Respond with the corresponding output schema fields wrapped in a ```json ``` block,"
|
|
208
|
+
sections << "starting with the heading `## Output values`."
|
|
209
|
+
end
|
|
210
|
+
|
|
166
211
|
sections.join("\n")
|
|
167
212
|
end
|
|
168
213
|
|
|
@@ -184,7 +229,8 @@ module DSPy
|
|
|
184
229
|
input_schema: @input_schema,
|
|
185
230
|
output_schema: @output_schema,
|
|
186
231
|
signature_class_name: @signature_class_name,
|
|
187
|
-
schema_format: @schema_format
|
|
232
|
+
schema_format: @schema_format,
|
|
233
|
+
data_format: @data_format
|
|
188
234
|
}
|
|
189
235
|
end
|
|
190
236
|
|
|
@@ -198,13 +244,24 @@ module DSPy
|
|
|
198
244
|
output_schema: hash[:output_schema] || {},
|
|
199
245
|
few_shot_examples: examples,
|
|
200
246
|
signature_class_name: hash[:signature_class_name],
|
|
201
|
-
schema_format: hash[:schema_format] || :json
|
|
247
|
+
schema_format: hash[:schema_format] || :json,
|
|
248
|
+
data_format: hash[:data_format] || :json
|
|
202
249
|
)
|
|
203
250
|
end
|
|
204
251
|
|
|
205
252
|
# Create prompt from signature class
|
|
206
|
-
sig
|
|
207
|
-
|
|
253
|
+
sig do
|
|
254
|
+
params(
|
|
255
|
+
signature_class: T.class_of(Signature),
|
|
256
|
+
schema_format: T.nilable(Symbol),
|
|
257
|
+
data_format: T.nilable(Symbol)
|
|
258
|
+
).returns(Prompt)
|
|
259
|
+
end
|
|
260
|
+
def self.from_signature(signature_class, schema_format: nil, data_format: nil)
|
|
261
|
+
lm = DSPy.config.lm
|
|
262
|
+
schema_format ||= lm&.schema_format || :json
|
|
263
|
+
data_format ||= (lm&.respond_to?(:data_format) ? lm.data_format : nil) || :json
|
|
264
|
+
|
|
208
265
|
new(
|
|
209
266
|
instruction: signature_class.description || "Complete this task.",
|
|
210
267
|
input_schema: signature_class.input_json_schema,
|
|
@@ -212,7 +269,8 @@ module DSPy
|
|
|
212
269
|
few_shot_examples: [],
|
|
213
270
|
signature_class_name: signature_class.name,
|
|
214
271
|
schema_format: schema_format,
|
|
215
|
-
signature_class: signature_class
|
|
272
|
+
signature_class: signature_class,
|
|
273
|
+
data_format: data_format
|
|
216
274
|
)
|
|
217
275
|
end
|
|
218
276
|
|
|
@@ -336,4 +394,4 @@ module DSPy
|
|
|
336
394
|
result
|
|
337
395
|
end
|
|
338
396
|
end
|
|
339
|
-
end
|
|
397
|
+
end
|
data/lib/dspy/re_act.rb
CHANGED
|
@@ -98,13 +98,14 @@ module DSPy
|
|
|
98
98
|
@tools = T.let({}, T::Hash[String, T.untyped])
|
|
99
99
|
tools.each { |tool| @tools[tool.name.downcase] = tool }
|
|
100
100
|
@max_iterations = max_iterations
|
|
101
|
+
@data_format = T.let(DSPy.config.lm&.data_format || :json, Symbol)
|
|
101
102
|
|
|
102
103
|
# Create dynamic ActionEnum class with tool names + finish
|
|
103
104
|
@action_enum_class = create_action_enum_class
|
|
104
105
|
|
|
105
106
|
# Create dynamic signature classes that include the original input fields
|
|
106
|
-
thought_signature = create_thought_signature(signature_class)
|
|
107
|
-
observation_signature = create_observation_signature(signature_class)
|
|
107
|
+
thought_signature = create_thought_signature(signature_class, @data_format)
|
|
108
|
+
observation_signature = create_observation_signature(signature_class, @data_format)
|
|
108
109
|
|
|
109
110
|
# Create thought generator using Predict to preserve field descriptions
|
|
110
111
|
@thought_generator = T.let(DSPy::Predict.new(thought_signature), DSPy::Predict)
|
|
@@ -216,6 +217,28 @@ module DSPy
|
|
|
216
217
|
end
|
|
217
218
|
end
|
|
218
219
|
|
|
220
|
+
sig { params(input_struct: T.untyped).returns(T.untyped) }
|
|
221
|
+
def format_input_context(input_struct)
|
|
222
|
+
return input_struct if toon_data_format?
|
|
223
|
+
|
|
224
|
+
DSPy::TypeSerializer.serialize(input_struct).to_json
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
sig { params(history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
228
|
+
def format_history(history)
|
|
229
|
+
toon_data_format? ? history : serialize_history_for_llm(history)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
sig { params(observation: T.untyped).returns(T.untyped) }
|
|
233
|
+
def format_observation(observation)
|
|
234
|
+
toon_data_format? ? observation : serialize_for_llm(observation)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sig { returns(T::Boolean) }
|
|
238
|
+
def toon_data_format?
|
|
239
|
+
@data_format == :toon
|
|
240
|
+
end
|
|
241
|
+
|
|
219
242
|
# Creates a dynamic ActionEnum class with tool names and "finish"
|
|
220
243
|
sig { returns(T.class_of(T::Enum)) }
|
|
221
244
|
def create_action_enum_class
|
|
@@ -241,9 +264,14 @@ module DSPy
|
|
|
241
264
|
end
|
|
242
265
|
|
|
243
266
|
# Creates a dynamic Thought signature that includes the original input fields
|
|
244
|
-
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
|
|
245
|
-
def create_thought_signature(signature_class)
|
|
267
|
+
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
268
|
+
def create_thought_signature(signature_class, data_format)
|
|
246
269
|
action_enum_class = @action_enum_class
|
|
270
|
+
input_context_type = if data_format == :toon
|
|
271
|
+
signature_class.input_struct_class || String
|
|
272
|
+
else
|
|
273
|
+
String
|
|
274
|
+
end
|
|
247
275
|
# Create new class that inherits from DSPy::Signature
|
|
248
276
|
Class.new(DSPy::Signature) do
|
|
249
277
|
# Set description
|
|
@@ -251,8 +279,8 @@ module DSPy
|
|
|
251
279
|
|
|
252
280
|
# Define input fields
|
|
253
281
|
input do
|
|
254
|
-
const :input_context,
|
|
255
|
-
description: "Serialized representation of all input fields"
|
|
282
|
+
const :input_context, input_context_type,
|
|
283
|
+
description: data_format == :toon ? "All original input fields with their typed values" : "Serialized representation of all input fields"
|
|
256
284
|
const :history, T::Array[HistoryEntry],
|
|
257
285
|
description: "Previous thoughts and actions, including observations from tools."
|
|
258
286
|
const :available_tools, T::Array[AvailableTool],
|
|
@@ -272,8 +300,13 @@ module DSPy
|
|
|
272
300
|
end
|
|
273
301
|
|
|
274
302
|
# Creates a dynamic observation signature that includes the original input fields
|
|
275
|
-
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
|
|
276
|
-
def create_observation_signature(signature_class)
|
|
303
|
+
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
304
|
+
def create_observation_signature(signature_class, data_format)
|
|
305
|
+
input_context_type = if data_format == :toon
|
|
306
|
+
signature_class.input_struct_class || String
|
|
307
|
+
else
|
|
308
|
+
String
|
|
309
|
+
end
|
|
277
310
|
# Create new class that inherits from DSPy::Signature
|
|
278
311
|
Class.new(DSPy::Signature) do
|
|
279
312
|
# Set description
|
|
@@ -281,8 +314,8 @@ module DSPy
|
|
|
281
314
|
|
|
282
315
|
# Define input fields
|
|
283
316
|
input do
|
|
284
|
-
const :input_context,
|
|
285
|
-
description: "Serialized representation of all input fields"
|
|
317
|
+
const :input_context, input_context_type,
|
|
318
|
+
description: data_format == :toon ? "All original input fields with their typed values" : "Serialized representation of all input fields"
|
|
286
319
|
const :history, T::Array[HistoryEntry],
|
|
287
320
|
description: "Previous thoughts, actions, and observations."
|
|
288
321
|
const :observation, T.untyped,
|
|
@@ -358,8 +391,8 @@ module DSPy
|
|
|
358
391
|
) do
|
|
359
392
|
# Generate thought and action
|
|
360
393
|
thought_obj = @thought_generator.forward(
|
|
361
|
-
input_context:
|
|
362
|
-
history:
|
|
394
|
+
input_context: format_input_context(input_struct),
|
|
395
|
+
history: format_history(history),
|
|
363
396
|
available_tools: available_tools_desc
|
|
364
397
|
)
|
|
365
398
|
|
|
@@ -617,9 +650,9 @@ module DSPy
|
|
|
617
650
|
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: T.untyped, available_tools_desc: T::Array[AvailableTool], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
|
|
618
651
|
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
|
619
652
|
observation_result = @observation_processor.forward(
|
|
620
|
-
input_context:
|
|
621
|
-
history:
|
|
622
|
-
observation:
|
|
653
|
+
input_context: format_input_context(input_struct),
|
|
654
|
+
history: format_history(history),
|
|
655
|
+
observation: format_observation(observation)
|
|
623
656
|
)
|
|
624
657
|
|
|
625
658
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
|
@@ -634,8 +667,8 @@ module DSPy
|
|
|
634
667
|
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer).returns(T.untyped) }
|
|
635
668
|
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
|
636
669
|
final_thought = @thought_generator.forward(
|
|
637
|
-
input_context:
|
|
638
|
-
history:
|
|
670
|
+
input_context: format_input_context(input_struct),
|
|
671
|
+
history: format_history(history),
|
|
639
672
|
available_tools: available_tools_desc
|
|
640
673
|
)
|
|
641
674
|
|
|
@@ -254,10 +254,13 @@ module DSPy
|
|
|
254
254
|
"DSPy uses _type for automatic type detection in union types."
|
|
255
255
|
end
|
|
256
256
|
|
|
257
|
+
struct_name = struct_class.name || "Struct#{format('%x', struct_class.object_id)}"
|
|
258
|
+
simple_name = struct_name.split('::').last || struct_name
|
|
259
|
+
|
|
257
260
|
# Add automatic _type field for type detection
|
|
258
261
|
properties[:_type] = {
|
|
259
262
|
type: "string",
|
|
260
|
-
const:
|
|
263
|
+
const: simple_name # Use the simple class name
|
|
261
264
|
}
|
|
262
265
|
required << "_type"
|
|
263
266
|
|
|
@@ -280,7 +283,7 @@ module DSPy
|
|
|
280
283
|
type: "object",
|
|
281
284
|
properties: properties,
|
|
282
285
|
required: required,
|
|
283
|
-
description: "#{
|
|
286
|
+
description: "#{struct_name} struct"
|
|
284
287
|
}
|
|
285
288
|
end
|
|
286
289
|
|