dspy 0.34.3 → 0.34.4
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 +8 -16
- data/lib/dspy/context.rb +53 -20
- data/lib/dspy/lm/json_strategy.rb +28 -102
- data/lib/dspy/lm/response.rb +1 -1
- data/lib/dspy/lm/usage.rb +31 -6
- data/lib/dspy/lm.rb +13 -2
- data/lib/dspy/mixins/type_coercion.rb +76 -14
- data/lib/dspy/module.rb +133 -6
- data/lib/dspy/predict.rb +1 -1
- data/lib/dspy/prediction.rb +10 -1
- data/lib/dspy/prompt.rb +2 -46
- data/lib/dspy/re_act.rb +159 -34
- data/lib/dspy/signature.rb +4 -5
- data/lib/dspy/structured_outputs_prompt.rb +1 -1
- data/lib/dspy/utils/serialization.rb +2 -6
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +49 -1
- metadata +7 -1
data/lib/dspy/module.rb
CHANGED
|
@@ -261,21 +261,64 @@ module DSPy
|
|
|
261
261
|
def instrument_forward_call(call_args, call_kwargs)
|
|
262
262
|
ensure_module_subscriptions!
|
|
263
263
|
|
|
264
|
+
input_json = serialize_module_input(call_args, call_kwargs)
|
|
265
|
+
root_call = DSPy::Context.current[:span_stack].empty?
|
|
266
|
+
|
|
264
267
|
DSPy::Context.with_module(self) do
|
|
265
268
|
observation_type = DSPy::ObservationType.for_module_class(self.class)
|
|
266
269
|
span_attributes = observation_type.langfuse_attributes.merge(
|
|
267
|
-
'langfuse.observation.input' =>
|
|
270
|
+
'langfuse.observation.input' => input_json,
|
|
268
271
|
'dspy.module' => self.class.name
|
|
269
272
|
)
|
|
273
|
+
operation_name = "#{self.class.name}.forward"
|
|
274
|
+
span_attributes.merge!(root_trace_attributes(call_args, call_kwargs, input_json)) if root_call
|
|
275
|
+
|
|
276
|
+
if self.class.name == 'DSPy::Predict' && respond_to?(:signature_class)
|
|
277
|
+
signature_name = signature_class&.name
|
|
278
|
+
span_attributes['dspy.signature'] = signature_name || 'anonymous'
|
|
279
|
+
span_attributes['dspy.signature_kind'] = infer_signature_kind(signature_name)
|
|
280
|
+
span_attributes['dspy.predictor_label'] = module_scope_label if module_scope_label
|
|
281
|
+
operation_name = "DSPy::Predict(#{signature_name}).forward" if signature_name
|
|
282
|
+
end
|
|
270
283
|
|
|
271
284
|
DSPy::Context.with_span(
|
|
272
|
-
operation:
|
|
285
|
+
operation: operation_name,
|
|
273
286
|
**span_attributes
|
|
274
287
|
) do |span|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
span
|
|
288
|
+
begin
|
|
289
|
+
yield.tap do |result|
|
|
290
|
+
if span && !result.nil?
|
|
291
|
+
span.set_attribute('langfuse.observation.output', serialize_module_output(result))
|
|
292
|
+
span.set_attribute('langfuse.observation.status', 'completed')
|
|
293
|
+
span.set_attribute('dspy.status', 'completed')
|
|
294
|
+
if root_call
|
|
295
|
+
span.set_attribute('langfuse.trace.output', serialize_module_output(result))
|
|
296
|
+
span.set_attribute('langfuse.trace.status', 'completed')
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
if span
|
|
302
|
+
span.set_attribute('langfuse.observation.output', serialize_module_error_output(e))
|
|
303
|
+
span.set_attribute('langfuse.observation.status', 'error')
|
|
304
|
+
span.set_attribute('dspy.error.class', e.class.name)
|
|
305
|
+
span.set_attribute('dspy.error.message', e.message.to_s[0, 2000]) if e.message
|
|
306
|
+
span.set_attribute('dspy.status', 'error')
|
|
307
|
+
if root_call
|
|
308
|
+
span.set_attribute('langfuse.trace.output', serialize_module_error_output(e))
|
|
309
|
+
span.set_attribute('langfuse.trace.status', 'error')
|
|
310
|
+
end
|
|
311
|
+
if e.respond_to?(:iterations)
|
|
312
|
+
span.set_attribute('dspy.error.iterations', e.iterations.to_i) unless e.iterations.nil?
|
|
313
|
+
end
|
|
314
|
+
if e.respond_to?(:max_iterations)
|
|
315
|
+
span.set_attribute('dspy.error.max_iterations', e.max_iterations.to_i) unless e.max_iterations.nil?
|
|
316
|
+
end
|
|
317
|
+
if e.respond_to?(:tools_used)
|
|
318
|
+
span.set_attribute('dspy.error.tools_used', Array(e.tools_used).map(&:to_s))
|
|
319
|
+
end
|
|
278
320
|
end
|
|
321
|
+
raise
|
|
279
322
|
end
|
|
280
323
|
end
|
|
281
324
|
end
|
|
@@ -303,7 +346,91 @@ module DSPy
|
|
|
303
346
|
result.to_s
|
|
304
347
|
end
|
|
305
348
|
|
|
306
|
-
|
|
349
|
+
def serialize_module_error_output(error)
|
|
350
|
+
payload = {
|
|
351
|
+
error: {
|
|
352
|
+
class: error.class.name,
|
|
353
|
+
message: error.message.to_s
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if error.respond_to?(:iterations) || error.respond_to?(:max_iterations) || error.respond_to?(:tools_used)
|
|
358
|
+
payload[:react] = {}
|
|
359
|
+
payload[:react][:iterations] = error.iterations if error.respond_to?(:iterations)
|
|
360
|
+
payload[:react][:max_iterations] = error.max_iterations if error.respond_to?(:max_iterations)
|
|
361
|
+
payload[:react][:tools_used] = Array(error.tools_used) if error.respond_to?(:tools_used)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
serialized = DSPy::TypeSerializer.serialize(payload)
|
|
365
|
+
JSON.generate(serialized)
|
|
366
|
+
rescue StandardError
|
|
367
|
+
"#{error.class}: #{error.message}"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def root_trace_attributes(call_args, call_kwargs, input_json)
|
|
371
|
+
metadata = {
|
|
372
|
+
module: self.class.name,
|
|
373
|
+
signature: (respond_to?(:signature_class) ? signature_class&.name : nil),
|
|
374
|
+
signature_kind: (respond_to?(:signature_class) ? infer_signature_kind(signature_class&.name) : nil),
|
|
375
|
+
predictor_label: module_scope_label
|
|
376
|
+
}.compact
|
|
377
|
+
conversation_id, conversation_id_source = resolve_conversation_id(call_args, call_kwargs)
|
|
378
|
+
metadata[:conversation_id_source] = conversation_id_source if conversation_id_source
|
|
379
|
+
|
|
380
|
+
{
|
|
381
|
+
'langfuse.trace.name' => "#{self.class.name}.forward",
|
|
382
|
+
'langfuse.trace.input' => input_json,
|
|
383
|
+
'langfuse.trace.metadata' => JSON.generate(metadata),
|
|
384
|
+
'langfuse.trace.output' => '{"status":"in_progress"}',
|
|
385
|
+
'conversation_id' => conversation_id,
|
|
386
|
+
'dspy.conversation_id' => conversation_id
|
|
387
|
+
}
|
|
388
|
+
rescue StandardError
|
|
389
|
+
{}
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Conversation ID precedence is deterministic:
|
|
393
|
+
# 1. top-level kwargs[:conversation_id]
|
|
394
|
+
# 2. first positional hash arg[:conversation_id]
|
|
395
|
+
# 3. kwargs[:input_context][:conversation_id]
|
|
396
|
+
# 4. DSPy::Context.current[:conversation_id]
|
|
397
|
+
def resolve_conversation_id(call_args, call_kwargs)
|
|
398
|
+
direct = fetch_hash_value(call_kwargs, :conversation_id)
|
|
399
|
+
return [direct.to_s, 'kwargs.conversation_id'] if present_value?(direct)
|
|
400
|
+
|
|
401
|
+
first_arg = call_args.first if call_args.is_a?(Array) && call_args.first.is_a?(Hash)
|
|
402
|
+
arg_value = fetch_hash_value(first_arg, :conversation_id)
|
|
403
|
+
return [arg_value.to_s, 'args[0].conversation_id'] if present_value?(arg_value)
|
|
404
|
+
|
|
405
|
+
input_context = fetch_hash_value(call_kwargs, :input_context)
|
|
406
|
+
nested = fetch_hash_value(input_context, :conversation_id)
|
|
407
|
+
return [nested.to_s, 'kwargs.input_context.conversation_id'] if present_value?(nested)
|
|
408
|
+
|
|
409
|
+
context_value = fetch_hash_value(DSPy::Context.current, :conversation_id)
|
|
410
|
+
return [context_value.to_s, 'context.conversation_id'] if present_value?(context_value)
|
|
411
|
+
|
|
412
|
+
[nil, nil]
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def fetch_hash_value(hash, key)
|
|
416
|
+
return nil unless hash.is_a?(Hash)
|
|
417
|
+
|
|
418
|
+
hash[key] || hash[key.to_s]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def present_value?(value)
|
|
422
|
+
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def infer_signature_kind(signature_name)
|
|
426
|
+
return 'custom' unless signature_name
|
|
427
|
+
return 'thought' if signature_name.match?(/thought/i)
|
|
428
|
+
return 'observation' if signature_name.match?(/observation/i)
|
|
429
|
+
|
|
430
|
+
'custom'
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
private :instrument_forward_call, :serialize_module_input, :serialize_module_output, :serialize_module_error_output, :root_trace_attributes, :resolve_conversation_id, :fetch_hash_value, :present_value?, :infer_signature_kind
|
|
307
434
|
|
|
308
435
|
sig { returns(String) }
|
|
309
436
|
def module_scope_id
|
data/lib/dspy/predict.rb
CHANGED
|
@@ -304,7 +304,7 @@ module DSPy
|
|
|
304
304
|
next unless prop_type
|
|
305
305
|
|
|
306
306
|
# For nilable fields with nil values, ensure proper handling
|
|
307
|
-
if value.nil? &&
|
|
307
|
+
if value.nil? && nilable_type?(prop_type)
|
|
308
308
|
# For nilable fields, nil is valid - keep it as is
|
|
309
309
|
next
|
|
310
310
|
elsif value.nil? && prop_info[:fully_optional]
|
data/lib/dspy/prediction.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative 'utils/serialization'
|
|
5
|
+
|
|
4
6
|
module DSPy
|
|
5
7
|
class Prediction
|
|
6
8
|
extend T::Sig
|
|
@@ -54,7 +56,14 @@ module DSPy
|
|
|
54
56
|
|
|
55
57
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
56
58
|
def to_h
|
|
57
|
-
@_struct.serialize
|
|
59
|
+
hash = DSPy::Utils::Serialization.deep_serialize(@_struct.serialize)
|
|
60
|
+
hash.delete('_prediction_marker')
|
|
61
|
+
hash
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { params(args: T.untyped).returns(String) }
|
|
65
|
+
def to_json(*args)
|
|
66
|
+
to_h.to_json(*args)
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
private
|
data/lib/dspy/prompt.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'sorbet-runtime'
|
|
|
5
5
|
require 'sorbet/toon'
|
|
6
6
|
|
|
7
7
|
require_relative 'few_shot_example'
|
|
8
|
+
require_relative 'utils/serialization'
|
|
8
9
|
require_relative 'schema/sorbet_toon_adapter'
|
|
9
10
|
|
|
10
11
|
module DSPy
|
|
@@ -241,7 +242,7 @@ module DSPy
|
|
|
241
242
|
else
|
|
242
243
|
sections << "## Input Values"
|
|
243
244
|
sections << "```json"
|
|
244
|
-
sections << JSON.pretty_generate(
|
|
245
|
+
sections << JSON.pretty_generate(DSPy::Utils::Serialization.deep_serialize(input_values))
|
|
245
246
|
sections << "```"
|
|
246
247
|
sections << ""
|
|
247
248
|
sections << "Respond with the corresponding output schema fields wrapped in a ```json ``` block,"
|
|
@@ -376,51 +377,6 @@ module DSPy
|
|
|
376
377
|
"# Please install: gem install sorbet-baml"
|
|
377
378
|
end
|
|
378
379
|
|
|
379
|
-
# Recursively serialize complex objects for JSON representation
|
|
380
|
-
sig { params(obj: T.untyped).returns(T.untyped) }
|
|
381
|
-
def serialize_for_json(obj)
|
|
382
|
-
case obj
|
|
383
|
-
when T::Struct
|
|
384
|
-
# Convert T::Struct to hash using to_h method if available
|
|
385
|
-
if obj.respond_to?(:to_h)
|
|
386
|
-
serialize_for_json(obj.to_h)
|
|
387
|
-
else
|
|
388
|
-
# Fallback: serialize using struct properties
|
|
389
|
-
serialize_struct_to_hash(obj)
|
|
390
|
-
end
|
|
391
|
-
when Hash
|
|
392
|
-
# Recursively serialize hash values
|
|
393
|
-
obj.transform_values { |v| serialize_for_json(v) }
|
|
394
|
-
when Array
|
|
395
|
-
# Recursively serialize array elements
|
|
396
|
-
obj.map { |item| serialize_for_json(item) }
|
|
397
|
-
when T::Enum
|
|
398
|
-
# Serialize enums to their string representation
|
|
399
|
-
obj.serialize
|
|
400
|
-
else
|
|
401
|
-
# For basic types (String, Integer, Float, Boolean, etc.), return as-is
|
|
402
|
-
obj
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
# Fallback method to serialize T::Struct to hash when to_h is not available
|
|
407
|
-
sig { params(struct_obj: T::Struct).returns(T::Hash[Symbol, T.untyped]) }
|
|
408
|
-
def serialize_struct_to_hash(struct_obj)
|
|
409
|
-
result = {}
|
|
410
|
-
|
|
411
|
-
# Use struct's props method to get all properties
|
|
412
|
-
if struct_obj.class.respond_to?(:props)
|
|
413
|
-
struct_obj.class.props.each do |prop_name, _prop_info|
|
|
414
|
-
if struct_obj.respond_to?(prop_name)
|
|
415
|
-
value = struct_obj.public_send(prop_name)
|
|
416
|
-
result[prop_name] = serialize_for_json(value)
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
result
|
|
422
|
-
end
|
|
423
|
-
|
|
424
380
|
def toon_data_format_enabled?
|
|
425
381
|
data_format == :toon && @signature_class
|
|
426
382
|
end
|
data/lib/dspy/re_act.rb
CHANGED
|
@@ -47,7 +47,56 @@ module DSPy
|
|
|
47
47
|
include Mixins::StructBuilder
|
|
48
48
|
|
|
49
49
|
# Custom error classes
|
|
50
|
-
class MaxIterationsError < StandardError
|
|
50
|
+
class MaxIterationsError < StandardError
|
|
51
|
+
extend T::Sig
|
|
52
|
+
|
|
53
|
+
sig { returns(T.nilable(Integer)) }
|
|
54
|
+
attr_reader :iterations
|
|
55
|
+
|
|
56
|
+
sig { returns(T.nilable(Integer)) }
|
|
57
|
+
attr_reader :max_iterations
|
|
58
|
+
|
|
59
|
+
sig { returns(T::Array[String]) }
|
|
60
|
+
attr_reader :tools_used
|
|
61
|
+
|
|
62
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
63
|
+
attr_reader :history
|
|
64
|
+
|
|
65
|
+
sig { returns(T.untyped) }
|
|
66
|
+
attr_reader :last_observation
|
|
67
|
+
|
|
68
|
+
sig { returns(T.untyped) }
|
|
69
|
+
attr_reader :partial_final_answer
|
|
70
|
+
|
|
71
|
+
sig do
|
|
72
|
+
params(
|
|
73
|
+
message: String,
|
|
74
|
+
iterations: T.nilable(Integer),
|
|
75
|
+
max_iterations: T.nilable(Integer),
|
|
76
|
+
tools_used: T::Array[String],
|
|
77
|
+
history: T::Array[T::Hash[Symbol, T.untyped]],
|
|
78
|
+
last_observation: T.untyped,
|
|
79
|
+
partial_final_answer: T.untyped
|
|
80
|
+
).void
|
|
81
|
+
end
|
|
82
|
+
def initialize(
|
|
83
|
+
message = "Agent reached maximum iterations without producing a final answer",
|
|
84
|
+
iterations: nil,
|
|
85
|
+
max_iterations: nil,
|
|
86
|
+
tools_used: [],
|
|
87
|
+
history: [],
|
|
88
|
+
last_observation: nil,
|
|
89
|
+
partial_final_answer: nil
|
|
90
|
+
)
|
|
91
|
+
@iterations = T.let(iterations, T.nilable(Integer))
|
|
92
|
+
@max_iterations = T.let(max_iterations, T.nilable(Integer))
|
|
93
|
+
@tools_used = T.let(tools_used, T::Array[String])
|
|
94
|
+
@history = T.let(history, T::Array[T::Hash[Symbol, T.untyped]])
|
|
95
|
+
@last_observation = last_observation
|
|
96
|
+
@partial_final_answer = partial_final_answer
|
|
97
|
+
super(message)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
51
100
|
class InvalidActionError < StandardError; end
|
|
52
101
|
class TypeMismatchError < StandardError; end
|
|
53
102
|
|
|
@@ -201,19 +250,17 @@ module DSPy
|
|
|
201
250
|
|
|
202
251
|
sig { params(input_struct: T.untyped).returns(T.untyped) }
|
|
203
252
|
def format_input_context(input_struct)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
DSPy::TypeSerializer.serialize(input_struct).to_json
|
|
253
|
+
input_struct
|
|
207
254
|
end
|
|
208
255
|
|
|
209
256
|
sig { params(history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
210
257
|
def format_history(history)
|
|
211
|
-
|
|
258
|
+
history
|
|
212
259
|
end
|
|
213
260
|
|
|
214
261
|
sig { params(observation: T.untyped).returns(T.untyped) }
|
|
215
262
|
def format_observation(observation)
|
|
216
|
-
|
|
263
|
+
observation
|
|
217
264
|
end
|
|
218
265
|
|
|
219
266
|
sig { returns(T::Boolean) }
|
|
@@ -253,11 +300,7 @@ module DSPy
|
|
|
253
300
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
254
301
|
def create_thought_signature(signature_class, data_format)
|
|
255
302
|
action_enum_class = @action_enum_class
|
|
256
|
-
input_context_type =
|
|
257
|
-
signature_class.input_struct_class || String
|
|
258
|
-
else
|
|
259
|
-
String
|
|
260
|
-
end
|
|
303
|
+
input_context_type = signature_class.input_struct_class || String
|
|
261
304
|
|
|
262
305
|
# Get the output field type for the final_answer field
|
|
263
306
|
output_field_name = signature_class.output_struct_class.props.keys.first
|
|
@@ -271,7 +314,7 @@ module DSPy
|
|
|
271
314
|
# Define input fields
|
|
272
315
|
input do
|
|
273
316
|
const :input_context, input_context_type,
|
|
274
|
-
description:
|
|
317
|
+
description: "All original input fields with their typed values"
|
|
275
318
|
const :history, T::Array[HistoryEntry],
|
|
276
319
|
description: "Previous thoughts and actions, including observations from tools."
|
|
277
320
|
const :available_tools, T::Array[AvailableTool],
|
|
@@ -295,11 +338,7 @@ module DSPy
|
|
|
295
338
|
# Creates a dynamic observation signature that includes the original input fields
|
|
296
339
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
297
340
|
def create_observation_signature(signature_class, data_format)
|
|
298
|
-
input_context_type =
|
|
299
|
-
signature_class.input_struct_class || String
|
|
300
|
-
else
|
|
301
|
-
String
|
|
302
|
-
end
|
|
341
|
+
input_context_type = signature_class.input_struct_class || String
|
|
303
342
|
# Create new class that inherits from DSPy::Signature
|
|
304
343
|
Class.new(DSPy::Signature) do
|
|
305
344
|
# Set description
|
|
@@ -308,7 +347,7 @@ module DSPy
|
|
|
308
347
|
# Define input fields
|
|
309
348
|
input do
|
|
310
349
|
const :input_context, input_context_type,
|
|
311
|
-
description:
|
|
350
|
+
description: "All original input fields with their typed values"
|
|
312
351
|
const :history, T::Array[HistoryEntry],
|
|
313
352
|
description: "Previous thoughts, actions, and observations."
|
|
314
353
|
const :observation, T.untyped,
|
|
@@ -392,7 +431,7 @@ module DSPy
|
|
|
392
431
|
if finish_action?(thought_obj.action)
|
|
393
432
|
final_answer = handle_finish_action(
|
|
394
433
|
thought_obj.final_answer, last_observation, iteration,
|
|
395
|
-
thought_obj.thought, thought_obj.action, history
|
|
434
|
+
thought_obj.thought, thought_obj.action, history, expected_output_field_type
|
|
396
435
|
)
|
|
397
436
|
return { should_finish: true, final_answer: final_answer }
|
|
398
437
|
end
|
|
@@ -416,7 +455,7 @@ module DSPy
|
|
|
416
455
|
|
|
417
456
|
# Process observation and decide next step
|
|
418
457
|
observation_decision = process_observation_and_decide_next_step(
|
|
419
|
-
input_struct, history, observation, available_tools_desc, iteration
|
|
458
|
+
input_struct, history, observation, available_tools_desc, iteration, tools_used
|
|
420
459
|
)
|
|
421
460
|
|
|
422
461
|
if observation_decision[:should_finish]
|
|
@@ -463,7 +502,8 @@ module DSPy
|
|
|
463
502
|
if final_answer.nil?
|
|
464
503
|
iterations = reasoning_result[:iterations]
|
|
465
504
|
tools_used = reasoning_result[:tools_used]
|
|
466
|
-
|
|
505
|
+
history = T.cast(reasoning_result[:history], T::Array[HistoryEntry])
|
|
506
|
+
raise build_max_iterations_error(iterations: iterations, tools_used: tools_used, history: history)
|
|
467
507
|
end
|
|
468
508
|
|
|
469
509
|
output_data = input_kwargs.merge({
|
|
@@ -489,6 +529,19 @@ module DSPy
|
|
|
489
529
|
history.reverse.find { |entry| !entry.observation.nil? }&.observation
|
|
490
530
|
end
|
|
491
531
|
|
|
532
|
+
sig { params(iterations: Integer, tools_used: T::Array[String], history: T::Array[HistoryEntry], partial_final_answer: T.untyped).returns(MaxIterationsError) }
|
|
533
|
+
def build_max_iterations_error(iterations:, tools_used:, history:, partial_final_answer: nil)
|
|
534
|
+
MaxIterationsError.new(
|
|
535
|
+
"Agent reached maximum iterations (#{iterations}) without producing a final answer. Tools used: #{tools_used.join(', ')}",
|
|
536
|
+
iterations: iterations,
|
|
537
|
+
max_iterations: @max_iterations,
|
|
538
|
+
tools_used: tools_used.uniq,
|
|
539
|
+
history: history.map(&:to_h),
|
|
540
|
+
last_observation: find_last_tool_observation(history),
|
|
541
|
+
partial_final_answer: partial_final_answer
|
|
542
|
+
)
|
|
543
|
+
end
|
|
544
|
+
|
|
492
545
|
# Deserialize final answer to match expected output type
|
|
493
546
|
# Routes to appropriate deserialization based on type classification
|
|
494
547
|
sig { params(final_answer: T.untyped, output_field_type: T.untyped, history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
@@ -532,6 +585,12 @@ module DSPy
|
|
|
532
585
|
return last_tool_observation
|
|
533
586
|
end
|
|
534
587
|
|
|
588
|
+
# Structured outputs frequently arrive as JSON strings; parse before coercion.
|
|
589
|
+
if final_answer.is_a?(String)
|
|
590
|
+
parsed = parse_json_string(final_answer)
|
|
591
|
+
final_answer = parsed unless parsed.nil?
|
|
592
|
+
end
|
|
593
|
+
|
|
535
594
|
# If final_answer already matches, use it
|
|
536
595
|
return final_answer if type_matches?(final_answer, output_field_type)
|
|
537
596
|
|
|
@@ -572,7 +631,11 @@ module DSPy
|
|
|
572
631
|
def type_matches?(value, type_object)
|
|
573
632
|
case type_object
|
|
574
633
|
when T::Types::TypedArray
|
|
575
|
-
|
|
634
|
+
return false unless value.is_a?(Array)
|
|
635
|
+
return true if value.empty?
|
|
636
|
+
|
|
637
|
+
element_type = type_object.type
|
|
638
|
+
value.all? { |item| type_matches?(item, element_type) }
|
|
576
639
|
when T::Types::TypedHash
|
|
577
640
|
value.is_a?(Hash)
|
|
578
641
|
when T::Types::Simple
|
|
@@ -622,12 +685,25 @@ module DSPy
|
|
|
622
685
|
'dspy.module' => 'ReAct',
|
|
623
686
|
'react.iteration' => iteration,
|
|
624
687
|
'tool.name' => action_str.downcase,
|
|
688
|
+
'langfuse.observation.input' => serialize_tool_payload(tool_input),
|
|
625
689
|
'tool.input' => tool_input
|
|
626
|
-
) do
|
|
627
|
-
execute_action(action_str, tool_input)
|
|
690
|
+
) do |span|
|
|
691
|
+
result = execute_action(action_str, tool_input)
|
|
692
|
+
if span
|
|
693
|
+
span.set_attribute('langfuse.observation.output', serialize_tool_payload(result))
|
|
694
|
+
end
|
|
695
|
+
result
|
|
628
696
|
end
|
|
629
697
|
end
|
|
630
698
|
|
|
699
|
+
sig { params(payload: T.untyped).returns(String) }
|
|
700
|
+
def serialize_tool_payload(payload)
|
|
701
|
+
serialized = DSPy::TypeSerializer.serialize(payload)
|
|
702
|
+
JSON.generate(serialized)
|
|
703
|
+
rescue StandardError
|
|
704
|
+
payload.to_s
|
|
705
|
+
end
|
|
706
|
+
|
|
631
707
|
sig { params(step: Integer, thought: String, action: String, tool_input: ToolInput, observation: T.untyped).returns(HistoryEntry) }
|
|
632
708
|
def create_history_entry(step, thought, action, tool_input, observation)
|
|
633
709
|
HistoryEntry.new(
|
|
@@ -639,8 +715,8 @@ module DSPy
|
|
|
639
715
|
)
|
|
640
716
|
end
|
|
641
717
|
|
|
642
|
-
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]) }
|
|
643
|
-
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
|
718
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: T.untyped, available_tools_desc: T::Array[AvailableTool], iteration: Integer, tools_used: T::Array[String]).returns(T::Hash[Symbol, T.untyped]) }
|
|
719
|
+
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration, tools_used)
|
|
644
720
|
observation_result = @observation_processor.forward(
|
|
645
721
|
input_context: format_input_context(input_struct),
|
|
646
722
|
history: format_history(history),
|
|
@@ -650,14 +726,14 @@ module DSPy
|
|
|
650
726
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
|
651
727
|
|
|
652
728
|
final_answer = generate_forced_final_answer(
|
|
653
|
-
input_struct, history, available_tools_desc, observation_result, iteration
|
|
729
|
+
input_struct, history, available_tools_desc, observation_result, iteration, tools_used
|
|
654
730
|
)
|
|
655
731
|
|
|
656
732
|
{ should_finish: true, final_answer: final_answer }
|
|
657
733
|
end
|
|
658
734
|
|
|
659
|
-
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) }
|
|
660
|
-
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
|
735
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer, tools_used: T::Array[String]).returns(T.untyped) }
|
|
736
|
+
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration, tools_used)
|
|
661
737
|
final_thought = @thought_generator.forward(
|
|
662
738
|
input_context: format_input_context(input_struct),
|
|
663
739
|
history: format_history(history),
|
|
@@ -665,6 +741,8 @@ module DSPy
|
|
|
665
741
|
)
|
|
666
742
|
|
|
667
743
|
action_str = final_thought.action.respond_to?(:serialize) ? final_thought.action.serialize : final_thought.action.to_s
|
|
744
|
+
output_field_type = expected_output_field_type
|
|
745
|
+
|
|
668
746
|
if action_str.downcase != FINISH_ACTION
|
|
669
747
|
# Use interpretation if available, otherwise use last observation
|
|
670
748
|
forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
|
|
@@ -674,9 +752,23 @@ module DSPy
|
|
|
674
752
|
else
|
|
675
753
|
raise MaxIterationsError, "Observation processor indicated finish but no answer is available"
|
|
676
754
|
end
|
|
677
|
-
|
|
755
|
+
|
|
756
|
+
if structured_type?(output_field_type)
|
|
757
|
+
coerced_forced_answer = coerce_candidate_for_output(forced_answer, output_field_type)
|
|
758
|
+
if coerced_forced_answer.nil?
|
|
759
|
+
raise build_max_iterations_error(
|
|
760
|
+
iterations: iteration,
|
|
761
|
+
tools_used: tools_used,
|
|
762
|
+
history: history,
|
|
763
|
+
partial_final_answer: forced_answer
|
|
764
|
+
)
|
|
765
|
+
end
|
|
766
|
+
handle_finish_action(coerced_forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history, output_field_type)
|
|
767
|
+
else
|
|
768
|
+
handle_finish_action(forced_answer, history.last&.observation, iteration + 1, final_thought.thought, FINISH_ACTION, history, output_field_type)
|
|
769
|
+
end
|
|
678
770
|
else
|
|
679
|
-
handle_finish_action(final_thought.final_answer, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history)
|
|
771
|
+
handle_finish_action(final_thought.final_answer, history.last&.observation, iteration + 1, final_thought.thought, final_thought.action, history, output_field_type)
|
|
680
772
|
end
|
|
681
773
|
end
|
|
682
774
|
|
|
@@ -738,13 +830,14 @@ module DSPy
|
|
|
738
830
|
example
|
|
739
831
|
end
|
|
740
832
|
|
|
741
|
-
sig { params(final_answer_value: T.untyped, last_observation: T.untyped, step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry]).returns(T.untyped) }
|
|
742
|
-
def handle_finish_action(final_answer_value, last_observation, step, thought, action, history)
|
|
833
|
+
sig { params(final_answer_value: T.untyped, last_observation: T.untyped, step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry], output_field_type: T.untyped).returns(T.untyped) }
|
|
834
|
+
def handle_finish_action(final_answer_value, last_observation, step, thought, action, history, output_field_type)
|
|
743
835
|
final_answer = final_answer_value
|
|
744
836
|
|
|
745
837
|
# If final_answer is empty/nil but we have a last observation, use it
|
|
746
838
|
if (final_answer.nil? || (final_answer.is_a?(String) && final_answer.empty?)) && last_observation
|
|
747
|
-
|
|
839
|
+
fallback_value = coerce_candidate_for_output(last_observation, output_field_type)
|
|
840
|
+
final_answer = fallback_value unless fallback_value.nil?
|
|
748
841
|
end
|
|
749
842
|
|
|
750
843
|
# Convert action enum to string for storage in history
|
|
@@ -761,5 +854,37 @@ module DSPy
|
|
|
761
854
|
|
|
762
855
|
final_answer
|
|
763
856
|
end
|
|
857
|
+
|
|
858
|
+
sig { returns(T.untyped) }
|
|
859
|
+
def expected_output_field_type
|
|
860
|
+
output_field_name = @original_signature_class.output_struct_class.props.keys.first
|
|
861
|
+
@original_signature_class.output_struct_class.props[output_field_name][:type_object]
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
sig { params(value: T.untyped, output_field_type: T.untyped).returns(T.untyped) }
|
|
865
|
+
def coerce_candidate_for_output(value, output_field_type)
|
|
866
|
+
return value if type_matches?(value, output_field_type)
|
|
867
|
+
|
|
868
|
+
candidate = value
|
|
869
|
+
if candidate.is_a?(String) && structured_type?(output_field_type)
|
|
870
|
+
parsed = parse_json_string(candidate)
|
|
871
|
+
candidate = parsed unless parsed.nil?
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
return candidate if type_matches?(candidate, output_field_type)
|
|
875
|
+
|
|
876
|
+
converted = convert_to_expected_type(candidate, output_field_type)
|
|
877
|
+
return converted if type_matches?(converted, output_field_type)
|
|
878
|
+
|
|
879
|
+
nil
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
sig { params(value: String).returns(T.nilable(T.untyped)) }
|
|
883
|
+
def parse_json_string(value)
|
|
884
|
+
parsed = JSON.parse(value)
|
|
885
|
+
parsed
|
|
886
|
+
rescue JSON::ParserError
|
|
887
|
+
nil
|
|
888
|
+
end
|
|
764
889
|
end
|
|
765
890
|
end
|
data/lib/dspy/signature.rb
CHANGED
|
@@ -58,11 +58,10 @@ module DSPy
|
|
|
58
58
|
Class.new(T::Struct) do
|
|
59
59
|
extend T::Sig
|
|
60
60
|
descriptors.each do |name, descriptor|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
61
|
+
opts = {}
|
|
62
|
+
opts[:default] = descriptor.default_value if descriptor.has_default
|
|
63
|
+
opts[:description] = descriptor.description if descriptor.description
|
|
64
|
+
const name, descriptor.type, **opts
|
|
66
65
|
end
|
|
67
66
|
end
|
|
68
67
|
end
|
|
@@ -80,7 +80,7 @@ module DSPy
|
|
|
80
80
|
|
|
81
81
|
sections << "## Input Values"
|
|
82
82
|
sections << "```json"
|
|
83
|
-
sections << JSON.pretty_generate(
|
|
83
|
+
sections << JSON.pretty_generate(DSPy::Utils::Serialization.deep_serialize(input_values))
|
|
84
84
|
sections << "```"
|
|
85
85
|
|
|
86
86
|
sections.join("\n")
|
|
@@ -13,6 +13,8 @@ module DSPy
|
|
|
13
13
|
when T::Struct
|
|
14
14
|
# Use the serialize method to convert to a plain hash
|
|
15
15
|
deep_serialize(obj.serialize)
|
|
16
|
+
when T::Enum
|
|
17
|
+
obj.serialize
|
|
16
18
|
when Hash
|
|
17
19
|
# Recursively serialize hash values
|
|
18
20
|
obj.transform_values { |v| deep_serialize(v) }
|
|
@@ -24,12 +26,6 @@ module DSPy
|
|
|
24
26
|
obj
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
|
-
|
|
28
|
-
# Serializes an object to JSON with proper T::Struct handling
|
|
29
|
-
sig { params(obj: T.untyped).returns(String) }
|
|
30
|
-
def self.to_json(obj)
|
|
31
|
-
deep_serialize(obj).to_json
|
|
32
|
-
end
|
|
33
29
|
end
|
|
34
30
|
end
|
|
35
31
|
end
|
data/lib/dspy/version.rb
CHANGED