dspy 0.11.0 → 0.12.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 +3 -0
- data/lib/dspy/lm/message_builder.rb +28 -0
- data/lib/dspy/lm.rb +101 -36
- data/lib/dspy/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d40d9681a87073c6c4f5531025891bf871bee086434ee131e15a4391cba7b893
|
4
|
+
data.tar.gz: 1a1a4ce935e7cdf4ce9c06617f124ed8858d7074a3f6bec4484d6db24dbfa232
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cdd3ee7e867344b4e45dee7fd0d73dc875ec2c730a2cf75e5f620a19c535f0aac50cc6c88dfb7beb3e075b147e7467ccd4e55a7724f49f3ac111eb080fd88e0
|
7
|
+
data.tar.gz: f5c5ed5dd3b6aa5401c20e31fbbd9638eb1d8c0f3b7fe5adb2e2b69bbb3b051d0ecd277d6e2f3b4d4896fd19f39a89345c9503d97106a3b8e005b1f55a3e5b2b
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# DSPy.rb
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/dspy)
|
4
|
+
[](https://rubygems.org/gems/dspy)
|
5
|
+
|
3
6
|
**Build reliable LLM applications in Ruby using composable, type-safe modules.**
|
4
7
|
|
5
8
|
DSPy.rb brings structured LLM programming to Ruby developers. Instead of wrestling with prompt strings and parsing responses, you define typed signatures and compose them into pipelines that just work.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DSPy
|
4
|
+
class LM
|
5
|
+
class MessageBuilder
|
6
|
+
attr_reader :messages
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@messages = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def system(content)
|
13
|
+
@messages << { role: 'system', content: content.to_s }
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def user(content)
|
18
|
+
@messages << { role: 'user', content: content.to_s }
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def assistant(content)
|
23
|
+
@messages << { role: 'assistant', content: content.to_s }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/dspy/lm.rb
CHANGED
@@ -18,6 +18,9 @@ require_relative 'lm/adapters/anthropic_adapter'
|
|
18
18
|
require_relative 'lm/strategy_selector'
|
19
19
|
require_relative 'lm/retry_handler'
|
20
20
|
|
21
|
+
# Load message builder
|
22
|
+
require_relative 'lm/message_builder'
|
23
|
+
|
21
24
|
module DSPy
|
22
25
|
class LM
|
23
26
|
attr_reader :model_id, :api_key, :model, :provider, :adapter
|
@@ -39,41 +42,13 @@ module DSPy
|
|
39
42
|
# Build messages from inference module
|
40
43
|
messages = build_messages(inference_module, input_values)
|
41
44
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# Use smart consolidation: emit LM events only when not in nested context
|
47
|
-
response = nil
|
48
|
-
token_usage = {}
|
45
|
+
# Execute with instrumentation
|
46
|
+
response = instrument_lm_request(messages, signature_class.name) do
|
47
|
+
chat_with_strategy(messages, signature_class, &block)
|
48
|
+
end
|
49
49
|
|
50
|
+
# Instrument response parsing
|
50
51
|
if should_emit_lm_events?
|
51
|
-
# Emit all LM events when not in nested context
|
52
|
-
response = Instrumentation.instrument('dspy.lm.request', {
|
53
|
-
gen_ai_operation_name: 'chat',
|
54
|
-
gen_ai_system: provider,
|
55
|
-
gen_ai_request_model: model,
|
56
|
-
signature_class: signature_class.name,
|
57
|
-
provider: provider,
|
58
|
-
adapter_class: adapter.class.name,
|
59
|
-
input_size: input_size
|
60
|
-
}) do
|
61
|
-
chat_with_strategy(messages, signature_class, &block)
|
62
|
-
end
|
63
|
-
|
64
|
-
# Extract actual token usage from response (more accurate than estimation)
|
65
|
-
token_usage = Instrumentation::TokenTracker.extract_token_usage(response, provider)
|
66
|
-
|
67
|
-
# Emit token usage event if available
|
68
|
-
if token_usage.any?
|
69
|
-
Instrumentation.emit('dspy.lm.tokens', token_usage.merge({
|
70
|
-
gen_ai_system: provider,
|
71
|
-
gen_ai_request_model: model,
|
72
|
-
signature_class: signature_class.name
|
73
|
-
}))
|
74
|
-
end
|
75
|
-
|
76
|
-
# Instrument response parsing
|
77
52
|
parsed_result = Instrumentation.instrument('dspy.lm.response.parsed', {
|
78
53
|
signature_class: signature_class.name,
|
79
54
|
provider: provider,
|
@@ -82,15 +57,33 @@ module DSPy
|
|
82
57
|
parse_response(response, input_values, signature_class)
|
83
58
|
end
|
84
59
|
else
|
85
|
-
# Consolidated mode: execute without nested instrumentation
|
86
|
-
response = chat_with_strategy(messages, signature_class, &block)
|
87
|
-
token_usage = Instrumentation::TokenTracker.extract_token_usage(response, provider)
|
88
60
|
parsed_result = parse_response(response, input_values, signature_class)
|
89
61
|
end
|
90
62
|
|
91
63
|
parsed_result
|
92
64
|
end
|
93
65
|
|
66
|
+
def raw_chat(messages = nil, &block)
|
67
|
+
# Support both array format and builder DSL
|
68
|
+
if block_given? && messages.nil?
|
69
|
+
# DSL mode - block is for building messages
|
70
|
+
builder = MessageBuilder.new
|
71
|
+
yield builder
|
72
|
+
messages = builder.messages
|
73
|
+
streaming_block = nil
|
74
|
+
else
|
75
|
+
# Array mode - block is for streaming
|
76
|
+
messages ||= []
|
77
|
+
streaming_block = block
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate messages format
|
81
|
+
validate_messages!(messages)
|
82
|
+
|
83
|
+
# Execute with instrumentation
|
84
|
+
execute_raw_chat(messages, &streaming_block)
|
85
|
+
end
|
86
|
+
|
94
87
|
private
|
95
88
|
|
96
89
|
def chat_with_strategy(messages, signature_class, &block)
|
@@ -208,5 +201,77 @@ module DSPy
|
|
208
201
|
raise "Failed to parse LLM response as JSON: #{e.message}. Original content length: #{response.content&.length || 0} chars"
|
209
202
|
end
|
210
203
|
end
|
204
|
+
|
205
|
+
# Common instrumentation method for LM requests
|
206
|
+
def instrument_lm_request(messages, signature_class_name, &execution_block)
|
207
|
+
input_text = messages.map { |m| m[:content] }.join(' ')
|
208
|
+
input_size = input_text.length
|
209
|
+
|
210
|
+
response = nil
|
211
|
+
|
212
|
+
if should_emit_lm_events?
|
213
|
+
# Emit dspy.lm.request event
|
214
|
+
response = Instrumentation.instrument('dspy.lm.request', {
|
215
|
+
gen_ai_operation_name: 'chat',
|
216
|
+
gen_ai_system: provider,
|
217
|
+
gen_ai_request_model: model,
|
218
|
+
signature_class: signature_class_name,
|
219
|
+
provider: provider,
|
220
|
+
adapter_class: adapter.class.name,
|
221
|
+
input_size: input_size
|
222
|
+
}, &execution_block)
|
223
|
+
|
224
|
+
# Extract and emit token usage
|
225
|
+
emit_token_usage(response, signature_class_name)
|
226
|
+
else
|
227
|
+
# Consolidated mode: execute without instrumentation
|
228
|
+
response = execution_block.call
|
229
|
+
end
|
230
|
+
|
231
|
+
response
|
232
|
+
end
|
233
|
+
|
234
|
+
# Common method to emit token usage events
|
235
|
+
def emit_token_usage(response, signature_class_name)
|
236
|
+
token_usage = Instrumentation::TokenTracker.extract_token_usage(response, provider)
|
237
|
+
|
238
|
+
if token_usage.any?
|
239
|
+
Instrumentation.emit('dspy.lm.tokens', token_usage.merge({
|
240
|
+
gen_ai_system: provider,
|
241
|
+
gen_ai_request_model: model,
|
242
|
+
signature_class: signature_class_name
|
243
|
+
}))
|
244
|
+
end
|
245
|
+
|
246
|
+
token_usage
|
247
|
+
end
|
248
|
+
|
249
|
+
def validate_messages!(messages)
|
250
|
+
unless messages.is_a?(Array)
|
251
|
+
raise ArgumentError, "messages must be an array"
|
252
|
+
end
|
253
|
+
|
254
|
+
valid_roles = %w[system user assistant]
|
255
|
+
|
256
|
+
messages.each do |message|
|
257
|
+
unless message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
|
258
|
+
raise ArgumentError, "Each message must have :role and :content"
|
259
|
+
end
|
260
|
+
|
261
|
+
unless valid_roles.include?(message[:role])
|
262
|
+
raise ArgumentError, "Invalid role: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def execute_raw_chat(messages, &streaming_block)
|
268
|
+
response = instrument_lm_request(messages, 'RawPrompt') do
|
269
|
+
# Direct adapter call, no strategies or JSON parsing
|
270
|
+
adapter.chat(messages: messages, signature: nil, &streaming_block)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Return raw response content, not parsed JSON
|
274
|
+
response.content
|
275
|
+
end
|
211
276
|
end
|
212
277
|
end
|
data/lib/dspy/version.rb
CHANGED
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.
|
4
|
+
version: 0.12.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-07-
|
11
|
+
date: 2025-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-configurable
|
@@ -177,6 +177,7 @@ files:
|
|
177
177
|
- lib/dspy/lm/adapters/openai_adapter.rb
|
178
178
|
- lib/dspy/lm/cache_manager.rb
|
179
179
|
- lib/dspy/lm/errors.rb
|
180
|
+
- lib/dspy/lm/message_builder.rb
|
180
181
|
- lib/dspy/lm/response.rb
|
181
182
|
- lib/dspy/lm/retry_handler.rb
|
182
183
|
- lib/dspy/lm/strategies/anthropic_extraction_strategy.rb
|