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 +4 -4
- data/lib/dspy/mixins/type_coercion.rb +66 -3
- data/lib/dspy/predict.rb +13 -9
- data/lib/dspy/re_act.rb +89 -32
- data/lib/dspy/signature.rb +12 -0
- data/lib/dspy/tools/github_cli_toolset.rb +5 -112
- data/lib/dspy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d697eb8eb574ca5c23914c1911f1d7a03ad7411aa83b19bedf2231cacc544460
|
4
|
+
data.tar.gz: 3086cbaa86d01b0dd09512c9f5893f8a31b8d9988eed6782a967c24e1c12fb01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
41
|
-
when
|
40
|
+
coerce_enum_value(value, prop_type)
|
41
|
+
when ->(type) { type == Float || simple_type_match?(type, Float) }
|
42
42
|
value.to_f
|
43
|
-
when
|
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
|
-
#
|
141
|
-
|
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(
|
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,
|
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(
|
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
|
-
|
192
|
+
description: "Serialized representation of all input fields"
|
158
193
|
const :history, T::Array[HistoryEntry],
|
159
|
-
|
160
|
-
const :available_tools, T::Array[
|
161
|
-
|
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
|
-
|
168
|
-
const :action,
|
169
|
-
|
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
|
-
|
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
|
-
|
222
|
+
description: "Serialized representation of all input fields"
|
188
223
|
const :history, T::Array[HistoryEntry],
|
189
|
-
|
224
|
+
description: "Previous thoughts, actions, and observations."
|
190
225
|
const :observation, String,
|
191
|
-
|
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
|
-
|
232
|
+
description: "Interpretation of the observation"
|
198
233
|
const :next_step, NextStep,
|
199
|
-
|
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|
|
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[
|
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 <<
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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' =>
|
415
|
+
'tool.name' => action_str.downcase,
|
363
416
|
'tool.input' => action_input
|
364
417
|
) do
|
365
|
-
execute_action(
|
418
|
+
execute_action(action_str, action_input)
|
366
419
|
end
|
367
420
|
else
|
368
|
-
"Unknown action: #{
|
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[
|
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[
|
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
|
-
|
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:
|
589
|
+
action: action_str,
|
533
590
|
action_input: final_answer,
|
534
591
|
observation: nil # No observation for finish action
|
535
592
|
)
|
data/lib/dspy/signature.rb
CHANGED
@@ -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
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.
|
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-
|
10
|
+
date: 2025-09-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-configurable
|