dspy 0.27.2 → 0.27.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07ebaf2db9b6279275ac4dd61ac4f994ed5415561b0063310f006db084306092
4
- data.tar.gz: 8e8c1cbfcb36d9ffa4bfd68f62317fba7d5d32a6b1409ce43749f555a719fc97
3
+ metadata.gz: d697eb8eb574ca5c23914c1911f1d7a03ad7411aa83b19bedf2231cacc544460
4
+ data.tar.gz: 3086cbaa86d01b0dd09512c9f5893f8a31b8d9988eed6782a967c24e1c12fb01
5
5
  SHA512:
6
- metadata.gz: 8f19c7e01b5e03743457c9784b8cca05ef42e8002ff210dd59f08915f11ceb7c1c000f8b0eac6cd102c847f069194923a46c5197473ea53cda7616921ea1da26
7
- data.tar.gz: 9ed8e5ec4e08eb83f17dbea912a7a14b28212968f36dc7adf6cad7dd7e62de8506e371e99bb4f8f1d785847b45783fdfada53dae3d1d039a4f8695aba54984b4
6
+ metadata.gz: eae9e4cba177e6cea359f1ffd55ebaa4203cc5a6594b86ab5fc2b9b9e8c54cf24838a8d18102ab96fc9a8f4b85827c3cb02ef0b75c3d077c6c70301abb52f48d
7
+ data.tar.gz: ecc26be5f85df66e911a71d5a1fa878cc42c79c7d63e42b2d1440836859814837f4ba782db18584161598867a5aef5ebbfef056a2988b4208767b8e0c1999013
@@ -37,11 +37,17 @@ module DSPy
37
37
  when ->(type) { hash_type?(type) }
38
38
  coerce_hash_value(value, prop_type)
39
39
  when ->(type) { enum_type?(type) }
40
- extract_enum_class(prop_type).deserialize(value)
41
- when Float, ->(type) { simple_type_match?(type, Float) }
40
+ coerce_enum_value(value, prop_type)
41
+ when ->(type) { type == Float || simple_type_match?(type, Float) }
42
42
  value.to_f
43
- when Integer, ->(type) { simple_type_match?(type, Integer) }
43
+ when ->(type) { type == Integer || simple_type_match?(type, Integer) }
44
44
  value.to_i
45
+ when ->(type) { type == Date || simple_type_match?(type, Date) }
46
+ coerce_date_value(value)
47
+ when ->(type) { type == DateTime || simple_type_match?(type, DateTime) }
48
+ coerce_datetime_value(value)
49
+ when ->(type) { type == Time || simple_type_match?(type, Time) }
50
+ coerce_time_value(value)
45
51
  when ->(type) { struct_type?(type) }
46
52
  coerce_struct_value(value, prop_type)
47
53
  else
@@ -244,6 +250,63 @@ module DSPy
244
250
  DSPy.logger.debug("Failed to coerce union type: #{e.message}")
245
251
  value
246
252
  end
253
+
254
+ # Coerces a date value from string using ISO 8601 format
255
+ sig { params(value: T.untyped).returns(T.nilable(Date)) }
256
+ def coerce_date_value(value)
257
+ return value if value.is_a?(Date)
258
+ return nil if value.nil? || value.to_s.strip.empty?
259
+
260
+ # Support ISO 8601 format (YYYY-MM-DD) like ActiveRecord
261
+ Date.parse(value.to_s)
262
+ rescue ArgumentError, TypeError
263
+ # Return nil for invalid dates rather than crashing
264
+ DSPy.logger.debug("Failed to coerce to Date: #{value}")
265
+ nil
266
+ end
267
+
268
+ # Coerces a datetime value from string using ISO 8601 format with timezone
269
+ sig { params(value: T.untyped).returns(T.nilable(DateTime)) }
270
+ def coerce_datetime_value(value)
271
+ return value if value.is_a?(DateTime)
272
+ return nil if value.nil? || value.to_s.strip.empty?
273
+
274
+ # Parse ISO 8601 with timezone like ActiveRecord
275
+ # Formats: 2024-01-15T10:30:45Z, 2024-01-15T10:30:45+00:00, 2024-01-15 10:30:45
276
+ DateTime.parse(value.to_s)
277
+ rescue ArgumentError, TypeError
278
+ DSPy.logger.debug("Failed to coerce to DateTime: #{value}")
279
+ nil
280
+ end
281
+
282
+ # Coerces a time value from string, converting to UTC like ActiveRecord
283
+ sig { params(value: T.untyped).returns(T.nilable(Time)) }
284
+ def coerce_time_value(value)
285
+ return value if value.is_a?(Time)
286
+ return nil if value.nil? || value.to_s.strip.empty?
287
+
288
+ # Parse and convert to UTC (like ActiveRecord with time_zone_aware_attributes)
289
+ # This ensures consistent timezone handling across the system
290
+ Time.parse(value.to_s).utc
291
+ rescue ArgumentError, TypeError
292
+ DSPy.logger.debug("Failed to coerce to Time: #{value}")
293
+ nil
294
+ end
295
+
296
+ # Coerces a value to an enum, handling both strings and existing enum instances
297
+ sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
298
+ def coerce_enum_value(value, prop_type)
299
+ enum_class = extract_enum_class(prop_type)
300
+
301
+ # If value is already an instance of the enum class, return it as-is
302
+ return value if value.is_a?(enum_class)
303
+
304
+ # Otherwise, try to deserialize from string
305
+ enum_class.deserialize(value.to_s)
306
+ rescue ArgumentError, KeyError => e
307
+ DSPy.logger.debug("Failed to coerce to enum #{enum_class}: #{e.message}")
308
+ value
309
+ end
247
310
  end
