dspy 0.29.0 → 0.30.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +45 -0
  3. data/README.md +121 -101
  4. data/lib/dspy/callbacks.rb +74 -19
  5. data/lib/dspy/context.rb +49 -4
  6. data/lib/dspy/errors.rb +19 -1
  7. data/lib/dspy/{datasets.rb → evals/version.rb} +2 -3
  8. data/lib/dspy/{evaluate.rb → evals.rb} +373 -110
  9. data/lib/dspy/mixins/instruction_updatable.rb +22 -0
  10. data/lib/dspy/observability.rb +40 -182
  11. data/lib/dspy/predict.rb +10 -2
  12. data/lib/dspy/propose/dataset_summary_generator.rb +28 -18
  13. data/lib/dspy/re_act.rb +21 -0
  14. data/lib/dspy/schema/sorbet_json_schema.rb +302 -0
  15. data/lib/dspy/schema/version.rb +7 -0
  16. data/lib/dspy/schema.rb +4 -0
  17. data/lib/dspy/structured_outputs_prompt.rb +48 -0
  18. data/lib/dspy/support/warning_filters.rb +27 -0
  19. data/lib/dspy/teleprompt/gepa.rb +9 -588
  20. data/lib/dspy/teleprompt/instruction_updates.rb +94 -0
  21. data/lib/dspy/teleprompt/teleprompter.rb +6 -6
  22. data/lib/dspy/teleprompt/utils.rb +5 -65
  23. data/lib/dspy/type_system/sorbet_json_schema.rb +2 -299
  24. data/lib/dspy/version.rb +1 -1
  25. data/lib/dspy.rb +33 -7
  26. metadata +14 -60
  27. data/lib/dspy/code_act.rb +0 -477
  28. data/lib/dspy/datasets/ade.rb +0 -90
  29. data/lib/dspy/observability/async_span_processor.rb +0 -250
  30. data/lib/dspy/observability/observation_type.rb +0 -65
  31. data/lib/dspy/optimizers/gaussian_process.rb +0 -141
  32. data/lib/dspy/teleprompt/mipro_v2.rb +0 -1423
  33. data/lib/gepa/api.rb +0 -61
  34. data/lib/gepa/core/engine.rb +0 -226
  35. data/lib/gepa/core/evaluation_batch.rb +0 -26
  36. data/lib/gepa/core/result.rb +0 -92
  37. data/lib/gepa/core/state.rb +0 -231
  38. data/lib/gepa/logging/experiment_tracker.rb +0 -54
  39. data/lib/gepa/logging/logger.rb +0 -57
  40. data/lib/gepa/logging.rb +0 -9
  41. data/lib/gepa/proposer/base.rb +0 -27
  42. data/lib/gepa/proposer/merge_proposer.rb +0 -424
  43. data/lib/gepa/proposer/reflective_mutation/base.rb +0 -48
  44. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +0 -188
  45. data/lib/gepa/strategies/batch_sampler.rb +0 -91
  46. data/lib/gepa/strategies/candidate_selector.rb +0 -97
  47. data/lib/gepa/strategies/component_selector.rb +0 -57
  48. data/lib/gepa/strategies/instruction_proposal.rb +0 -120
  49. data/lib/gepa/telemetry.rb +0 -122
  50. data/lib/gepa/utils/pareto.rb +0 -119
  51. data/lib/gepa.rb +0 -21
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-19 00:00:00.000000000 Z
11
+ date: 2025-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-configurable
@@ -151,19 +151,19 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0.1'
153
153
  - !ruby/object:Gem::Dependency
154
- name: numo-narray
154
+ name: dspy-schema
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '0.9'
159
+ version: 1.0.0
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '0.9'
166
+ version: 1.0.0
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: informers
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -178,34 +178,6 @@ dependencies:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
180
  version: '1.2'
