dspy 0.11.0 → 0.13.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/instrumentation/token_tracker.rb +18 -2
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +4 -1
- data/lib/dspy/lm/adapters/openai_adapter.rb +4 -1
- data/lib/dspy/lm/message_builder.rb +28 -0
- data/lib/dspy/lm/response.rb +2 -0
- data/lib/dspy/lm/usage.rb +157 -0
- data/lib/dspy/lm.rb +101 -36
- data/lib/dspy/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d80d5b0166fe5a101e4918ffee13a70dec6ca67b493cf2e68dff1c18b2df36c1
|
4
|
+
data.tar.gz: 1687fe88d41c5d4627592ff5e98f87ca9f40186870386291b1ead091f51235da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bedebf2e58243bedcf8003d25b4f55789a7e4a611f9f1997a322e93db553fe117fde155415c5a607a65b49f61f2a640c7899a0fd4793bcdf9e8672602f54755
|
7
|
+
data.tar.gz: cdf39e605a7550e94334c5bad13ccc5af2a255b7039559b56b918e1ba3706e671c52960558459a0c96edab318c68bc9828b1eb03530b47daa896766a2cb7aa7d
|
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.
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative '../lm/usage'
|
4
|
+
|
3
5
|
module DSPy
|
4
6
|
module Instrumentation
|
5
7
|
# Utility for extracting token usage from different LM adapters
|
@@ -9,6 +11,18 @@ module DSPy
|
|
9
11
|
|
10
12
|
# Extract actual token usage from API responses
|
11
13
|
def extract_token_usage(response, provider)
|
14
|
+
return {} unless response&.usage
|
15
|
+
|
16
|
+
# Handle Usage struct
|
17
|
+
if response.usage.is_a?(DSPy::LM::Usage) || response.usage.is_a?(DSPy::LM::OpenAIUsage)
|
18
|
+
return {
|
19
|
+
input_tokens: response.usage.input_tokens,
|
20
|
+
output_tokens: response.usage.output_tokens,
|
21
|
+
total_tokens: response.usage.total_tokens
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fallback to legacy hash handling
|
12
26
|
case provider.to_s.downcase
|
13
27
|
when 'openai'
|
14
28
|
extract_openai_tokens(response)
|
@@ -27,11 +41,12 @@ module DSPy
|
|
27
41
|
usage = response.usage
|
28
42
|
return {} unless usage.is_a?(Hash)
|
29
43
|
|
44
|
+
# Handle both symbol and string keys for VCR compatibility
|
30
45
|
{
|
31
46
|
input_tokens: usage[:prompt_tokens] || usage['prompt_tokens'],
|
32
47
|
output_tokens: usage[:completion_tokens] || usage['completion_tokens'],
|
33
48
|
total_tokens: usage[:total_tokens] || usage['total_tokens']
|
34
|
-
}
|
49
|
+
}.compact # Remove nil values
|
35
50
|
end
|
36
51
|
|
37
52
|
def extract_anthropic_tokens(response)
|
@@ -40,6 +55,7 @@ module DSPy
|
|
40
55
|
usage = response.usage
|
41
56
|
return {} unless usage.is_a?(Hash)
|
42
57
|
|
58
|
+
# Handle both symbol and string keys for VCR compatibility
|
43
59
|
input_tokens = usage[:input_tokens] || usage['input_tokens'] || 0
|
44
60
|
output_tokens = usage[:output_tokens] || usage['output_tokens'] || 0
|
45
61
|
|
@@ -47,7 +63,7 @@ module DSPy
|
|
47
63
|
input_tokens: input_tokens,
|
48
64
|
output_tokens: output_tokens,
|
49
65
|
total_tokens: input_tokens + output_tokens
|
50
|
-
}
|
66
|
+
}.compact # Remove nil values
|
51
67
|
end
|
52
68
|
end
|
53
69
|
end
|
@@ -63,9 +63,12 @@ module DSPy
|
|
63
63
|
content = response.content.first.text if response.content.is_a?(Array) && response.content.first
|
64
64
|
usage = response.usage
|
65
65
|
|
66
|
+
# Convert usage data to typed struct
|
67
|
+
usage_struct = UsageFactory.create('anthropic', usage)
|
68
|
+
|
66
69
|
Response.new(
|
67
70
|
content: content,
|
68
|
-
usage:
|
71
|
+
usage: usage_struct,
|
69
72
|
metadata: {
|
70
73
|
provider: 'anthropic',
|
71
74
|
model: model,
|
@@ -52,9 +52,12 @@ module DSPy
|
|
52
52
|
raise AdapterError, "OpenAI refused to generate output: #{message.refusal}"
|
53
53
|
end
|
54
54
|
|
55
|
+
# Convert usage data to typed struct
|
56
|
+
usage_struct = UsageFactory.create('openai', usage)
|
57
|
+
|
55
58
|
Response.new(
|
56
59
|
content: content,
|
57
|
-
usage:
|
60
|
+
usage: usage_struct,
|
58
61
|
metadata: {
|
59
62
|
provider: 'openai',
|
60
63
|
model: model,
|
@@ -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/response.rb
CHANGED
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
class LM
|
7
|
+
# Base class for token usage information
|
8
|
+
class Usage < T::Struct
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
const :input_tokens, Integer
|
12
|
+
const :output_tokens, Integer
|
13
|
+
const :total_tokens, Integer
|
14
|
+
|
15
|
+
sig { returns(Hash) }
|
16
|
+
def to_h
|
17
|
+
{
|
18
|
+
input_tokens: input_tokens,
|
19
|
+
output_tokens: output_tokens,
|
20
|
+
total_tokens: total_tokens
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# OpenAI-specific usage information with additional fields
|
26
|
+
class OpenAIUsage < T::Struct
|
27
|
+
extend T::Sig
|
28
|
+
|
29
|
+
const :input_tokens, Integer
|
30
|
+
const :output_tokens, Integer
|
31
|
+
const :total_tokens, Integer
|
32
|
+
const :prompt_tokens_details, T.nilable(T::Hash[Symbol, Integer]), default: nil
|
33
|
+
const :completion_tokens_details, T.nilable(T::Hash[Symbol, Integer]), default: nil
|
34
|
+
|
35
|
+
sig { returns(Hash) }
|
36
|
+
def to_h
|
37
|
+
base = {
|
38
|
+
input_tokens: input_tokens,
|
39
|
+
output_tokens: output_tokens,
|
40
|
+
total_tokens: total_tokens
|
41
|
+
}
|
42
|
+
base[:prompt_tokens_details] = prompt_tokens_details if prompt_tokens_details
|
43
|
+
base[:completion_tokens_details] = completion_tokens_details if completion_tokens_details
|
44
|
+
base
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Factory for creating appropriate usage objects
|
49
|
+
module UsageFactory
|
50
|
+
extend T::Sig
|
51
|
+
|
52
|
+
sig { params(provider: String, usage_data: T.untyped).returns(T.nilable(T.any(Usage, OpenAIUsage))) }
|
53
|
+
def self.create(provider, usage_data)
|
54
|
+
return nil if usage_data.nil?
|
55
|
+
|
56
|
+
# If already a Usage struct, return as-is
|
57
|
+
return usage_data if usage_data.is_a?(Usage)
|
58
|
+
|
59
|
+
# Handle test doubles by converting to hash
|
60
|
+
if usage_data.respond_to?(:to_h)
|
61
|
+
usage_data = usage_data.to_h
|
62
|
+
end
|
63
|
+
|
64
|
+
# Convert hash to appropriate struct
|
65
|
+
return nil unless usage_data.is_a?(Hash)
|
66
|
+
|
67
|
+
# Normalize keys to symbols
|
68
|
+
normalized = usage_data.transform_keys(&:to_sym)
|
69
|
+
|
70
|
+
case provider.to_s.downcase
|
71
|
+
when 'openai'
|
72
|
+
create_openai_usage(normalized)
|
73
|
+
when 'anthropic'
|
74
|
+
create_anthropic_usage(normalized)
|
75
|
+
else
|
76
|
+
create_generic_usage(normalized)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(OpenAIUsage)) }
|
83
|
+
def self.create_openai_usage(data)
|
84
|
+
# OpenAI uses prompt_tokens/completion_tokens
|
85
|
+
input_tokens = data[:prompt_tokens] || data[:input_tokens] || 0
|
86
|
+
output_tokens = data[:completion_tokens] || data[:output_tokens] || 0
|
87
|
+
total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
|
88
|
+
|
89
|
+
# Convert prompt_tokens_details and completion_tokens_details to hashes if needed
|
90
|
+
prompt_details = convert_to_hash(data[:prompt_tokens_details])
|
91
|
+
completion_details = convert_to_hash(data[:completion_tokens_details])
|
92
|
+
|
93
|
+
OpenAIUsage.new(
|
94
|
+
input_tokens: input_tokens,
|
95
|
+
output_tokens: output_tokens,
|
96
|
+
total_tokens: total_tokens,
|
97
|
+
prompt_tokens_details: prompt_details,
|
98
|
+
completion_tokens_details: completion_details
|
99
|
+
)
|
100
|
+
rescue => e
|
101
|
+
DSPy.logger.debug("Failed to create OpenAI usage: #{e.message}")
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
sig { params(value: T.untyped).returns(T.nilable(T::Hash[Symbol, Integer])) }
|
106
|
+
def self.convert_to_hash(value)
|
107
|
+
return nil if value.nil?
|
108
|
+
return value if value.is_a?(Hash) && value.keys.all? { |k| k.is_a?(Symbol) }
|
109
|
+
|
110
|
+
# Convert object to hash if it responds to to_h
|
111
|
+
if value.respond_to?(:to_h)
|
112
|
+
hash = value.to_h
|
113
|
+
# Ensure symbol keys and integer values
|
114
|
+
hash.transform_keys(&:to_sym).transform_values(&:to_i)
|
115
|
+
else
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
rescue
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
|
123
|
+
def self.create_anthropic_usage(data)
|
124
|
+
# Anthropic uses input_tokens/output_tokens
|
125
|
+
input_tokens = data[:input_tokens] || 0
|
126
|
+
output_tokens = data[:output_tokens] || 0
|
127
|
+
total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
|
128
|
+
|
129
|
+
Usage.new(
|
130
|
+
input_tokens: input_tokens,
|
131
|
+
output_tokens: output_tokens,
|
132
|
+
total_tokens: total_tokens
|
133
|
+
)
|
134
|
+
rescue => e
|
135
|
+
DSPy.logger.debug("Failed to create Anthropic usage: #{e.message}")
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
|
140
|
+
def self.create_generic_usage(data)
|
141
|
+
# Generic fallback
|
142
|
+
input_tokens = data[:input_tokens] || data[:prompt_tokens] || 0
|
143
|
+
output_tokens = data[:output_tokens] || data[:completion_tokens] || 0
|
144
|
+
total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
|
145
|
+
|
146
|
+
Usage.new(
|
147
|
+
input_tokens: input_tokens,
|
148
|
+
output_tokens: output_tokens,
|
149
|
+
total_tokens: total_tokens
|
150
|
+
)
|
151
|
+
rescue => e
|
152
|
+
DSPy.logger.debug("Failed to create generic usage: #{e.message}")
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
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.13.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-25 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
|
@@ -185,6 +186,7 @@ files:
|
|
185
186
|
- lib/dspy/lm/strategies/openai_structured_output_strategy.rb
|
186
187
|
- lib/dspy/lm/strategy_selector.rb
|
187
188
|
- lib/dspy/lm/structured_output_strategy.rb
|
189
|
+
- lib/dspy/lm/usage.rb
|
188
190
|
- lib/dspy/memory.rb
|
189
191
|
- lib/dspy/memory/embedding_engine.rb
|
190
192
|
- lib/dspy/memory/in_memory_store.rb
|