248
311
  end
249
312
  end
data/lib/dspy/predict.rb CHANGED
@@ -137,11 +137,15 @@ module DSPy
137
137
  def forward_untyped(**input_values)
138
138
  # Module#forward handles span creation, we just do the prediction logic
139
139
 
140
- # Store input values for optimization
141
- @last_input_values = input_values.clone
140
+ # Apply type coercion to input values first
141
+ input_props = @signature_class.input_struct_class.props
142
+ coerced_input_values = coerce_output_attributes(input_values, input_props)
143
+
144
+ # Store coerced input values for optimization
145
+ @last_input_values = coerced_input_values.clone
142
146
 
143
- # Validate input
144
- validate_input_struct(input_values)
147
+ # Validate input with coerced values
148
+ validate_input_struct(coerced_input_values)
145
149
 
146
150
  # Check if LM is configured
147
151
  current_lm = lm
@@ -149,19 +153,19 @@ module DSPy
149
153
  raise DSPy::ConfigurationError.missing_lm(self.class.name)
150
154
  end
151
155
 
152
- # Call LM and process response
153
- output_attributes = current_lm.chat(self, input_values)
156
+ # Call LM and process response with coerced input values
157
+ output_attributes = current_lm.chat(self, coerced_input_values)
154
158
  processed_output = process_lm_output(output_attributes)
155
159
 
156
- # Create combined result struct
157
- prediction_result = create_prediction_result(input_values, processed_output)
160
+ # Create combined result struct with coerced input values
161
+ prediction_result = create_prediction_result(coerced_input_values, processed_output)
158
162
 
159
163
  prediction_result
160
164
  end
161
165
 
162
166
  private
163
167
 
164
- # Validates input using signature struct
168
+ # Validates input using signature struct (assumes input is already coerced)
165
169
  sig { params(input_values: T::Hash[Symbol, T.untyped]).void }
166
170
  def validate_input_struct(input_values)
167
171
  @signature_class.input_struct_class.new(**input_values)
data/lib/dspy/re_act.rb CHANGED
@@ -66,6 +66,13 @@ module DSPy
66
66
  extend T::Sig
67
67
  include Mixins::StructBuilder
68
68
 
69
+ # AvailableTool struct for better type safety in ReAct agents
70
+ class AvailableTool < T::Struct
71
+ const :name, String
72
+ const :description, String
73
+ const :schema, T::Hash[Symbol, T.untyped]
74
+ end
75
+
69
76
  FINISH_ACTION = "finish"
70
77
  sig { returns(T.class_of(DSPy::Signature)) }
71
78
  attr_reader :original_signature_class
@@ -87,6 +94,9 @@ module DSPy
87
94
  tools.each { |tool| @tools[tool.name.downcase] = tool }
88
95
  @max_iterations = max_iterations
89
96
 
97
+ # Create dynamic ActionEnum class with tool names + finish
98
+ @action_enum_class = create_action_enum_class
99
+
90
100
  # Create dynamic signature classes that include the original input fields
91
101
  thought_signature = create_thought_signature(signature_class)