181
- - !ruby/object:Gem::Dependency
182
- name: opentelemetry-sdk
183
- requirement: !ruby/object:Gem::Requirement
184
- requirements:
185
- - - "~>"
186
- - !ruby/object:Gem::Version
187
- version: '1.8'
188
- type: :runtime
189
- prerelease: false
190
- version_requirements: !ruby/object:Gem::Requirement
191
- requirements:
192
- - - "~>"
193
- - !ruby/object:Gem::Version
194
- version: '1.8'
195
- - !ruby/object:Gem::Dependency
196
- name: opentelemetry-exporter-otlp
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - "~>"
200
- - !ruby/object:Gem::Version
201
- version: '0.30'
202
- type: :runtime
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - "~>"
207
- - !ruby/object:Gem::Version
208
- version: '0.30'
209
181
  description: The Ruby framework for programming with large language models. DSPy.rb
210
182
  brings structured LLM programming to Ruby developers. Instead of wrestling with
211
183
  prompt strings and parsing responses, you define typed signatures using idiomatic
@@ -216,17 +188,16 @@ executables: []
216
188
  extensions: []
217
189
  extra_rdoc_files: []
218
190
  files:
191
+ - LICENSE
219
192
  - README.md
220
193
  - lib/dspy.rb
221
194
  - lib/dspy/callbacks.rb
222
195
  - lib/dspy/chain_of_thought.rb
223
- - lib/dspy/code_act.rb
224
196
  - lib/dspy/context.rb
225
- - lib/dspy/datasets.rb
226
- - lib/dspy/datasets/ade.rb
227
197
  - lib/dspy/error_formatter.rb
228
198
  - lib/dspy/errors.rb
229
- - lib/dspy/evaluate.rb
199
+ - lib/dspy/evals.rb
200
+ - lib/dspy/evals/version.rb
230
201
  - lib/dspy/events.rb
231
202
  - lib/dspy/events/subscriber_mixin.rb
232
203
  - lib/dspy/events/subscribers.rb
@@ -261,13 +232,11 @@ files:
261
232
  - lib/dspy/memory/memory_manager.rb
262
233
  - lib/dspy/memory/memory_record.rb
263
234
  - lib/dspy/memory/memory_store.rb
235
+ - lib/dspy/mixins/instruction_updatable.rb
264
236
  - lib/dspy/mixins/struct_builder.rb
265
237
  - lib/dspy/mixins/type_coercion.rb
266
238
  - lib/dspy/module.rb
267
239
  - lib/dspy/observability.rb
268
- - lib/dspy/observability/async_span_processor.rb
269
- - lib/dspy/observability/observation_type.rb
270
- - lib/dspy/optimizers/gaussian_process.rb
271
240
  - lib/dspy/predict.rb
272
241
  - lib/dspy/prediction.rb
273
242
  - lib/dspy/prompt.rb
@@ -277,15 +246,19 @@ files:
277
246
  - lib/dspy/reflection_lm.rb
278
247
  - lib/dspy/registry/registry_manager.rb
279
248
  - lib/dspy/registry/signature_registry.rb
249
+ - lib/dspy/schema.rb
250
+ - lib/dspy/schema/sorbet_json_schema.rb
251
+ - lib/dspy/schema/version.rb
280
252
  - lib/dspy/schema_adapters.rb
281
253
  - lib/dspy/signature.rb
282
254
  - lib/dspy/storage/program_storage.rb
283
255
  - lib/dspy/storage/storage_manager.rb
284
256
  - lib/dspy/structured_outputs_prompt.rb
257
+ - lib/dspy/support/warning_filters.rb
285
258
  - lib/dspy/teleprompt/bootstrap_strategy.rb
286
259
  - lib/dspy/teleprompt/data_handler.rb
287
260
  - lib/dspy/teleprompt/gepa.rb
288
- - lib/dspy/teleprompt/mipro_v2.rb
261
+ - lib/dspy/teleprompt/instruction_updates.rb
289
262
  - lib/dspy/teleprompt/teleprompter.rb
290
263
  - lib/dspy/teleprompt/utils.rb
