dspy 0.2.0 → 0.3.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 +357 -248
- data/lib/dspy/chain_of_thought.rb +151 -11
- data/lib/dspy/instrumentation/token_tracker.rb +54 -0
- data/lib/dspy/instrumentation.rb +100 -0
- data/lib/dspy/lm/adapter.rb +41 -0
- data/lib/dspy/lm/adapter_factory.rb +59 -0
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +96 -0
- data/lib/dspy/lm/adapters/openai_adapter.rb +53 -0
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +81 -0
- data/lib/dspy/lm/errors.rb +10 -0
- data/lib/dspy/lm/response.rb +28 -0
- data/lib/dspy/lm.rb +92 -40
- data/lib/dspy/module.rb +51 -6
- data/lib/dspy/predict.rb +135 -15
- data/lib/dspy/re_act.rb +366 -191
- data/lib/dspy/schema_adapters.rb +55 -0
- data/lib/dspy/signature.rb +282 -10
- data/lib/dspy/subscribers/logger_subscriber.rb +197 -0
- data/lib/dspy/tools/{sorbet_tool.rb → base.rb} +33 -33
- data/lib/dspy/tools.rb +1 -1
- data/lib/dspy.rb +19 -10
- metadata +60 -28
- data/lib/dspy/ext/dry_schema.rb +0 -94
- data/lib/dspy/sorbet_chain_of_thought.rb +0 -91
- data/lib/dspy/sorbet_module.rb +0 -47
- data/lib/dspy/sorbet_predict.rb +0 -180
- data/lib/dspy/sorbet_re_act.rb +0 -332
- data/lib/dspy/sorbet_signature.rb +0 -218
- data/lib/dspy/types.rb +0 -3
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-schema'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
# Schema adapters for integrating sorbet-schema with T::Struct
|
7
|
+
module SchemaAdapters
|
8
|
+
# Handles sorbet-schema integration for serialization/deserialization
|
9
|
+
class SorbetSchemaAdapter
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Serialize a hash to a T::Struct using sorbet-schema
|
13
|
+
#
|
14
|
+
# @param struct_class [Class] T::Struct class
|
15
|
+
# @param hash_data [Hash] Data to serialize
|
16
|
+
# @return [T::Struct] Validated struct instance
|
17
|
+
sig { params(struct_class: T.class_of(T::Struct), hash_data: T::Hash[T.untyped, T.untyped]).returns(T::Struct) }
|
18
|
+
def self.from_hash(struct_class, hash_data)
|
19
|
+
# TODO: Implement using sorbet-schema
|
20
|
+
# For now, fall back to direct struct creation
|
21
|
+
struct_class.new(**hash_data.transform_keys(&:to_sym))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Deserialize a T::Struct to a hash using sorbet-schema
|
25
|
+
#
|
26
|
+
# @param struct_instance [T::Struct] Struct instance to serialize
|
27
|
+
# @return [Hash] Serialized hash
|
28
|
+
sig { params(struct_instance: T::Struct).returns(T::Hash[T.untyped, T.untyped]) }
|
29
|
+
def self.to_hash(struct_instance)
|
30
|
+
# TODO: Implement using sorbet-schema
|
31
|
+
# For now, fall back to simple serialization
|
32
|
+
result = {}
|
33
|
+
struct_instance.class.props.each do |name, _prop_info|
|
34
|
+
result[name] = struct_instance.send(name)
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validate data against a T::Struct schema using sorbet-schema
|
40
|
+
#
|
41
|
+
# @param struct_class [Class] T::Struct class
|
42
|
+
# @param hash_data [Hash] Data to validate
|
43
|
+
# @return [Array] [success_boolean, result_or_errors]
|
44
|
+
sig { params(struct_class: T.class_of(T::Struct), hash_data: T::Hash[T.untyped, T.untyped]).returns([T::Boolean, T.untyped]) }
|
45
|
+
def self.validate(struct_class, hash_data)
|
46
|
+
begin
|
47
|
+
result = from_hash(struct_class, hash_data)
|
48
|
+
[true, result]
|
49
|
+
rescue => e
|
50
|
+
[false, [e.message]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/dspy/signature.rb
CHANGED
@@ -1,25 +1,297 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require_relative 'schema_adapters'
|
5
|
+
|
3
6
|
module DSPy
|
4
7
|
class Signature
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
# Container for field type and description
|
11
|
+
class FieldDescriptor
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { returns(T.untyped) }
|
15
|
+
attr_reader :type
|
16
|
+
|
17
|
+
sig { returns(T.nilable(String)) }
|
18
|
+
attr_reader :description
|
19
|
+
|
20
|
+
sig { returns(T::Boolean) }
|
21
|
+
attr_reader :has_default
|
22
|
+
|
23
|
+
sig { params(type: T.untyped, description: T.nilable(String), has_default: T::Boolean).void }
|
24
|
+
def initialize(type, description = nil, has_default = false)
|
25
|
+
@type = type
|
26
|
+
@description = description
|
27
|
+
@has_default = has_default
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# DSL helper for building struct classes with field descriptions
|
32
|
+
class StructBuilder
|
33
|
+
extend T::Sig
|
34
|
+
|
35
|
+
sig { returns(T::Hash[Symbol, FieldDescriptor]) }
|
36
|
+
attr_reader :field_descriptors
|
37
|
+
|
38
|
+
sig { void }
|
39
|
+
def initialize
|
40
|
+
@field_descriptors = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(name: Symbol, type: T.untyped, kwargs: T.untyped).void }
|
44
|
+
def const(name, type, **kwargs)
|
45
|
+
description = kwargs[:description]
|
46
|
+
has_default = kwargs.key?(:default)
|
47
|
+
@field_descriptors[name] = FieldDescriptor.new(type, description, has_default)
|
48
|
+
# Store default for future use if needed
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { returns(T.class_of(T::Struct)) }
|
52
|
+
def build_struct_class
|
53
|
+
descriptors = @field_descriptors
|
54
|
+
Class.new(T::Struct) do
|
55
|
+
extend T::Sig
|
56
|
+
descriptors.each do |name, descriptor|
|
57
|
+
const name, descriptor.type
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
5
63
|
class << self
|
6
|
-
|
7
|
-
|
64
|
+
extend T::Sig
|
65
|
+
|
66
|
+
sig { returns(T.nilable(String)) }
|
67
|
+
attr_reader :desc
|
68
|
+
|
69
|
+
sig { returns(T.nilable(T.class_of(T::Struct))) }
|
70
|
+
attr_reader :input_struct_class
|
71
|
+
|
72
|
+
sig { returns(T.nilable(T.class_of(T::Struct))) }
|
73
|
+
attr_reader :output_struct_class
|
74
|
+
|
75
|
+
sig { returns(T::Hash[Symbol, FieldDescriptor]) }
|
76
|
+
attr_reader :input_field_descriptors
|
77
|
+
|
78
|
+
sig { returns(T::Hash[Symbol, FieldDescriptor]) }
|
79
|
+
attr_reader :output_field_descriptors
|
80
|
+
|
81
|
+
sig { params(desc: T.nilable(String)).returns(T.nilable(String)) }
|
82
|
+
def description(desc = nil)
|
83
|
+
if desc.nil?
|
84
|
+
@desc
|
85
|
+
else
|
86
|
+
@desc = desc
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { params(block: T.proc.void).void }
|
91
|
+
def input(&block)
|
92
|
+
builder = StructBuilder.new
|
8
93
|
|
9
|
-
|
10
|
-
|
11
|
-
@description = text
|
94
|
+
if block.arity > 0
|
95
|
+
block.call(builder)
|
12
96
|
else
|
13
|
-
|
97
|
+
# Preferred format
|
98
|
+
builder.instance_eval(&block)
|
14
99
|
end
|
100
|
+
|
101
|
+
@input_field_descriptors = builder.field_descriptors
|
102
|
+
@input_struct_class = builder.build_struct_class
|
15
103
|
end
|
16
104
|
|
17
|
-
|
18
|
-
|
105
|
+
sig { params(block: T.proc.void).void }
|
106
|
+
def output(&block)
|
107
|
+
builder = StructBuilder.new
|
108
|
+
|
109
|
+
if block.arity > 0
|
110
|
+
block.call(builder)
|
111
|
+
else
|
112
|
+
# Preferred format
|
113
|
+
builder.instance_eval(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
@output_field_descriptors = builder.field_descriptors
|
117
|
+
@output_struct_class = builder.build_struct_class
|
19
118
|
end
|
20
119
|
|
21
|
-
|
22
|
-
|
120
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
121
|
+
def input_json_schema
|
122
|
+
return {} unless @input_struct_class
|
123
|
+
|
124
|
+
properties = {}
|
125
|
+
required = []
|
126
|
+
|
127
|
+
@input_field_descriptors&.each do |name, descriptor|
|
128
|
+
schema = type_to_json_schema(descriptor.type)
|
129
|
+
schema[:description] = descriptor.description if descriptor.description
|
130
|
+
properties[name] = schema
|
131
|
+
required << name.to_s unless descriptor.has_default
|
132
|
+
end
|
133
|
+
|
134
|
+
{
|
135
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
136
|
+
type: "object",
|
137
|
+
properties: properties,
|
138
|
+
required: required
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
143
|
+
def output_json_schema
|
144
|
+
return {} unless @output_struct_class
|
145
|
+
|
146
|
+
properties = {}
|
147
|
+
required = []
|
148
|
+
|
149
|
+
@output_field_descriptors&.each do |name, descriptor|
|
150
|
+
schema = type_to_json_schema(descriptor.type)
|
151
|
+
schema[:description] = descriptor.description if descriptor.description
|
152
|
+
properties[name] = schema
|
153
|
+
required << name.to_s unless descriptor.has_default
|
154
|
+
end
|
155
|
+
|
156
|
+
{
|
157
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
158
|
+
type: "object",
|
159
|
+
properties: properties,
|
160
|
+
required: required
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
sig { params(type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
167
|
+
def type_to_json_schema(type)
|
168
|
+
# Handle T::Boolean type alias first
|
169
|
+
if type == T::Boolean
|
170
|
+
return { type: "boolean" }
|
171
|
+
end
|
172
|
+
|
173
|
+
# Handle raw class types first
|
174
|
+
if type.is_a?(Class)
|
175
|
+
if type < T::Enum
|
176
|
+
# Get all enum values
|
177
|
+
values = type.values.map(&:serialize)
|
178
|
+
{ type: "string", enum: values }
|
179
|
+
elsif type == String
|
180
|
+
{ type: "string" }
|
181
|
+
elsif type == Integer
|
182
|
+
{ type: "integer" }
|
183
|
+
elsif type == Float
|
184
|
+
{ type: "number" }
|
185
|
+
elsif type == Numeric
|
186
|
+
{ type: "number" }
|
187
|
+
elsif [TrueClass, FalseClass].include?(type)
|
188
|
+
{ type: "boolean" }
|
189
|
+
elsif type < T::Struct
|
190
|
+
# Handle custom T::Struct classes by generating nested object schema
|
191
|
+
generate_struct_schema(type)
|
192
|
+
else
|
193
|
+
{ type: "string" } # Default fallback
|
194
|
+
end
|
195
|
+
elsif type.is_a?(T::Types::Simple)
|
196
|
+
case type.raw_type.to_s
|
197
|
+
when "String"
|
198
|
+
{ type: "string" }
|
199
|
+
when "Integer"
|
200
|
+
{ type: "integer" }
|
201
|
+
when "Float"
|
202
|
+
{ type: "number" }
|
203
|
+
when "Numeric"
|
204
|
+
{ type: "number" }
|
205
|
+
when "TrueClass", "FalseClass"
|
206
|
+
{ type: "boolean" }
|
207
|
+
when "T::Boolean"
|
208
|
+
{ type: "boolean" }
|
209
|
+
else
|
210
|
+
# Check if it's an enum
|
211
|
+
if type.raw_type < T::Enum
|
212
|
+
# Get all enum values
|
213
|
+
values = type.raw_type.values.map(&:serialize)
|
214
|
+
{ type: "string", enum: values }
|
215
|
+
elsif type.raw_type < T::Struct
|
216
|
+
# Handle custom T::Struct classes
|
217
|
+
generate_struct_schema(type.raw_type)
|
218
|
+
else
|
219
|
+
{ type: "string" } # Default fallback
|
220
|
+
end
|
221
|
+
end
|
222
|
+
elsif type.is_a?(T::Types::TypedArray)
|
223
|
+
# Handle arrays properly with nested item type
|
224
|
+
{
|
225
|
+
type: "array",
|
226
|
+
items: type_to_json_schema(type.type)
|
227
|
+
}
|
228
|
+
elsif type.is_a?(T::Types::TypedHash)
|
229
|
+
# Handle hashes as objects with additionalProperties
|
230
|
+
# TypedHash has keys and values methods to access its key and value types
|
231
|
+
key_schema = type_to_json_schema(type.keys)
|
232
|
+
value_schema = type_to_json_schema(type.values)
|
233
|
+
|
234
|
+
# Create a more descriptive schema for nested structures
|
235
|
+
{
|
236
|
+
type: "object",
|
237
|
+
propertyNames: key_schema, # Describe key constraints
|
238
|
+
additionalProperties: value_schema,
|
239
|
+
# Add a more explicit description of the expected structure
|
240
|
+
description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
|
241
|
+
}
|
242
|
+
elsif type.is_a?(T::Types::Union)
|
243
|
+
# For optional types (T.nilable), just use the non-nil type
|
244
|
+
non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
245
|
+
if non_nil_types.size == 1
|
246
|
+
type_to_json_schema(non_nil_types.first)
|
247
|
+
elsif non_nil_types.size > 1
|
248
|
+
# Handle complex unions with oneOf for better JSON schema compliance
|
249
|
+
{
|
250
|
+
oneOf: non_nil_types.map { |t| type_to_json_schema(t) },
|
251
|
+
description: "Union of multiple types"
|
252
|
+
}
|
253
|
+
else
|
254
|
+
{ type: "string" } # Fallback for complex unions
|
255
|
+
end
|
256
|
+
elsif type.is_a?(T::Types::ClassOf)
|
257
|
+
# Handle T.class_of() types
|
258
|
+
{
|
259
|
+
type: "string",
|
260
|
+
description: "Class name (T.class_of type)"
|
261
|
+
}
|
262
|
+
else
|
263
|
+
{ type: "string" } # Default fallback
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
# Generate JSON schema for custom T::Struct classes
|
270
|
+
sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[Symbol, T.untyped]) }
|
271
|
+
def generate_struct_schema(struct_class)
|
272
|
+
return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
|
273
|
+
|
274
|
+
properties = {}
|
275
|
+
required = []
|
276
|
+
|
277
|
+
struct_class.props.each do |prop_name, prop_info|
|
278
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
279
|
+
properties[prop_name] = type_to_json_schema(prop_type)
|
280
|
+
|
281
|
+
# A field is required if it's not fully optional
|
282
|
+
# fully_optional is true for nilable prop fields
|
283
|
+
# immutable const fields are required unless nilable
|
284
|
+
unless prop_info[:fully_optional]
|
285
|
+
required << prop_name.to_s
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
{
|
290
|
+
type: "object",
|
291
|
+
properties: properties,
|
292
|
+
required: required,
|
293
|
+
description: "#{struct_class.name} struct"
|
294
|
+
}
|
23
295
|
end
|
24
296
|
end
|
25
297
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DSPy
|
4
|
+
module Subscribers
|
5
|
+
# Logger subscriber that provides detailed logging based on instrumentation events
|
6
|
+
# Subscribes to DSPy events and logs relevant information for debugging and monitoring
|
7
|
+
class LoggerSubscriber
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(logger: T.nilable(Logger)).void }
|
11
|
+
def initialize(logger: nil)
|
12
|
+
@logger = T.let(logger || DSPy.config.logger, Logger)
|
13
|
+
setup_event_subscriptions
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
sig { void }
|
19
|
+
def setup_event_subscriptions
|
20
|
+
# Subscribe to DSPy instrumentation events
|
21
|
+
DSPy::Instrumentation.subscribe('dspy.lm.request') do |event|
|
22
|
+
log_lm_request(event)
|
23
|
+
end
|
24
|
+
|
25
|
+
DSPy::Instrumentation.subscribe('dspy.predict') do |event|
|
26
|
+
log_prediction(event)
|
27
|
+
end
|
28
|
+
|
29
|
+
DSPy::Instrumentation.subscribe('dspy.chain_of_thought') do |event|
|
30
|
+
log_chain_of_thought(event)
|
31
|
+
end
|
32
|
+
|
33
|
+
DSPy::Instrumentation.subscribe('dspy.react') do |event|
|
34
|
+
log_react(event)
|
35
|
+
end
|
36
|
+
|
37
|
+
DSPy::Instrumentation.subscribe('dspy.react.iteration_complete') do |event|
|
38
|
+
log_react_iteration_complete(event)
|
39
|
+
end
|
40
|
+
|
41
|
+
DSPy::Instrumentation.subscribe('dspy.react.tool_call') do |event|
|
42
|
+
log_react_tool_call(event)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Callback methods for different event types
|
47
|
+
sig { params(event: T.untyped).void }
|
48
|
+
def on_lm_request(event)
|
49
|
+
log_lm_request(event)
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(event: T.untyped).void }
|
53
|
+
def on_predict(event)
|
54
|
+
log_prediction(event)
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { params(event: T.untyped).void }
|
58
|
+
def on_chain_of_thought(event)
|
59
|
+
log_chain_of_thought(event)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(event: T.untyped).void }
|
63
|
+
def on_react(event)
|
64
|
+
log_react(event)
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(event: T.untyped).void }
|
68
|
+
def on_react_iteration_complete(event)
|
69
|
+
log_react_iteration_complete(event)
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(event: T.untyped).void }
|
73
|
+
def on_react_tool_call(event)
|
74
|
+
log_react_tool_call(event)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Event logging methods
|
78
|
+
sig { params(event: T.untyped).void }
|
79
|
+
def log_lm_request(event)
|
80
|
+
payload = event.payload
|
81
|
+
provider = payload[:provider]
|
82
|
+
model = payload[:gen_ai_request_model] || payload[:model]
|
83
|
+
duration = payload[:duration_ms]&.round(2)
|
84
|
+
status = payload[:status]
|
85
|
+
tokens = if payload[:tokens_total]
|
86
|
+
" (#{payload[:tokens_total]} tokens)"
|
87
|
+
else
|
88
|
+
""
|
89
|
+
end
|
90
|
+
|
91
|
+
status_emoji = status == 'success' ? '✅' : '❌'
|
92
|
+
@logger.info("#{status_emoji} LM Request [#{provider}/#{model}] - #{status} (#{duration}ms)#{tokens}")
|
93
|
+
|
94
|
+
if status == 'error' && payload[:error_message]
|
95
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { params(event: T.untyped).void }
|
100
|
+
def log_prediction(event)
|
101
|
+
payload = event.payload
|
102
|
+
signature = payload[:signature_class]
|
103
|
+
duration = payload[:duration_ms]&.round(2)
|
104
|
+
status = payload[:status]
|
105
|
+
input_size = payload[:input_size]
|
106
|
+
|
107
|
+
status_emoji = status == 'success' ? '🔮' : '❌'
|
108
|
+
@logger.info("#{status_emoji} Prediction [#{signature}] - #{status} (#{duration}ms)")
|
109
|
+
@logger.info(" Input size: #{input_size} chars") if input_size
|
110
|
+
|
111
|
+
if status == 'error' && payload[:error_message]
|
112
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { params(event: T.untyped).void }
|
117
|
+
def log_chain_of_thought(event)
|
118
|
+
payload = event.payload
|
119
|
+
signature = payload[:signature_class]
|
120
|
+
duration = payload[:duration_ms]&.round(2)
|
121
|
+
status = payload[:status]
|
122
|
+
reasoning_steps = payload[:reasoning_steps]
|
123
|
+
reasoning_length = payload[:reasoning_length]
|
124
|
+
|
125
|
+
status_emoji = status == 'success' ? '🧠' : '❌'
|
126
|
+
@logger.info("#{status_emoji} Chain of Thought [#{signature}] - #{status} (#{duration}ms)")
|
127
|
+
@logger.info(" Reasoning steps: #{reasoning_steps}") if reasoning_steps
|
128
|
+
@logger.info(" Reasoning length: #{reasoning_length} chars") if reasoning_length
|
129
|
+
|
130
|
+
if status == 'error' && payload[:error_message]
|
131
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { params(event: T.untyped).void }
|
136
|
+
def log_react(event)
|
137
|
+
payload = event.payload
|
138
|
+
signature = payload[:signature_class]
|
139
|
+
duration = payload[:duration_ms]&.round(2)
|
140
|
+
status = payload[:status]
|
141
|
+
iteration_count = payload[:iteration_count]
|
142
|
+
tools_used = payload[:tools_used]
|
143
|
+
final_answer = payload[:final_answer]
|
144
|
+
|
145
|
+
status_emoji = case status
|
146
|
+
when 'success' then '🤖'
|
147
|
+
when 'max_iterations' then '⏰'
|
148
|
+
else '❌'
|
149
|
+
end
|
150
|
+
|
151
|
+
@logger.info("#{status_emoji} ReAct Agent [#{signature}] - #{status} (#{duration}ms)")
|
152
|
+
@logger.info(" Iterations: #{iteration_count}") if iteration_count
|
153
|
+
@logger.info(" Tools used: #{tools_used.join(', ')}") if tools_used&.any?
|
154
|
+
@logger.info(" Final answer: #{final_answer}") if final_answer
|
155
|
+
|
156
|
+
if status == 'error' && payload[:error_message]
|
157
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
sig { params(event: T.untyped).void }
|
162
|
+
def log_react_iteration_complete(event)
|
163
|
+
payload = event.payload
|
164
|
+
iteration = payload[:iteration]
|
165
|
+
thought = payload[:thought]
|
166
|
+
action = payload[:action]
|
167
|
+
duration = payload[:duration_ms]&.round(2)
|
168
|
+
status = payload[:status]
|
169
|
+
|
170
|
+
status_emoji = status == 'success' ? '🔄' : '❌'
|
171
|
+
@logger.info("#{status_emoji} ReAct Iteration #{iteration} - #{status} (#{duration}ms)")
|
172
|
+
@logger.info(" Thought: #{thought.truncate(100)}") if thought
|
173
|
+
@logger.info(" Action: #{action}") if action
|
174
|
+
|
175
|
+
if status == 'error' && payload[:error_message]
|
176
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
sig { params(event: T.untyped).void }
|
181
|
+
def log_react_tool_call(event)
|
182
|
+
payload = event.payload
|
183
|
+
iteration = payload[:iteration]
|
184
|
+
tool_name = payload[:tool_name]
|
185
|
+
duration = payload[:duration_ms]&.round(2)
|
186
|
+
status = payload[:status]
|
187
|
+
|
188
|
+
status_emoji = status == 'success' ? '🔧' : '❌'
|
189
|
+
@logger.info("#{status_emoji} Tool Call [#{tool_name}] (Iteration #{iteration}) - #{status} (#{duration}ms)")
|
190
|
+
|
191
|
+
if status == 'error' && payload[:error_message]
|
192
|
+
@logger.error(" Error: #{payload[:error_message]}")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|