92
102
  observation_signature = create_observation_signature(signature_class)
@@ -143,9 +153,34 @@ module DSPy
143
153
 
144
154
  private
145
155
 
156
+ # Creates a dynamic ActionEnum class with tool names and "finish"
157
+ sig { returns(T.class_of(T::Enum)) }
158
+ def create_action_enum_class
159
+ tool_names = @tools.keys
160
+ all_actions = tool_names + [FINISH_ACTION]
161
+
162
+ # Create a dynamic enum class using proper T::Enum pattern
163
+ enum_class = Class.new(T::Enum)
164
+
165
+ # Build the enums block code dynamically
166
+ enum_definitions = all_actions.map do |action_name|
167
+ const_name = action_name.upcase.gsub(/[^A-Z0-9_]/, '_')
168
+ "#{const_name} = new(#{action_name.inspect})"
169
+ end.join("\n ")
170
+
171
+ enum_class.class_eval <<~RUBY
172
+ enums do
173
+ #{enum_definitions}
174
+ end
175
+ RUBY
176
+
177
+ enum_class
178
+ end
179
+
146
180
  # Creates a dynamic Thought signature that includes the original input fields
147
181
  sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
148
182
  def create_thought_signature(signature_class)
183
+ action_enum_class = @action_enum_class
149
184
  # Create new class that inherits from DSPy::Signature
150
185
  Class.new(DSPy::Signature) do
151
186
  # Set description
@@ -154,21 +189,21 @@ module DSPy
154
189
  # Define input fields
155
190
  input do
156
191
  const :input_context, String,
157
- desc: "Serialized representation of all input fields"
192
+ description: "Serialized representation of all input fields"
158
193
  const :history, T::Array[HistoryEntry],
159
- desc: "Previous thoughts and actions, including observations from tools."
160
- const :available_tools, T::Array[T::Hash[String, T.untyped]],
161
- desc: "Array of available tools with their JSON schemas."
194
+ description: "Previous thoughts and actions, including observations from tools."
195
+ const :available_tools, T::Array[AvailableTool],
196
+ description: "Array of available tools with their JSON schemas."
162
197
  end
163
198
 
164
199
  # Define output fields (same as ThoughtBase)
165
200
  output do
166
201
  const :thought, String,
167
- desc: "Reasoning about what to do next, considering the history and observations."
168
- const :action, String,
169
- desc: "The action to take. MUST be one of the tool names listed in `available_tools` input, or the literal string \"finish\" to provide the final answer."
202
+ description: "Reasoning about what to do next, considering the history and observations."
203
+ const :action, action_enum_class,
204
+ description: "The action to take. MUST be one of the tool names listed in `available_tools` input, or the literal string \"finish\" to provide the final answer."
170
205
  const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
171
- desc: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final result based on processing the input data."
206
+ description: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final result based on processing the input data."
172
207
  end
173
208
  end
174
209
  end
@@ -184,19 +219,19 @@ module DSPy
184
219
  # Define input fields
185
220
  input do
186
221
  const :input_context, String,
187
- desc: "Serialized representation of all input fields"
222
+ description: "Serialized representation of all input fields"
188
223
  const :history, T::Array[HistoryEntry],
189
- desc: "Previous thoughts, actions, and observations."
224
+ description: "Previous thoughts, actions, and observations."
190
225
  const :observation, String,
191
- desc: "The result from the last action"
226
+ description: "The result from the last action"
192
227
  end
193
228
 
194
229
  # Define output fields (same as ReActObservationBase)
195
230
  output do
196
231
  const :interpretation, String,
197
- desc: "Interpretation of the observation"
232
+ description: "Interpretation of the observation"
198
233
  const :next_step, NextStep,
199
- desc: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
234
+ description: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
200
235
  end
201
236
  end
202
237
  end