291
264
  - lib/dspy/tools.rb
@@ -298,25 +271,6 @@ files:
298
271
  - lib/dspy/type_system/sorbet_json_schema.rb
299
272
  - lib/dspy/utils/serialization.rb
300
273
  - lib/dspy/version.rb
301
- - lib/gepa.rb
302
- - lib/gepa/api.rb
303
- - lib/gepa/core/engine.rb
304
- - lib/gepa/core/evaluation_batch.rb
305
- - lib/gepa/core/result.rb
306
- - lib/gepa/core/state.rb
307
- - lib/gepa/logging.rb
308
- - lib/gepa/logging/experiment_tracker.rb
309
- - lib/gepa/logging/logger.rb
310
- - lib/gepa/proposer/base.rb
311
- - lib/gepa/proposer/merge_proposer.rb
312
- - lib/gepa/proposer/reflective_mutation/base.rb
313
- - lib/gepa/proposer/reflective_mutation/reflective_mutation.rb
314
- - lib/gepa/strategies/batch_sampler.rb
315
- - lib/gepa/strategies/candidate_selector.rb
316
- - lib/gepa/strategies/component_selector.rb
317
- - lib/gepa/strategies/instruction_proposal.rb
318
- - lib/gepa/telemetry.rb
319
- - lib/gepa/utils/pareto.rb
320
274
  homepage: https://github.com/vicentereig/dspy.rb
321
275
  licenses:
322
276
  - MIT
