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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de6126089636bd0d5fdaf9cd2bba791157186683a7984174bac980be8e32a483
4
- data.tar.gz: 1e78c20f7e1a37cf025158be0f4014ee2abe9ba95b84682361ff9ea544fecd2c
3
+ metadata.gz: d40d9681a87073c6c4f5531025891bf871bee086434ee131e15a4391cba7b893
4
+ data.tar.gz: 1a1a4ce935e7cdf4ce9c06617f124ed8858d7074a3f6bec4484d6db24dbfa232
5
5
  SHA512:
6
- metadata.gz: 7b3dbde7e5040dc1b0562142bdc560f9fc393bbb7cf20bef4d767541be079408dc1c6e196257f07193266713585bdfb21ac77bd08df3c6ab69607354393987c2
7
- data.tar.gz: 412cf071635f85bb3fb08f339a1dce23d4a9f00144104fc03fc25cc706d87c86c707221bc8a98f6847a7e157e2fa86de8bee4ee7c44d2bd1b9b0ab41934dd411
6
+ metadata.gz: 2cdd3ee7e867344b4e45dee7fd0d73dc875ec2c730a2cf75e5f620a19c535f0aac50cc6c88dfb7beb3e075b147e7467ccd4e55a7724f49f3ac111eb080fd88e0
7
+ data.tar.gz: f5c5ed5dd3b6aa5401c20e31fbbd9638eb1d8c0f3b7fe5adb2e2b69bbb3b051d0ecd277d6e2f3b4d4896fd19f39a89345c9503d97106a3b8e005b1f55a3e5b2b
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # DSPy.rb
2
2
 
3
+ [![Gem Version](https://img.shields.io/gem/v/dspy)](https://rubygems.org/gems/dspy)
4
+ [![Total Downloads](https://img.shields.io/gem/dt/dspy)](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
- # Calculate input size for monitoring
43
- input_text = messages.map { |m| m[:content] }.join(' ')
44
- input_size = input_text.length
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
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.11.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-21 00:00:00.000000000 Z
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