@@ -205,7 +240,14 @@ module DSPy
205
240
  sig { params(input_struct: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
206
241
  def execute_react_reasoning_loop(input_struct)
207
242
  history = T.let([], T::Array[HistoryEntry])
208
- available_tools_desc = @tools.map { |name, tool| JSON.parse(tool.schema) }
243
+ available_tools_desc = @tools.map { |name, tool|
244
+ schema = JSON.parse(tool.schema)
245
+ AvailableTool.new(
246
+ name: name,
247
+ description: tool.description,
248
+ schema: schema.transform_keys(&:to_sym)
249
+ )
250
+ }
209
251
  final_answer = T.let(nil, T.nilable(String))
210
252
  iterations_count = 0
211
253
  last_observation = T.let(nil, T.nilable(String))
@@ -239,7 +281,7 @@ module DSPy
239
281
  end
240
282
 
241
283
  # Executes a single iteration of the ReAct loop
242
- sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
284
+ sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
243
285
  def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
244
286
  # Track each iteration with agent span
245
287
  DSPy::Context.with_span(
@@ -272,12 +314,15 @@ module DSPy
272
314
  thought_obj.action, thought_obj.action_input, iteration
273
315
  )
274
316
 
317
+ # Convert action enum to string for processing and storage
318
+ action_str = thought_obj.action.respond_to?(:serialize) ? thought_obj.action.serialize : thought_obj.action.to_s
319
+
275
320
  # Track tools used
276
- tools_used << thought_obj.action.downcase if valid_tool?(thought_obj.action)
321
+ tools_used << action_str.downcase if valid_tool?(thought_obj.action)
277
322
 
278
323
  # Add to history
279
324
  history << create_history_entry(
280
- iteration, thought_obj.thought, thought_obj.action,
325
+ iteration, thought_obj.thought, action_str,
281
326
  thought_obj.action_input, observation
282
327
  )
283
328
 
@@ -291,7 +336,7 @@ module DSPy
291
336
  end
292
337
 
293
338
  emit_iteration_complete_event(
294
- iteration, thought_obj.thought, thought_obj.action,
339
+ iteration, thought_obj.thought, action_str,
295
340
  thought_obj.action_input, observation, tools_used
296
341
  )
297
342
 
@@ -341,31 +386,39 @@ module DSPy
341
386
  final_answer.nil? && (@max_iterations.nil? || iterations_count < @max_iterations)
342
387
  end
343
388
 
344
- sig { params(action: T.nilable(String)).returns(T::Boolean) }
389
+ sig { params(action: T.nilable(T.any(String, T::Enum))).returns(T::Boolean) }
345
390
  def finish_action?(action)
346
- action&.downcase == FINISH_ACTION
391
+ return false unless action
392
+ action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
393
+ action_str.downcase == FINISH_ACTION
347
394
  end
348
395
 
349
- sig { params(action: T.nilable(String)).returns(T::Boolean) }
396
+ sig { params(action: T.nilable(T.any(String, T::Enum))).returns(T::Boolean) }
350
397
  def valid_tool?(action)
351
- !!(action && @tools[action.downcase])
398
+ return false unless action
399
+ action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
400
+ !!@tools[action_str.downcase]
352
401
  end
353
402
 
354
- sig { params(action: T.nilable(String), action_input: T.untyped, iteration: Integer).returns(String) }
403
+ sig { params(action: T.nilable(T.any(String, T::Enum)), action_input: T.untyped, iteration: Integer).returns(String) }
355
404
  def execute_tool_with_instrumentation(action, action_input, iteration)
356
- if action && @tools[action.downcase]
405
+ return "Unknown action: #{action}. Available actions: #{@tools.keys.join(', ')}, finish" unless action
406
+
407
+ action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
408
+
409
+ if @tools[action_str.downcase]
357
410
  DSPy::Context.with_span(
358
411
  operation: 'react.tool_call',
359
412
  **DSPy::ObservationType::Tool.langfuse_attributes,
360
413
  'dspy.module' => 'ReAct',
361
414
  'react.iteration' => iteration,
362
- 'tool.name' => action.downcase,
415
+ 'tool.name' => action_str.downcase,
363
416
  'tool.input' => action_input
364
417
  ) do
365
- execute_action(action, action_input)
418
+ execute_action(action_str, action_input)
366
419
  end
367
420
  else
368
- "Unknown action: #{action}. Available actions: #{@tools.keys.join(', ')}, finish"
421
+ "Unknown action: #{action_str}. Available actions: #{@tools.keys.join(', ')}, finish"
369
422
  end
370
423
  end
371
424
 
@@ -380,7 +433,7 @@ module DSPy
380
433
  )
381
434
  end
382
435
 
383
- sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
436
+ sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[AvailableTool], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
384
437
  def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
385
438
  return { should_finish: false } if observation.include?("Unknown action")
386
439
 
@@ -399,7 +452,7 @@ module DSPy
399
452
  { should_finish: true, final_answer: final_answer }
400
453
  end
401
454
 
402
- sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], observation_result: T.untyped, iteration: Integer).returns(String) }
455
+ sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[AvailableTool], observation_result: T.untyped, iteration: Integer).returns(String) }
403
456
  def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