data/lib/dspy/code_act.rb DELETED
@@ -1,477 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require 'sorbet-runtime'
5
- require_relative 'predict'
6
- require_relative 'signature'
7
- require 'json'
8
- require 'stringio'
9
- require_relative 'mixins/struct_builder'
10
- require_relative 'type_serializer'
11
-
12
- module DSPy
13
- # Define a simple struct for CodeAct history entries with proper type annotations
14
- class CodeActHistoryEntry < T::Struct
15
- const :step, Integer
16
- prop :thought, T.nilable(String)
17
- prop :ruby_code, T.nilable(String)
18
- prop :execution_result, T.nilable(String)
19
- prop :error_message, String
20
-
21
- # Custom serialization to ensure compatibility with the rest of the code
22
- def to_h
23
- {
24
- step: step,
25
- thought: thought,
26
- ruby_code: ruby_code,
27
- execution_result: execution_result,
28
- error_message: error_message
29
- }.compact
30
- end
31
- end
32
-
33
- # Defines the signature for Ruby code generation
34
- class RubyCodeGeneration < DSPy::Signature
35
- description "Generate Ruby code to solve the given task."
36
-
37
- input do
38
- const :task, String,
39
- description: "JSON representation of all input fields for the task"
40
- const :context, String,
41
- description: "Available variables and previous results from code execution history"
42
- const :history, T::Array[CodeActHistoryEntry],
43
- description: "Previous thoughts and code executions with their results. Use this to understand what has been tried and what variables are available."
44
- end
45
-
46
- output do
47
- const :thought, String,
48
- description: "Reasoning about the approach to solve the task with Ruby code"
49
- const :ruby_code, String,
50
- description: "Ruby code to execute. This should be valid Ruby code that can be evaluated safely. Avoid system calls, file operations, or other potentially dangerous operations."
51
- const :explanation, String,
52
- description: "Brief explanation of what the code does and why this approach was chosen"
53
- end
54
- end
55
-
56
- class CodeActNextStep < T::Enum
57
- enums do
58
- Continue = new("continue")
59
- Finish = new("finish")
60
- end
61
- end
62
-
63
- # Defines the signature for processing code execution results
64
- class RubyCodeObservation < DSPy::Signature
65
- description "Process the result of Ruby code execution and decide what to do next."
66
-
67
- input do
68
- const :task, String,
69
- description: "JSON representation of all input fields for the task"
70
- const :history, T::Array[CodeActHistoryEntry],
71
- description: "Previous thoughts, code executions, and their results"
72
- const :execution_result, T.nilable(String),
73
- description: "The result from executing the Ruby code"
74
- const :error_message, String,
75
- description: "Error message if the code execution failed (empty string if no error)"
76
- end
77
-
78
- output do
79
- const :observation, String,
80
- description: "Analysis of the execution result and what it means for solving the task"
81
- const :next_step, CodeActNextStep,
82
- description: "What to do next: '#{CodeActNextStep::Continue}' to continue with more code or '#{CodeActNextStep::Finish}' if the task is complete"
83
- const :final_answer, T.nilable(String),
84
- description: "If next_step is 'finish', provide the final answer to the task based on the execution results"
85
- end
86
- end
87
-
88
- # CodeAct Agent using Think-Code-Observe pattern
89
- class CodeAct < Predict
90
- extend T::Sig
91
- include Mixins::StructBuilder
92
-
93
- sig { returns(T.class_of(DSPy::Signature)) }
94
- attr_reader :original_signature_class
95
-
96
- sig { returns(T.class_of(T::Struct)) }
97
- attr_reader :enhanced_output_struct
98
-
99
- sig { returns(Integer) }
100
- attr_reader :max_iterations
101
-
102
- sig { returns(T::Hash[Symbol, T.untyped]) }
103
- attr_reader :execution_context
104
-
105
- sig { params(signature_class: T.class_of(DSPy::Signature), max_iterations: Integer).void }
106
- def initialize(signature_class, max_iterations: 10)
107
- @original_signature_class = signature_class
108
- @max_iterations = max_iterations
109
- @execution_context = T.let({}, T::Hash[Symbol, T.untyped])
110
-
111
- # Create code generator using Predict to preserve field descriptions
112
- @code_generator = T.let(DSPy::Predict.new(RubyCodeGeneration), DSPy::Predict)
113
-
114
- # Create observation processor using Predict to preserve field descriptions
115
- @observation_processor = T.let(DSPy::Predict.new(RubyCodeObservation), DSPy::Predict)
116
-
117
- # Create enhanced output struct with CodeAct fields
118
- @enhanced_output_struct = create_enhanced_output_struct(signature_class)
119
- enhanced_output_struct = @enhanced_output_struct
120
-
121
- # Create enhanced signature class
122
- enhanced_signature = Class.new(DSPy::Signature) do
123
- # Set the description
124
- description signature_class.description
125
-
126
- # Use the same input struct
127
- @input_struct_class = signature_class.input_struct_class
128
-
129
- # Use the enhanced output struct with CodeAct fields
130
- @output_struct_class = enhanced_output_struct
131
-
132
- # Store original signature name
133
- @original_signature_name = signature_class.name
134
-
135
- class << self
136
- attr_reader :input_struct_class, :output_struct_class, :original_signature_name
137
-
138
- # Override name to return the original signature name
139
- def name
140
- @original_signature_name || super
141
- end
142
- end
143
- end
144
-
145
- # Call parent constructor with enhanced signature
146
- super(enhanced_signature)
147
- end
148
-
149
- sig { override.returns(T::Array[[String, DSPy::Module]]) }
150
- def named_predictors
151
- pairs = T.let([], T::Array[[String, DSPy::Module]])
152
- pairs << ["code_generator", @code_generator]
153
- pairs << ["observation_processor", @observation_processor]
154
- pairs
155
- end
156
-
157
- sig { override.returns(T::Array[DSPy::Module]) }
158
- def predictors
159
- named_predictors.map { |(_, predictor)| predictor }
160
- end
161
-
162
- sig { params(kwargs: T.untyped).returns(T.untyped).override }
163
- def forward(**kwargs)
164
- # Validate input and serialize all fields as task context
165
- input_struct = @original_signature_class.input_struct_class.new(**kwargs)
166
- task = DSPy::TypeSerializer.serialize(input_struct).to_json
167
-
168
- # Execute CodeAct reasoning loop
169
- reasoning_result = execute_codeact_reasoning_loop(task)
170
-
171
- # Create enhanced output with all CodeAct data
172
- create_enhanced_result(kwargs, reasoning_result)
173
- end
174
-
175
- private
176
-
177
- # Executes the main CodeAct reasoning loop (Think-Code-Observe)
178
- sig { params(task: String).returns(T::Hash[Symbol, T.untyped]) }
179
- def execute_codeact_reasoning_loop(task)
180
- history = T.let([], T::Array[CodeActHistoryEntry])
181
- final_answer = T.let(nil, T.nilable(String))
182
- iterations_count = 0
183
- context = ""
184
-
185
- while should_continue_iteration?(iterations_count, final_answer)
186
- iterations_count += 1
187
-
188
- iteration_result = execute_single_iteration(
189
- task, history, context, iterations_count
190
- )
191
-
192
- if iteration_result[:should_finish]
193
- final_answer = iteration_result[:final_answer]
194
- break
195
- end
196
-
197
- history = iteration_result[:history]
198
- context = iteration_result[:context]
199
- end
200
-
201
- handle_max_iterations_if_needed(iterations_count, final_answer, history)
202
-
203
- {
204
- history: history,
205
- iterations: iterations_count,
206
- final_answer: final_answer || default_no_answer_message,
207
- execution_context: @execution_context
208
- }
209
- end
210
-
211
- # Executes a single iteration of the Think-Code-Observe loop
212
- sig { params(task: String, history: T::Array[CodeActHistoryEntry], context: String, iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
213
- def execute_single_iteration(task, history, context, iteration)
214
- DSPy::Context.with_span(
215
- operation: 'codeact.iteration',
216
- 'dspy.module' => 'CodeAct',
217
- 'codeact.iteration' => iteration,
218
- 'codeact.max_iterations' => @max_iterations,
219
- 'codeact.history_length' => history.length
220
- ) do
221
- execution_state = execute_think_code_step(task, context, history, iteration)
222
-
223
- observation_decision = process_observation_and_decide_next_step(
224
- task, execution_state[:history], execution_state[:execution_result],
225
- execution_state[:error_message], iteration
226
- )
227
-
228
- if observation_decision[:should_finish]
229
- return { should_finish: true, final_answer: observation_decision[:final_answer] }
230
- end
231
-
232
- finalize_iteration(execution_state, iteration)
233
- end
234
- end
235
-
236
- # Executes the Think-Code step: generates code and executes it
237
- sig { params(task: String, context: String, history: T::Array[CodeActHistoryEntry], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
238
- def execute_think_code_step(task, context, history, iteration)
239
- code_obj = @code_generator.forward(
240
- task: task,
241
- context: context.empty? ? "No previous context available." : context,
242
- history: history
243
- )
244
-
245
- execution_result, error_message = execute_ruby_code_with_instrumentation(
246
- code_obj.ruby_code, iteration
247
- )
248
-
249
- history << create_history_entry(
250
- iteration, code_obj.thought, code_obj.ruby_code,
251
- execution_result, error_message
252
- )
253
-
254
- {
255
- history: history,
256
- thought: code_obj.thought,
257
- ruby_code: code_obj.ruby_code,
258
- execution_result: execution_result,
259
- error_message: error_message
260
- }
261
- end
262
-
263
- # Finalizes iteration by updating context and emitting events
264
- sig { params(execution_state: T::Hash[Symbol, T.untyped], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
265
- def finalize_iteration(execution_state, iteration)
266
- new_context = build_context_from_history(execution_state[:history])
267
-
268
- emit_iteration_complete_event(
269
- iteration, execution_state[:thought], execution_state[:ruby_code],
270
- execution_state[:execution_result], execution_state[:error_message]
271
- )
272
-
273
- {
274
- should_finish: false,
275
- history: execution_state[:history],
276
- context: new_context
277
- }
278
- end
279
-
280
- # Creates enhanced output struct with CodeAct-specific fields
281
- sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(T::Struct)) }
282
- def create_enhanced_output_struct(signature_class)
283
- input_props = signature_class.input_struct_class.props
284
- output_props = signature_class.output_struct_class.props
285
-
286
- build_enhanced_struct(
287
- { input: input_props, output: output_props },
288
- {
289
- history: [T::Array[T::Hash[Symbol, T.untyped]], "CodeAct execution history"],
290
- iterations: [Integer, "Number of iterations executed"],
291
- execution_context: [T::Hash[Symbol, T.untyped], "Variables and context from code execution"]
292
- }
293
- )
294
- end
295
-
296
- # Creates enhanced result struct
297
- sig { params(input_kwargs: T::Hash[Symbol, T.untyped], reasoning_result: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
298
- def create_enhanced_result(input_kwargs, reasoning_result)
299
- output_field_name = @original_signature_class.output_struct_class.props.keys.first
300
-
301
- output_data = input_kwargs.merge({
302
- history: reasoning_result[:history].map(&:to_h),
303
- iterations: reasoning_result[:iterations],
304
- execution_context: reasoning_result[:execution_context]
305
- })
306
- output_data[output_field_name] = reasoning_result[:final_answer]
307
-
308
- @enhanced_output_struct.new(**output_data)
309
- end
310
-
311
- # Helper methods for CodeAct logic
312
- sig { params(iterations_count: Integer, final_answer: T.nilable(String)).returns(T::Boolean) }
313
- def should_continue_iteration?(iterations_count, final_answer)
314
- final_answer.nil? && (@max_iterations.nil? || iterations_count < @max_iterations)
315
- end
316
-
317
- sig { params(ruby_code: String, iteration: Integer).returns([T.nilable(String), String]) }
318
- def execute_ruby_code_with_instrumentation(ruby_code, iteration)
319
- DSPy::Context.with_span(
320
- operation: 'codeact.code_execution',
321
- 'dspy.module' => 'CodeAct',
322
- 'codeact.iteration' => iteration,
323
- 'code.length' => ruby_code.length
324
- ) do
325
- execute_ruby_code_safely(ruby_code)
326
- end
327
- end
328
-
329
- sig { params(step: Integer, thought: String, ruby_code: String, execution_result: T.nilable(String), error_message: String).returns(CodeActHistoryEntry) }
330
- def create_history_entry(step, thought, ruby_code, execution_result, error_message)
331
- CodeActHistoryEntry.new(
332
- step: step,
333
- thought: thought,
334
- ruby_code: ruby_code,
335
- execution_result: execution_result,
336
- error_message: error_message
337
- )
338
- end
339
-
340
- sig { params(task: String, history: T::Array[CodeActHistoryEntry], execution_result: T.nilable(String), error_message: String, iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
341
- def process_observation_and_decide_next_step(task, history, execution_result, error_message, iteration)
342
- observation_result = @observation_processor.forward(
343
- task: task,
344
- history: history,
345
- execution_result: execution_result,
346
- error_message: error_message
347
- )
348
-
349
- return { should_finish: false } unless observation_result.next_step == CodeActNextStep::Finish
350
-
351
- final_answer = observation_result.final_answer || execution_result || "Task completed"
352
-
353
- { should_finish: true, final_answer: final_answer }
354
- end
355
-
356
- sig { params(history: T::Array[CodeActHistoryEntry]).returns(String) }
357
- def build_context_from_history(history)
358
- context_parts = []
359
-
360
- history.each do |entry|
361
- if entry.execution_result && !entry.execution_result.empty?
362
- context_parts << "Step #{entry.step} result: #{entry.execution_result}"
363
- end
364
- end
365
-
366
- context_parts.join("\n")
367
- end
368
-
369
- sig { params(iteration: Integer, thought: String, ruby_code: String, execution_result: T.nilable(String), error_message: T.nilable(String)).void }
370
- def emit_iteration_complete_event(iteration, thought, ruby_code, execution_result, error_message)
371
- DSPy.event('codeact.iteration_complete', {
372
- 'codeact.iteration' => iteration,
373
- 'codeact.thought' => thought,
374
- 'codeact.ruby_code' => ruby_code,
375
- 'codeact.execution_result' => execution_result,
376
- 'codeact.error_message' => error_message,
377
- 'codeact.success' => error_message.nil?
378
- })
379
- end
380
-
381
- sig { params(iterations_count: Integer, final_answer: T.nilable(String), history: T::Array[CodeActHistoryEntry]).void }
382
- def handle_max_iterations_if_needed(iterations_count, final_answer, history)
383
- if iterations_count >= @max_iterations && final_answer.nil?
384
- DSPy.event('codeact.max_iterations', {
385
- 'codeact.iteration_count' => iterations_count,
386
- 'codeact.max_iterations' => @max_iterations,
387
- 'codeact.final_history_length' => history.length
388
- })
389
- end
390
- end
391
-
392
- sig { returns(String) }
393
- def default_no_answer_message
394
- "No solution reached within #{@max_iterations} iterations"
395
- end
396
-
397
- # Safe Ruby code execution method - placeholder for now
398
- sig { params(ruby_code: String).returns([T.nilable(String), String]) }
399
- def execute_ruby_code_safely(ruby_code)
400
- # TODO: Implement proper sandboxing in Phase 2
401
- # For now, use basic eval with error handling
402
- original_stdout = nil
403
- captured_output = nil
404
-
405
- begin
406
- # Capture stdout to get print/puts output
407
- original_stdout = $stdout
408
- captured_output = StringIO.new
409
- $stdout = captured_output
410
-
411
- result = eval(ruby_code, binding)
412
-
413
- # Get the captured output
414
- output = captured_output.string
415
-
416
- # If there's captured output, use it, otherwise use the eval result
417
- final_result = output.empty? ? result.to_s : output.chomp
418
-
419
- [final_result, ""]
420
- rescue SyntaxError => e
421
- [nil, "Error: #{e.message}"]
422
- rescue => e
423
- [nil, "Error: #{e.message}"]
424
- ensure
425
- $stdout = original_stdout if original_stdout
426
- end
427
- end
428
-
429
- sig { params(output: T.untyped).void }
430
- def validate_output_schema!(output)
431
- # Validate that output is an instance of the enhanced output struct
432
- unless output.is_a?(@enhanced_output_struct)
433
- raise "Output must be an instance of #{@enhanced_output_struct}, got #{output.class}"
434
- end
435
-
436
- # Validate original signature output fields are present
437
- @original_signature_class.output_struct_class.props.each do |field_name, _prop|
438
- unless output.respond_to?(field_name)
439
- raise "Missing required field: #{field_name}"
440
- end
441
- end
442
-
443
- # Validate CodeAct-specific fields
444
- unless output.respond_to?(:history) && output.history.is_a?(Array)
445
- raise "Missing or invalid history field"
446
- end
447
-
448
- unless output.respond_to?(:iterations) && output.iterations.is_a?(Integer)
449
- raise "Missing or invalid iterations field"
450
- end
451
-
452
- unless output.respond_to?(:execution_context) && output.execution_context.is_a?(Hash)
453
- raise "Missing or invalid execution_context field"
454
- end
455
- end
456
-
457
- sig { returns(T::Hash[Symbol, T.untyped]) }
458
- def generate_example_output
459
- # Create a base example structure
460
- example = {}
461
-
462
- # Add CodeAct-specific example data
463
- example[:history] = [
464
- {
465
- step: 1,
466
- thought: "I need to write Ruby code to solve this task...",
467
- ruby_code: "result = 2 + 2",
468
- execution_result: "4",
469
- error_message: nil
470
- }
471
- ]
472
- example[:iterations] = 1
473
- example[:execution_context] = { result: 4 }
474
- example
475
- end
476
- end
477
- end