desiru 0.1.0 → 0.1.1
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/.env.example +34 -0
- data/.rubocop.yml +7 -4
- data/.ruby-version +1 -0
- data/CLAUDE.md +4 -0
- data/Gemfile +21 -2
- data/Gemfile.lock +87 -12
- data/README.md +295 -2
- data/Rakefile +1 -0
- data/db/migrations/001_create_initial_tables.rb +96 -0
- data/db/migrations/002_create_job_results.rb +39 -0
- data/desiru.db +0 -0
- data/desiru.gemspec +2 -5
- data/docs/background_processing_roadmap.md +87 -0
- data/docs/job_scheduling.md +167 -0
- data/dspy-analysis-swarm.yml +60 -0
- data/dspy-feature-analysis.md +121 -0
- data/examples/README.md +69 -0
- data/examples/api_with_persistence.rb +122 -0
- data/examples/assertions_example.rb +232 -0
- data/examples/async_processing.rb +2 -0
- data/examples/few_shot_learning.rb +1 -2
- data/examples/graphql_api.rb +4 -2
- data/examples/graphql_integration.rb +3 -3
- data/examples/graphql_optimization_summary.md +143 -0
- data/examples/graphql_performance_benchmark.rb +247 -0
- data/examples/persistence_example.rb +102 -0
- data/examples/react_agent.rb +203 -0
- data/examples/rest_api.rb +173 -0
- data/examples/rest_api_advanced.rb +333 -0
- data/examples/scheduled_job_example.rb +116 -0
- data/examples/simple_qa.rb +1 -2
- data/examples/sinatra_api.rb +109 -0
- data/examples/typed_signatures.rb +1 -2
- data/graphql_optimization_summary.md +53 -0
- data/lib/desiru/api/grape_integration.rb +284 -0
- data/lib/desiru/api/persistence_middleware.rb +148 -0
- data/lib/desiru/api/sinatra_integration.rb +217 -0
- data/lib/desiru/api.rb +42 -0
- data/lib/desiru/assertions.rb +74 -0
- data/lib/desiru/async_status.rb +65 -0
- data/lib/desiru/cache.rb +1 -1
- data/lib/desiru/configuration.rb +2 -1
- data/lib/desiru/errors.rb +160 -0
- data/lib/desiru/field.rb +17 -14
- data/lib/desiru/graphql/batch_loader.rb +85 -0
- data/lib/desiru/graphql/data_loader.rb +242 -75
- data/lib/desiru/graphql/enum_builder.rb +75 -0
- data/lib/desiru/graphql/executor.rb +37 -4
- data/lib/desiru/graphql/schema_generator.rb +62 -158
- data/lib/desiru/graphql/type_builder.rb +138 -0
- data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
- data/lib/desiru/jobs/async_predict.rb +1 -1
- data/lib/desiru/jobs/base.rb +67 -0
- data/lib/desiru/jobs/batch_processor.rb +6 -6
- data/lib/desiru/jobs/retriable.rb +119 -0
- data/lib/desiru/jobs/retry_strategies.rb +169 -0
- data/lib/desiru/jobs/scheduler.rb +219 -0
- data/lib/desiru/jobs/webhook_notifier.rb +242 -0
- data/lib/desiru/models/anthropic.rb +164 -0
- data/lib/desiru/models/base.rb +37 -3
- data/lib/desiru/models/open_ai.rb +151 -0
- data/lib/desiru/models/open_router.rb +161 -0
- data/lib/desiru/module.rb +59 -9
- data/lib/desiru/modules/chain_of_thought.rb +3 -3
- data/lib/desiru/modules/majority.rb +51 -0
- data/lib/desiru/modules/multi_chain_comparison.rb +204 -0
- data/lib/desiru/modules/predict.rb +8 -1
- data/lib/desiru/modules/program_of_thought.rb +139 -0
- data/lib/desiru/modules/react.rb +273 -0
- data/lib/desiru/modules/retrieve.rb +4 -2
- data/lib/desiru/optimizers/base.rb +2 -4
- data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
- data/lib/desiru/optimizers/copro.rb +268 -0
- data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
- data/lib/desiru/persistence/database.rb +71 -0
- data/lib/desiru/persistence/models/api_request.rb +38 -0
- data/lib/desiru/persistence/models/job_result.rb +138 -0
- data/lib/desiru/persistence/models/module_execution.rb +37 -0
- data/lib/desiru/persistence/models/optimization_result.rb +28 -0
- data/lib/desiru/persistence/models/training_example.rb +25 -0
- data/lib/desiru/persistence/models.rb +11 -0
- data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
- data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
- data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
- data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
- data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
- data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
- data/lib/desiru/persistence/repository.rb +29 -0
- data/lib/desiru/persistence/setup.rb +77 -0
- data/lib/desiru/persistence.rb +49 -0
- data/lib/desiru/registry.rb +3 -5
- data/lib/desiru/signature.rb +91 -24
- data/lib/desiru/version.rb +1 -1
- data/lib/desiru.rb +23 -8
- data/missing-features-analysis.md +192 -0
- metadata +63 -45
- data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -0,0 +1,122 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'desiru'
|
6
|
+
require 'desiru/persistence'
|
7
|
+
require 'rack'
|
8
|
+
|
9
|
+
# Configure Desiru
|
10
|
+
Desiru.configure do |config|
|
11
|
+
config.default_model = Desiru::Models::OpenAI.new(
|
12
|
+
api_key: ENV['OPENAI_API_KEY'] || 'your-api-key',
|
13
|
+
model: 'gpt-3.5-turbo'
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Setup persistence
|
18
|
+
puts "Setting up database..."
|
19
|
+
Desiru::Persistence.database_url = 'sqlite://api_tracking.db'
|
20
|
+
Desiru::Persistence.connect!
|
21
|
+
Desiru::Persistence.migrate!
|
22
|
+
|
23
|
+
# Define a simple module
|
24
|
+
class TextAnalyzer < Desiru::Module
|
25
|
+
signature 'TextAnalyzer', 'Analyze text sentiment and key themes'
|
26
|
+
|
27
|
+
input 'text', type: 'string', desc: 'Text to analyze'
|
28
|
+
|
29
|
+
output 'sentiment', type: 'string', desc: 'Overall sentiment (positive/negative/neutral)'
|
30
|
+
output 'themes', type: 'list[string]', desc: 'Key themes identified'
|
31
|
+
output 'confidence', type: 'float', desc: 'Confidence score (0-1)'
|
32
|
+
|
33
|
+
def forward(_text:)
|
34
|
+
# Simulate analysis
|
35
|
+
{
|
36
|
+
sentiment: %w[positive negative neutral].sample,
|
37
|
+
themes: %w[technology business health education].sample(2),
|
38
|
+
confidence: rand(0.7..0.95).round(2)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create API with persistence tracking
|
44
|
+
api = Desiru::API.create(framework: :sinatra) do
|
45
|
+
register_module '/analyze', TextAnalyzer.new,
|
46
|
+
description: 'Analyze text sentiment and themes'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add persistence tracking
|
50
|
+
app = api.with_persistence(enabled: true)
|
51
|
+
|
52
|
+
# Add a simple UI endpoint
|
53
|
+
ui_app = Rack::Builder.new do
|
54
|
+
use Desiru::API::PersistenceMiddleware
|
55
|
+
|
56
|
+
map '/' do
|
57
|
+
run lambda { |_env|
|
58
|
+
html = <<~HTML
|
59
|
+
<!DOCTYPE html>
|
60
|
+
<html>
|
61
|
+
<head>
|
62
|
+
<title>Desiru API with Persistence</title>
|
63
|
+
<style>
|
64
|
+
body { font-family: Arial, sans-serif; margin: 40px; }
|
65
|
+
.endpoint { background: #f0f0f0; padding: 10px; margin: 10px 0; }
|
66
|
+
.stats { background: #e0f0ff; padding: 15px; margin: 20px 0; }
|
67
|
+
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
68
|
+
</style>
|
69
|
+
</head>
|
70
|
+
<body>
|
71
|
+
<h1>Desiru API with Persistence Tracking</h1>
|
72
|
+
#{' '}
|
73
|
+
<div class="endpoint">
|
74
|
+
<h2>Text Analysis Endpoint</h2>
|
75
|
+
<p><strong>POST /api/v1/analyze</strong></p>
|
76
|
+
<p>Analyze text sentiment and extract key themes</p>
|
77
|
+
<pre>curl -X POST http://localhost:9294/api/v1/analyze \\
|
78
|
+
-H "Content-Type: application/json" \\
|
79
|
+
-d '{"text": "This is an amazing product that exceeds expectations!"}'</pre>
|
80
|
+
</div>
|
81
|
+
#{' '}
|
82
|
+
<div class="stats">
|
83
|
+
<h2>API Statistics</h2>
|
84
|
+
<ul>
|
85
|
+
<li>Total API Requests: #{Desiru::Persistence[:api_requests].count}</li>
|
86
|
+
<li>Module Executions: #{Desiru::Persistence[:module_executions].count}</li>
|
87
|
+
<li>Success Rate: #{Desiru::Persistence[:module_executions].success_rate}%</li>
|
88
|
+
<li>Average Response Time: #{Desiru::Persistence[:api_requests].average_response_time || 0}s</li>
|
89
|
+
</ul>
|
90
|
+
</div>
|
91
|
+
#{' '}
|
92
|
+
<div class="endpoint">
|
93
|
+
<h2>Recent Requests</h2>
|
94
|
+
<ul>
|
95
|
+
#{Desiru::Persistence[:api_requests].recent(5).map do |r|
|
96
|
+
"<li>#{r.method} #{r.path} - #{r.status_code} (#{r.response_time ? "#{(r.response_time * 1000).round}ms" : 'N/A'})</li>"
|
97
|
+
end.join("\n ")}
|
98
|
+
</ul>
|
99
|
+
</div>
|
100
|
+
</body>
|
101
|
+
</html>
|
102
|
+
HTML
|
103
|
+
|
104
|
+
[200, { 'Content-Type' => 'text/html' }, [html]]
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
map '/api' do
|
109
|
+
run app
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
puts "Starting API server with persistence tracking on http://localhost:9294"
|
114
|
+
puts "\nEndpoints:"
|
115
|
+
puts " GET / - Web UI with statistics"
|
116
|
+
puts " POST /api/v1/analyze - Text analysis endpoint"
|
117
|
+
puts " GET /api/v1/health - Health check"
|
118
|
+
puts "\nAll API requests are automatically tracked in the database!"
|
119
|
+
puts "Press Ctrl+C to stop the server"
|
120
|
+
|
121
|
+
# Start the server
|
122
|
+
Rack::Handler::WEBrick.run ui_app, Port: 9294
|
@@ -0,0 +1,232 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'desiru'
|
6
|
+
|
7
|
+
# Mock model for demonstration
|
8
|
+
class MockModel
|
9
|
+
def complete(_messages:, **_options)
|
10
|
+
# Simple mock that returns predefined responses
|
11
|
+
{ choices: [{ message: { content: "Mock response" } }] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Configure Desiru with assertions
|
16
|
+
Desiru.configure do |config|
|
17
|
+
config.default_model = MockModel.new
|
18
|
+
config.logger = Logger.new($stdout).tap do |log|
|
19
|
+
log.level = Logger::INFO
|
20
|
+
log.formatter = proc do |severity, datetime, _, msg|
|
21
|
+
"[#{severity}] #{datetime}: #{msg}\n"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Configure assertion behavior
|
27
|
+
Desiru::Assertions.configure do |config|
|
28
|
+
config.max_assertion_retries = 2
|
29
|
+
config.assertion_retry_delay = 0.5
|
30
|
+
end
|
31
|
+
|
32
|
+
# Example 1: Module with confidence assertion
|
33
|
+
class FactChecker < Desiru::Module
|
34
|
+
def forward(statement:)
|
35
|
+
# Simulate fact checking with confidence score
|
36
|
+
facts = [
|
37
|
+
{ statement: "The sky is blue", confidence: 0.95 },
|
38
|
+
{ statement: "Water boils at 100°C", confidence: 0.98 },
|
39
|
+
{ statement: "Cats can fly", confidence: 0.1 },
|
40
|
+
{ statement: "The Earth is flat", confidence: 0.05 }
|
41
|
+
]
|
42
|
+
|
43
|
+
# Find confidence for the statement
|
44
|
+
fact = facts.find { |f| f[:statement].downcase == statement.downcase }
|
45
|
+
confidence = fact ? fact[:confidence] : rand(0.3..0.9)
|
46
|
+
|
47
|
+
result = {
|
48
|
+
statement: statement,
|
49
|
+
confidence: confidence,
|
50
|
+
verified: confidence > 0.7
|
51
|
+
}
|
52
|
+
|
53
|
+
# Assert high confidence for fact verification
|
54
|
+
Desiru.assert(
|
55
|
+
result[:confidence] > 0.7,
|
56
|
+
"Low confidence score: #{result[:confidence]}. Cannot verify statement."
|
57
|
+
)
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Example 2: Module with suggestions for best practices
|
64
|
+
class CodeReviewer < Desiru::Module
|
65
|
+
def forward(code:, language:)
|
66
|
+
review = {
|
67
|
+
code: code,
|
68
|
+
language: language,
|
69
|
+
issues: [],
|
70
|
+
suggestions: []
|
71
|
+
}
|
72
|
+
|
73
|
+
# Simulate code analysis
|
74
|
+
if code.include?('TODO')
|
75
|
+
review[:issues] << "Found TODO comment"
|
76
|
+
review[:suggestions] << "Consider creating a ticket for TODO items"
|
77
|
+
end
|
78
|
+
|
79
|
+
if language == 'ruby' && !code.include?('frozen_string_literal')
|
80
|
+
review[:suggestions] << "Add frozen_string_literal pragma"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Suggest having tests
|
84
|
+
Desiru.suggest(
|
85
|
+
code.include?('test') || code.include?('spec'),
|
86
|
+
"No tests found in the code. Consider adding test coverage."
|
87
|
+
)
|
88
|
+
|
89
|
+
# Suggest documentation
|
90
|
+
Desiru.suggest(
|
91
|
+
code.include?('#') || code.include?('/**'),
|
92
|
+
"No comments found. Consider adding documentation."
|
93
|
+
)
|
94
|
+
|
95
|
+
review[:score] = 100 - (review[:issues].length * 10)
|
96
|
+
review
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Example 3: Module combining assertions and suggestions
|
101
|
+
class DataValidator < Desiru::Module
|
102
|
+
def forward(data:, schema:)
|
103
|
+
validation = {
|
104
|
+
data: data,
|
105
|
+
valid: true,
|
106
|
+
errors: [],
|
107
|
+
warnings: []
|
108
|
+
}
|
109
|
+
|
110
|
+
# Required field assertion
|
111
|
+
schema[:required]&.each do |field|
|
112
|
+
if !data.key?(field) || data[field].nil?
|
113
|
+
validation[:valid] = false
|
114
|
+
validation[:errors] << "Missing required field: #{field}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Assert data is valid
|
119
|
+
Desiru.assert(
|
120
|
+
validation[:valid],
|
121
|
+
"Data validation failed: #{validation[:errors].join(', ')}"
|
122
|
+
)
|
123
|
+
|
124
|
+
# Suggest best practices
|
125
|
+
if data.is_a?(Hash)
|
126
|
+
Desiru.suggest(
|
127
|
+
data.keys.all? { |k| k.is_a?(Symbol) },
|
128
|
+
"Consider using symbols for hash keys for better performance"
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check data types (suggestions)
|
133
|
+
schema[:types]&.each do |field, expected_type|
|
134
|
+
next unless data.key?(field)
|
135
|
+
|
136
|
+
actual_type = data[field].class
|
137
|
+
Desiru.suggest(
|
138
|
+
actual_type == expected_type,
|
139
|
+
"Field '#{field}' is #{actual_type}, expected #{expected_type}"
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
validation
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Demonstrate the modules
|
148
|
+
puts "=== Assertion Examples ==="
|
149
|
+
puts
|
150
|
+
|
151
|
+
# Example 1: Fact Checker with passing assertion
|
152
|
+
puts "1. Fact Checker - Valid Statement:"
|
153
|
+
fact_checker = FactChecker.new('statement:str -> statement:str, confidence:float, verified:bool')
|
154
|
+
begin
|
155
|
+
result = fact_checker.call(statement: "Water boils at 100°C")
|
156
|
+
puts " ✓ Statement: #{result[:statement]}"
|
157
|
+
puts " ✓ Confidence: #{result[:confidence]}"
|
158
|
+
puts " ✓ Verified: #{result[:verified]}"
|
159
|
+
rescue Desiru::Assertions::AssertionError => e
|
160
|
+
puts " ✗ Assertion failed: #{e.message}"
|
161
|
+
end
|
162
|
+
puts
|
163
|
+
|
164
|
+
# Example 2: Fact Checker with failing assertion
|
165
|
+
puts "2. Fact Checker - False Statement:"
|
166
|
+
begin
|
167
|
+
result = fact_checker.call(statement: "Cats can fly")
|
168
|
+
puts " ✓ Statement verified with confidence: #{result[:confidence]}"
|
169
|
+
rescue Desiru::Assertions::AssertionError => e
|
170
|
+
puts " ✗ Assertion failed after retries: #{e.message}"
|
171
|
+
puts " ✗ Module: #{e.module_name}"
|
172
|
+
puts " ✗ Retries: #{e.retry_count}"
|
173
|
+
end
|
174
|
+
puts
|
175
|
+
|
176
|
+
# Example 3: Code Reviewer with suggestions
|
177
|
+
puts "3. Code Reviewer - With Suggestions:"
|
178
|
+
code_reviewer = CodeReviewer.new(
|
179
|
+
'code:str, language:str -> code:str, language:str, issues:list, suggestions:list, score:int'
|
180
|
+
)
|
181
|
+
code = <<~RUBY
|
182
|
+
def calculate_sum(numbers)
|
183
|
+
# TODO: Add validation
|
184
|
+
numbers.sum
|
185
|
+
end
|
186
|
+
RUBY
|
187
|
+
|
188
|
+
result = code_reviewer.call(code: code, language: 'ruby')
|
189
|
+
puts " Code review score: #{result[:score]}"
|
190
|
+
puts " Issues: #{result[:issues].join(', ')}"
|
191
|
+
puts " Suggestions: #{result[:suggestions].join(', ')}"
|
192
|
+
puts
|
193
|
+
|
194
|
+
# Example 4: Data Validator with mixed validations
|
195
|
+
puts "4. Data Validator - Complete Example:"
|
196
|
+
validator = DataValidator.new('data:dict, schema:dict -> data:dict, valid:bool, errors:list, warnings:list')
|
197
|
+
|
198
|
+
schema = {
|
199
|
+
required: %i[name email],
|
200
|
+
types: {
|
201
|
+
name: String,
|
202
|
+
email: String,
|
203
|
+
age: Integer
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
# Valid data
|
208
|
+
puts " a) Valid data:"
|
209
|
+
begin
|
210
|
+
valid_data = { name: "John Doe", email: "john@example.com", age: 30 }
|
211
|
+
result = validator.call(data: valid_data, schema: schema)
|
212
|
+
puts " ✓ Validation passed"
|
213
|
+
puts " ✓ Data is valid: #{result[:valid]}"
|
214
|
+
rescue Desiru::Assertions::AssertionError => e
|
215
|
+
puts " ✗ Validation failed: #{e.message}"
|
216
|
+
end
|
217
|
+
|
218
|
+
# Invalid data
|
219
|
+
puts " b) Invalid data (missing required field):"
|
220
|
+
begin
|
221
|
+
invalid_data = { name: "Jane Doe", age: "twenty-five" }
|
222
|
+
validator.call(data: invalid_data, schema: schema)
|
223
|
+
puts " ✓ Validation passed"
|
224
|
+
rescue Desiru::Assertions::AssertionError => e
|
225
|
+
puts " ✗ Validation failed: #{e.message}"
|
226
|
+
end
|
227
|
+
|
228
|
+
puts
|
229
|
+
puts "=== Assertion Configuration ==="
|
230
|
+
puts "Max assertion retries: #{Desiru::Assertions.configuration.max_assertion_retries}"
|
231
|
+
puts "Retry delay: #{Desiru::Assertions.configuration.assertion_retry_delay}s"
|
232
|
+
puts "Assertions logged: #{Desiru::Assertions.configuration.log_assertions}"
|
@@ -52,6 +52,8 @@ puts " Status: Processing..."
|
|
52
52
|
# Check if ready (non-blocking)
|
53
53
|
sleep(0.1)
|
54
54
|
puts " Ready? #{result.ready?}"
|
55
|
+
puts " Status: #{result.status}"
|
56
|
+
puts " Progress: #{result.progress}%" if result.progress
|
55
57
|
|
56
58
|
# Wait for result (blocking with timeout)
|
57
59
|
begin
|
@@ -6,8 +6,7 @@ require 'desiru'
|
|
6
6
|
|
7
7
|
# Configure Desiru
|
8
8
|
Desiru.configure do |config|
|
9
|
-
config.default_model = Desiru::Models::
|
10
|
-
provider: :openai,
|
9
|
+
config.default_model = Desiru::Models::OpenAI.new(
|
11
10
|
model: 'gpt-3.5-turbo',
|
12
11
|
api_key: ENV['OPENAI_API_KEY'] || raise('Please set OPENAI_API_KEY environment variable')
|
13
12
|
)
|
data/examples/graphql_api.rb
CHANGED
@@ -8,8 +8,10 @@ require 'desiru/graphql/executor'
|
|
8
8
|
|
9
9
|
# Configure Desiru
|
10
10
|
Desiru.configure do |config|
|
11
|
-
# Use
|
12
|
-
config.default_model = Desiru::Models::
|
11
|
+
# Use OpenAI model for demonstration
|
12
|
+
config.default_model = Desiru::Models::OpenAI.new(
|
13
|
+
api_key: ENV['OPENAI_API_KEY'] || raise('Please set OPENAI_API_KEY environment variable')
|
14
|
+
)
|
13
15
|
end
|
14
16
|
|
15
17
|
# Create some example modules
|
@@ -10,9 +10,9 @@ require 'desiru/graphql/schema_generator'
|
|
10
10
|
|
11
11
|
# Configure Desiru
|
12
12
|
Desiru.configure do |config|
|
13
|
-
config.default_model = Desiru::Models::
|
14
|
-
|
15
|
-
|
13
|
+
config.default_model = Desiru::Models::OpenAI.new(
|
14
|
+
model: 'gpt-3.5-turbo',
|
15
|
+
api_key: ENV['OPENAI_API_KEY'] || raise('Please set OPENAI_API_KEY environment variable')
|
16
16
|
)
|
17
17
|
end
|
18
18
|
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# GraphQL DataLoader Optimization: Request Deduplication & Code Quality Improvements
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
I've implemented request deduplication in the GraphQL DataLoader to prevent duplicate operations and improve performance. This optimization is particularly beneficial for GraphQL APIs that handle complex queries with repeated fields. Additionally, I've refactored the code for better maintainability and added VCR integration for reproducible testing.
|
6
|
+
|
7
|
+
## Changes Made
|
8
|
+
|
9
|
+
### 1. Enhanced DataLoader (`lib/desiru/graphql/data_loader.rb`)
|
10
|
+
- Added `@pending_promises` tracking to detect duplicate requests
|
11
|
+
- Added `@mutex` for thread-safe operations
|
12
|
+
- Modified `perform_loads` to group identical requests and process only unique ones
|
13
|
+
- All duplicate requests receive the same result, preventing redundant processing
|
14
|
+
|
15
|
+
### 2. Updated BatchLoader
|
16
|
+
- Added `check_pending_promise` method to detect existing promises for the same inputs
|
17
|
+
- Modified `load` method to return existing promises for duplicate requests
|
18
|
+
- Ensures thread-safe promise management
|
19
|
+
|
20
|
+
### 3. Key Implementation Details
|
21
|
+
|
22
|
+
**Deduplication Logic:**
|
23
|
+
```ruby
|
24
|
+
# Group by unique inputs to deduplicate
|
25
|
+
unique_inputs_map = {}
|
26
|
+
promises_by_inputs = Hash.new { |h, k| h[k] = [] }
|
27
|
+
|
28
|
+
batch.each do |inputs, promise|
|
29
|
+
input_key = inputs.sort.to_h.hash
|
30
|
+
unique_inputs_map[input_key] = inputs
|
31
|
+
promises_by_inputs[input_key] << promise
|
32
|
+
end
|
33
|
+
|
34
|
+
# Process only unique inputs
|
35
|
+
unique_inputs = unique_inputs_map.values
|
36
|
+
```
|
37
|
+
|
38
|
+
**Thread Safety:**
|
39
|
+
- All shared state modifications are wrapped in mutex synchronization
|
40
|
+
- Promise fulfillment is handled atomically
|
41
|
+
- Concurrent duplicate requests are properly handled
|
42
|
+
|
43
|
+
## Performance Impact
|
44
|
+
|
45
|
+
The benchmark results show significant improvements:
|
46
|
+
|
47
|
+
1. **Query with 6 fields (3 unique)**: 89.5% improvement
|
48
|
+
2. **Nested query simulation**: 14.9% improvement
|
49
|
+
3. **Large batch (50 fields, 10 unique)**: 6.1% improvement with 5:1 deduplication ratio
|
50
|
+
|
51
|
+
## Benefits
|
52
|
+
|
53
|
+
1. **Prevents N+1 Problems**: Multiple requests for the same data are automatically deduplicated
|
54
|
+
2. **Improved Response Times**: Fewer actual module executions mean faster responses
|
55
|
+
3. **Resource Efficiency**: Reduces load on backend systems and LLMs
|
56
|
+
4. **Thread Safe**: Properly handles concurrent requests
|
57
|
+
5. **Transparent**: Works automatically without changes to GraphQL schemas or queries
|
58
|
+
|
59
|
+
## Testing
|
60
|
+
|
61
|
+
Added comprehensive test coverage including:
|
62
|
+
- Basic deduplication scenarios
|
63
|
+
- Different request patterns
|
64
|
+
- Cache interaction
|
65
|
+
- Key ordering independence
|
66
|
+
- Concurrent request handling
|
67
|
+
- Thread safety verification
|
68
|
+
|
69
|
+
All tests pass successfully.
|
70
|
+
|
71
|
+
## Usage
|
72
|
+
|
73
|
+
The optimization works automatically when using the GraphQL DataLoader:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
data_loader = Desiru::GraphQL::DataLoader.new
|
77
|
+
executor = Desiru::GraphQL::Executor.new(schema, data_loader: data_loader)
|
78
|
+
|
79
|
+
# Duplicate requests in the query are automatically deduplicated
|
80
|
+
result = executor.execute(graphql_query)
|
81
|
+
```
|
82
|
+
|
83
|
+
## Code Quality Improvements
|
84
|
+
|
85
|
+
### Refactored Complex Methods
|
86
|
+
- Split `perform_loads` method into smaller, focused methods:
|
87
|
+
- `process_loader_batch` - Handles individual loader batches
|
88
|
+
- `deduplicate_batch` - Extracts deduplication logic
|
89
|
+
- `execute_batch` - Handles batch execution and error handling
|
90
|
+
- `fulfill_promises` - Manages promise fulfillment
|
91
|
+
- Reduced method complexity from ABC size 43.69 to under 25
|
92
|
+
- Improved code readability and maintainability
|
93
|
+
|
94
|
+
### Architectural Improvements
|
95
|
+
- **Extracted TypeBuilder Module**: Moved GraphQL type generation logic into a separate module
|
96
|
+
- Reduced SchemaGenerator class length by ~140 lines
|
97
|
+
- Better separation of concerns
|
98
|
+
- Improved testability and reusability
|
99
|
+
- **Fixed Linting Issues**:
|
100
|
+
- Converted class variables to class instance variables in SchemaGenerator
|
101
|
+
- Fixed predicate method naming (`has_pending_loads?` → `pending_loads?`)
|
102
|
+
- Eliminated duplicate branch conditions in type resolution
|
103
|
+
- **Maintained Performance**: All optimizations preserved with 83.9% improvement for duplicate requests
|
104
|
+
|
105
|
+
### VCR Integration for Testing
|
106
|
+
Added comprehensive VCR support for GraphQL testing:
|
107
|
+
- **GraphQLVCRHelper** module for easy VCR configuration
|
108
|
+
- Custom GraphQL operation matching for accurate cassette playback
|
109
|
+
- Helpers for recording batch operations
|
110
|
+
- Support for error recording and playback
|
111
|
+
- Performance tracking across recordings
|
112
|
+
|
113
|
+
**Note**: To use VCR integration, add these gems to your Gemfile:
|
114
|
+
```ruby
|
115
|
+
group :development, :test do
|
116
|
+
gem 'vcr', '~> 6.0'
|
117
|
+
gem 'webmock', '~> 3.0'
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Example usage:
|
122
|
+
```ruby
|
123
|
+
with_graphql_vcr('api_calls') do
|
124
|
+
result = executor.execute(graphql_query)
|
125
|
+
assert_graphql_success(result)
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
Benefits:
|
130
|
+
- Reproducible tests without hitting real APIs
|
131
|
+
- Faster test execution with cassette playback
|
132
|
+
- Easy debugging with recorded interactions
|
133
|
+
- Consistent test results across environments
|
134
|
+
|
135
|
+
## Future Optimizations
|
136
|
+
|
137
|
+
Additional optimizations that could be implemented:
|
138
|
+
1. Smarter cache key generation using content hashing
|
139
|
+
2. Connection pooling for parallel batch processing
|
140
|
+
3. Adaptive batch sizing based on load patterns
|
141
|
+
4. Request prioritization for critical queries
|
142
|
+
5. Metrics collection for monitoring deduplication effectiveness
|
143
|
+
6. Integration with APM tools for performance tracking
|