404
457
  final_thought = @thought_generator.forward(
405
458
  input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
@@ -407,7 +460,8 @@ module DSPy
407
460
  available_tools: available_tools_desc
408
461
  )
409
462
 
410
- if final_thought.action&.downcase != FINISH_ACTION
463
+ action_str = final_thought.action.respond_to?(:serialize) ? final_thought.action.serialize : final_thought.action.to_s
464
+ if action_str.downcase != FINISH_ACTION
411
465
  forced_answer = if observation_result.interpretation && !observation_result.interpretation.empty?
412
466
  observation_result.interpretation
413
467
  else
@@ -516,7 +570,7 @@ module DSPy
516
570
  example
517
571
  end
518
572
 
519
- sig { params(action_input: T.untyped, last_observation: T.nilable(String), step: Integer, thought: String, action: String, history: T::Array[HistoryEntry]).returns(String) }
573
+ sig { params(action_input: T.untyped, last_observation: T.nilable(String), step: Integer, thought: String, action: T.any(String, T::Enum), history: T::Array[HistoryEntry]).returns(String) }
520
574
  def handle_finish_action(action_input, last_observation, step, thought, action, history)
521
575
  final_answer = action_input.to_s
522
576
 
@@ -525,11 +579,14 @@ module DSPy
525
579
  final_answer = last_observation
526
580
  end
527
581
 
582
+ # Convert action enum to string for storage in history
583
+ action_str = action.respond_to?(:serialize) ? action.serialize : action.to_s
584
+
528
585
  # Always add the finish action to history
529
586
  history << HistoryEntry.new(
530
587
  step: step,
531
588
  thought: thought,
532
- action: action,
589
+ action: action_str,
533
590
  action_input: final_answer,
534
591
  observation: nil # No observation for finish action
535
592
  )
@@ -207,6 +207,12 @@ module DSPy
207
207
  { type: "number" }
208
208
  elsif type == Numeric
209
209
  { type: "number" }
210
+ elsif type == Date
211
+ { type: "string", format: "date" }
212
+ elsif type == DateTime
213
+ { type: "string", format: "date-time" }
214
+ elsif type == Time
215
+ { type: "string", format: "date-time" }
210
216
  elsif [TrueClass, FalseClass].include?(type)
211
217
  { type: "boolean" }
212
218
  elsif type < T::Struct
@@ -225,6 +231,12 @@ module DSPy
225
231
  { type: "number" }
226
232
  when "Numeric"
227
233
  { type: "number" }
234
+ when "Date"
235
+ { type: "string", format: "date" }
236
+ when "DateTime"
237
+ { type: "string", format: "date-time" }
238
+ when "Time"
239
+ { type: "string", format: "date-time" }
228
240
  when "TrueClass", "FalseClass"
229
241
  { type: "boolean" }
230
242
  when "T::Boolean"
@@ -61,14 +61,10 @@ module DSPy
61
61
  toolset_name "github"
62
62
 
63
63
  # Expose methods as tools with descriptions
64
- tool :create_issue, description: "Create a new GitHub issue"
65
- tool :create_pr, description: "Create a new GitHub pull request"
66
64
  tool :list_issues, description: "List GitHub issues with optional filters"
67
65
  tool :list_prs, description: "List GitHub pull requests with optional filters"
68
66
  tool :get_issue, description: "Get details of a specific GitHub issue"
69
67
  tool :get_pr, description: "Get details of a specific GitHub pull request"
70
- tool :comment_on_issue, description: "Add a comment to a GitHub issue"
71
- tool :review_pr, description: "Add a review to a GitHub pull request"
72
68
  tool :api_request, description: "Make an arbitrary GitHub API request"
73
69
 
74
70
  sig { void }
@@ -76,64 +72,7 @@ module DSPy
76
72
  # No persistent state needed
77
73
  end
78
74
 
79
- sig { params(
80
- title: String,
81
- body: String,
82
- labels: T::Array[String],
83
- assignees: T::Array[String],
84
- repo: T.nilable(String)
85
- ).returns(String) }
86
- def create_issue(title:, body:, labels: [], assignees: [], repo: nil)
87
- cmd = build_gh_command(['issue', 'create'])
88
- cmd << ['--title', shell_escape(title)]
89
- cmd << ['--body', shell_escape(body)]
90
-
91
- labels.each { |label| cmd << ['--label', shell_escape(label)] }
92
- assignees.each { |assignee| cmd << ['--assignee', shell_escape(assignee)] }
93
-
94
- if repo
95
- cmd << ['--repo', shell_escape(repo)]
96
- end
97
-
98
- result = execute_command(cmd.flatten.join(' '))
99
-
100
- if result[:success]
101
- "Issue created successfully: #{result[:output].strip}"
102
- else
103
- "Failed to create issue: #{result[:error]}"
104
- end
105
- rescue => e
106
- "Error creating issue: #{e.message}"
107
- end
108
75
 
109
- sig { params(
110
- title: String,
111
- body: String,
112
- base: String,
113
- head: String,
114
- repo: T.nilable(String)
115
- ).returns(String) }
116
- def create_pr(title:, body:, base:, head:, repo: nil)
117
- cmd = build_gh_command(['pr', 'create'])
118
- cmd << ['--title', shell_escape(title)]
119
- cmd << ['--body', shell_escape(body)]
120
- cmd << ['--base', shell_escape(base)]
121
- cmd << ['--head', shell_escape(head)]
122
-
123
- if repo
124
- cmd << ['--repo', shell_escape(repo)]
125
- end
126
-
127
- result = execute_command(cmd.flatten.join(' '))
128
-
129
- if result[:success]
130
- "Pull request created successfully: #{result[:output].strip}"
131
- else
132
- "Failed to create pull request: #{result[:error]}"
133
- end
134
- rescue => e
135
- "Error creating pull request: #{e.message}"
136
- end
137
76
 
138
77
  sig { params(
139
78
  state: IssueState,
@@ -241,58 +180,7 @@ module DSPy
241
180
  "Error getting pull request: #{e.message}"
242
181
  end
243
182
 
244
- sig { params(
245
- issue_number: Integer,
246
- comment: String,
247
- repo: T.nilable(String)
248
- ).returns(String) }
249
- def comment_on_issue(issue_number:, comment:, repo: nil)
250
- cmd = build_gh_command(['issue', 'comment', issue_number.to_s])
251
- cmd << ['--body', shell_escape(comment)]
252
-
253
- if repo
254
- cmd << ['--repo', shell_escape(repo)]
255
- end
256
183
 
257
- result = execute_command(cmd.flatten.join(' '))
258
-
259
- if result[:success]
260
- "Comment added successfully to issue ##{issue_number}"
261
- else
262
- "Failed to add comment: #{result[:error]}"
263
- end
264
- rescue => e
265
- "Error adding comment: #{e.message}"
266
- end
267
-
268
- sig { params(
269
- pr_number: Integer,
270
- review_type: ReviewState,
271
- comment: T.nilable(String),
272
- repo: T.nilable(String)
273
- ).returns(String) }
274
- def review_pr(pr_number:, review_type:, comment: nil, repo: nil)
275
- cmd = build_gh_command(['pr', 'review', pr_number.to_s])
276
- cmd << ['--' + review_type.serialize.tr('_', '-')]
277
-
278
- if comment
279
- cmd << ['--body', shell_escape(comment)]
280
- end
281
-
282
- if repo
283
- cmd << ['--repo', shell_escape(repo)]
284
- end
285
-
286
- result = execute_command(cmd.flatten.join(' '))
287
-
288
- if result[:success]
289
- "Review added successfully to PR ##{pr_number}"
290
- else
291
- "Failed to add review: #{result[:error]}"
292
- end
293
- rescue => e
294
- "Error adding review: #{e.message}"
295
- end
296
184
 
297
185
  sig { params(
298
186
  endpoint: String,
@@ -301,6 +189,11 @@ module DSPy
301
189
  repo: T.nilable(String)
302
190
  ).returns(String) }
303
191
  def api_request(endpoint:, method: 'GET', fields: {}, repo: nil)
192
+ # Restrict to read-only operations
193
+ unless method.upcase == 'GET'
194
+ return "Error: Only GET requests are allowed for read-only access"
195
+ end
196
+
304
197
  cmd = build_gh_command(['api', endpoint])
305
198
  cmd << ['--method', method.upcase]
306
199
 
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.27.2"
4
+ VERSION = "0.27.3"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.2
4
+ version: 0.27.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-16 00:00:00.000000000 Z
10
+ date: 2025-09-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable