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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +34 -0
  3. data/.rubocop.yml +7 -4
  4. data/.ruby-version +1 -0
  5. data/CLAUDE.md +4 -0
  6. data/Gemfile +21 -2
  7. data/Gemfile.lock +87 -12
  8. data/README.md +295 -2
  9. data/Rakefile +1 -0
  10. data/db/migrations/001_create_initial_tables.rb +96 -0
  11. data/db/migrations/002_create_job_results.rb +39 -0
  12. data/desiru.db +0 -0
  13. data/desiru.gemspec +2 -5
  14. data/docs/background_processing_roadmap.md +87 -0
  15. data/docs/job_scheduling.md +167 -0
  16. data/dspy-analysis-swarm.yml +60 -0
  17. data/dspy-feature-analysis.md +121 -0
  18. data/examples/README.md +69 -0
  19. data/examples/api_with_persistence.rb +122 -0
  20. data/examples/assertions_example.rb +232 -0
  21. data/examples/async_processing.rb +2 -0
  22. data/examples/few_shot_learning.rb +1 -2
  23. data/examples/graphql_api.rb +4 -2
  24. data/examples/graphql_integration.rb +3 -3
  25. data/examples/graphql_optimization_summary.md +143 -0
  26. data/examples/graphql_performance_benchmark.rb +247 -0
  27. data/examples/persistence_example.rb +102 -0
  28. data/examples/react_agent.rb +203 -0
  29. data/examples/rest_api.rb +173 -0
  30. data/examples/rest_api_advanced.rb +333 -0
  31. data/examples/scheduled_job_example.rb +116 -0
  32. data/examples/simple_qa.rb +1 -2
  33. data/examples/sinatra_api.rb +109 -0
  34. data/examples/typed_signatures.rb +1 -2
  35. data/graphql_optimization_summary.md +53 -0
  36. data/lib/desiru/api/grape_integration.rb +284 -0
  37. data/lib/desiru/api/persistence_middleware.rb +148 -0
  38. data/lib/desiru/api/sinatra_integration.rb +217 -0
  39. data/lib/desiru/api.rb +42 -0
  40. data/lib/desiru/assertions.rb +74 -0
  41. data/lib/desiru/async_status.rb +65 -0
  42. data/lib/desiru/cache.rb +1 -1
  43. data/lib/desiru/configuration.rb +2 -1
  44. data/lib/desiru/errors.rb +160 -0
  45. data/lib/desiru/field.rb +17 -14
  46. data/lib/desiru/graphql/batch_loader.rb +85 -0
  47. data/lib/desiru/graphql/data_loader.rb +242 -75
  48. data/lib/desiru/graphql/enum_builder.rb +75 -0
  49. data/lib/desiru/graphql/executor.rb +37 -4
  50. data/lib/desiru/graphql/schema_generator.rb +62 -158
  51. data/lib/desiru/graphql/type_builder.rb +138 -0
  52. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  53. data/lib/desiru/jobs/async_predict.rb +1 -1
  54. data/lib/desiru/jobs/base.rb +67 -0
  55. data/lib/desiru/jobs/batch_processor.rb +6 -6
  56. data/lib/desiru/jobs/retriable.rb +119 -0
  57. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  58. data/lib/desiru/jobs/scheduler.rb +219 -0
  59. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  60. data/lib/desiru/models/anthropic.rb +164 -0
  61. data/lib/desiru/models/base.rb +37 -3
  62. data/lib/desiru/models/open_ai.rb +151 -0
  63. data/lib/desiru/models/open_router.rb +161 -0
  64. data/lib/desiru/module.rb +59 -9
  65. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  66. data/lib/desiru/modules/majority.rb +51 -0
  67. data/lib/desiru/modules/multi_chain_comparison.rb +204 -0
  68. data/lib/desiru/modules/predict.rb +8 -1
  69. data/lib/desiru/modules/program_of_thought.rb +139 -0
  70. data/lib/desiru/modules/react.rb +273 -0
  71. data/lib/desiru/modules/retrieve.rb +4 -2
  72. data/lib/desiru/optimizers/base.rb +2 -4
  73. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  74. data/lib/desiru/optimizers/copro.rb +268 -0
  75. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  76. data/lib/desiru/persistence/database.rb +71 -0
  77. data/lib/desiru/persistence/models/api_request.rb +38 -0
  78. data/lib/desiru/persistence/models/job_result.rb +138 -0
  79. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  80. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  81. data/lib/desiru/persistence/models/training_example.rb +25 -0
  82. data/lib/desiru/persistence/models.rb +11 -0
  83. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  84. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  85. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  86. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  87. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  88. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  89. data/lib/desiru/persistence/repository.rb +29 -0
  90. data/lib/desiru/persistence/setup.rb +77 -0
  91. data/lib/desiru/persistence.rb +49 -0
  92. data/lib/desiru/registry.rb +3 -5
  93. data/lib/desiru/signature.rb +91 -24
  94. data/lib/desiru/version.rb +1 -1
  95. data/lib/desiru.rb +23 -8
  96. data/missing-features-analysis.md +192 -0
  97. metadata +63 -45
  98. data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'desiru'
6
+ require 'rack'
7
+ require 'rack/handler/webrick'
8
+
9
+ # This example demonstrates how to create a lightweight REST API using Desiru's Sinatra integration
10
+
11
+ # Configure Desiru
12
+ Desiru.configure do |config|
13
+ config.default_model = Desiru::Models::OpenAI.new(
14
+ api_key: ENV['OPENAI_API_KEY'] || 'your-api-key',
15
+ model: 'gpt-3.5-turbo'
16
+ )
17
+ end
18
+
19
+ # Define a simple text processing module
20
+ class TextProcessor < Desiru::Module
21
+ signature 'TextProcessor', 'Process and analyze text'
22
+
23
+ input 'text', type: 'string', desc: 'Text to process'
24
+ input 'operation', type: 'string', desc: 'Operation to perform (uppercase, lowercase, reverse, analyze)'
25
+
26
+ output 'result', type: 'string', desc: 'Processed text result'
27
+ output 'metadata', type: 'dict', desc: 'Additional metadata about the operation'
28
+
29
+ def forward(text:, operation:)
30
+ result = case operation.downcase
31
+ when 'uppercase'
32
+ text.upcase
33
+ when 'lowercase'
34
+ text.downcase
35
+ when 'reverse'
36
+ text.reverse
37
+ when 'analyze'
38
+ "Text has #{text.length} characters and #{text.split.length} words"
39
+ else
40
+ raise ArgumentError, "Unknown operation: #{operation}"
41
+ end
42
+
43
+ {
44
+ result: result,
45
+ metadata: {
46
+ original_length: text.length,
47
+ processed_at: Time.now.iso8601,
48
+ operation: operation
49
+ }
50
+ }
51
+ end
52
+ end
53
+
54
+ # Define a calculator module
55
+ class Calculator < Desiru::Module
56
+ signature 'Calculator', 'Perform basic math operations'
57
+
58
+ input 'num1', type: 'float', desc: 'First number'
59
+ input 'num2', type: 'float', desc: 'Second number'
60
+ input 'operation', type: 'string', desc: 'Operation (+, -, *, /)'
61
+
62
+ output 'result', type: 'float', desc: 'Calculation result'
63
+
64
+ def forward(num1:, num2:, operation:)
65
+ result = case operation
66
+ when '+' then num1 + num2
67
+ when '-' then num1 - num2
68
+ when '*' then num1 * num2
69
+ when '/'
70
+ raise ArgumentError, "Division by zero" if num2.zero?
71
+
72
+ num1 / num2
73
+ else
74
+ raise ArgumentError, "Unknown operation: #{operation}"
75
+ end
76
+
77
+ { result: result }
78
+ end
79
+ end
80
+
81
+ # Create Sinatra API integration
82
+ api = Desiru::API.sinatra(async_enabled: true) do
83
+ register_module '/text', TextProcessor.new,
84
+ description: 'Process text with various operations'
85
+
86
+ register_module '/calculate', Calculator.new,
87
+ description: 'Perform basic math calculations'
88
+ end
89
+
90
+ # Create a Rack app with the API
91
+ app = api.to_rack_app
92
+
93
+ puts "Starting Sinatra-based Desiru API server on http://localhost:9293"
94
+ puts "\nAvailable endpoints:"
95
+ puts " POST /api/v1/health - Health check"
96
+ puts " POST /api/v1/text - Text processing"
97
+ puts " POST /api/v1/calculate - Calculator"
98
+ puts " POST /api/v1/async/text - Async text processing"
99
+ puts " POST /api/v1/async/calculate - Async calculator"
100
+ puts " GET /api/v1/jobs/:job_id - Check async job status"
101
+ puts "\nExample requests:"
102
+ puts " curl -X POST http://localhost:9293/api/v1/text " \
103
+ "-H 'Content-Type: application/json' -d '{\"text\": \"Hello World\", \"operation\": \"uppercase\"}'"
104
+ puts " curl -X POST http://localhost:9293/api/v1/calculate " \
105
+ "-H 'Content-Type: application/json' -d '{\"num1\": 10, \"num2\": 5, \"operation\": \"+\"}'"
106
+ puts "\nPress Ctrl+C to stop the server"
107
+
108
+ # Start the server
109
+ Rack::Handler::WEBrick.run app, Port: 9293
@@ -6,8 +6,7 @@ require 'desiru'
6
6
 
7
7
  # Configure Desiru
8
8
  Desiru.configure do |config|
9
- config.default_model = Desiru::Models::RaixAdapter.new(
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
  )
@@ -0,0 +1,53 @@
1
+ # GraphQL Optimization Summary
2
+
3
+ ## Overview
4
+ This document summarizes the GraphQL optimizations implemented in the Desiru project.
5
+
6
+ ## Optimizations Implemented
7
+
8
+ ### 1. Type Cache Key Generation (type_builder.rb)
9
+ - **Before**: Used string concatenation for cache keys, resulting in long strings like "Output:id:string:false:nil|name:string:true:nil"
10
+ - **After**: Uses hash-based approach for more compact keys like "Output:1403724691813815013"
11
+ - **Benefit**: Reduced memory usage and faster cache lookups
12
+
13
+ ### 2. Enum Type Extraction (enum_builder.rb)
14
+ - **Before**: Enum generation logic was embedded in TypeBuilder module
15
+ - **After**: Extracted into separate EnumBuilder module
16
+ - **Benefit**: Better separation of concerns, reduced module complexity
17
+
18
+ ### 3. Type Cache Warmer (type_cache_warmer.rb)
19
+ - **New Feature**: Pre-generates commonly used GraphQL types
20
+ - **Benefit**: Improves cold-start performance by warming the cache with common field combinations
21
+
22
+ ### 4. Request Deduplication (data_loader.rb)
23
+ - **Feature**: Groups identical requests within a single GraphQL query
24
+ - **Benefit**: Up to 90% performance improvement for queries with duplicate requests
25
+ - **Use Case**: Prevents N+1 query problems in nested GraphQL queries
26
+
27
+ ## Performance Improvements
28
+
29
+ Based on the performance benchmark:
30
+ - Queries with duplicate requests show ~90% performance improvement
31
+ - Deduplication ratio of 5:1 (50 requests reduced to 10 unique)
32
+ - Batch processing reduces overhead for multiple similar requests
33
+
34
+ ## Code Quality Improvements
35
+
36
+ 1. **RuboCop Compliance**: All GraphQL files now pass RuboCop linting
37
+ 2. **Modular Design**: Clear separation between TypeBuilder, EnumBuilder, and TypeCacheWarmer
38
+ 3. **Thread Safety**: Proper mutex usage for concurrent access to type cache
39
+
40
+ ## Files Modified
41
+
42
+ - `lib/desiru/graphql/type_builder.rb` - Optimized cache key generation
43
+ - `lib/desiru/graphql/enum_builder.rb` - New module for enum type building
44
+ - `lib/desiru/graphql/type_cache_warmer.rb` - New utility for cache warming
45
+ - `lib/desiru/graphql/schema_generator.rb` - Minor refactoring for RuboCop
46
+
47
+ ## Testing
48
+
49
+ All GraphQL tests pass successfully:
50
+ - 57 examples, 0 failures in GraphQL specs
51
+ - Request deduplication working correctly
52
+ - Type caching functioning as expected
53
+ - Cache warming creates 12 pre-built types (7 output types, 5 enum types)
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape'
4
+ require 'json'
5
+ require 'rack/cors'
6
+
7
+ module Desiru
8
+ module API
9
+ # Grape integration for Desiru - automatically generate REST API endpoints from signatures
10
+ class GrapeIntegration
11
+ attr_reader :modules, :async_enabled, :stream_enabled
12
+
13
+ def initialize(async_enabled: true, stream_enabled: false)
14
+ @modules = {}
15
+ @async_enabled = async_enabled
16
+ @stream_enabled = stream_enabled
17
+ end
18
+
19
+ # Register a Desiru module with an endpoint path
20
+ def register_module(path, desiru_module, description: nil)
21
+ @modules[path] = {
22
+ module: desiru_module,
23
+ description: description || "Endpoint for #{desiru_module.class.name}"
24
+ }
25
+ end
26
+
27
+ # Generate a Grape API class with all registered modules
28
+ def generate_api
29
+ modules_config = @modules
30
+ async = @async_enabled
31
+ stream = @stream_enabled
32
+
33
+ Class.new(Grape::API) do
34
+ format :json
35
+ prefix :api
36
+ version 'v1', using: :path
37
+
38
+ # Define class method for type conversion
39
+ def self.grape_type_for(type_string)
40
+ case type_string.to_s.downcase
41
+ when 'integer', 'int'
42
+ Integer
43
+ when 'float'
44
+ Float
45
+ when 'boolean', 'bool'
46
+ Grape::API::Boolean
47
+ when /^list/
48
+ Array
49
+ else
50
+ String # Default to String for unknown types (including 'string', 'str')
51
+ end
52
+ end
53
+
54
+ helpers do
55
+ def validate_params(signature, params)
56
+ errors = {}
57
+
58
+ signature.input_fields.each do |name, field|
59
+ value = params[name]
60
+
61
+ # Check required fields
62
+ if value.nil? && !field.optional?
63
+ errors[name] = "is required"
64
+ next
65
+ end
66
+
67
+ # Type validation
68
+ next unless value && field.type
69
+
70
+ errors[name] = "must be of type #{field.type}" unless validate_type(value, field.type)
71
+ end
72
+
73
+ errors
74
+ end
75
+
76
+ def validate_type(value, expected_type)
77
+ case expected_type.to_s.downcase
78
+ when 'string', 'str'
79
+ value.is_a?(String)
80
+ when 'integer', 'int'
81
+ value.is_a?(Integer) || (value.is_a?(String) && value.match?(/^\d+$/))
82
+ when 'float'
83
+ value.is_a?(Numeric)
84
+ when 'boolean', 'bool'
85
+ [true, false, 'true', 'false'].include?(value)
86
+ when /^list/
87
+ value.is_a?(Array)
88
+ else
89
+ true # Unknown types and literals pass validation
90
+ end
91
+ end
92
+
93
+ def format_response(result)
94
+ if result.is_a?(Hash)
95
+ result
96
+ else
97
+ { result: result }
98
+ end
99
+ end
100
+
101
+ def handle_async_request(desiru_module, inputs)
102
+ result = desiru_module.call_async(inputs)
103
+
104
+ {
105
+ job_id: result.job_id,
106
+ status: result.status,
107
+ progress: result.progress,
108
+ status_url: "/api/v1/jobs/#{result.job_id}"
109
+ }
110
+ end
111
+ end
112
+
113
+ # Health check endpoint
114
+ desc 'Health check'
115
+ get '/health' do
116
+ { status: 'ok', timestamp: Time.now.iso8601 }
117
+ end
118
+
119
+ # Generate endpoints for each registered module
120
+ modules_config.each do |path, config|
121
+ desiru_module = config[:module]
122
+ description = config[:description]
123
+
124
+ desc description
125
+ params do
126
+ # Generate params from signature
127
+ desiru_module.signature.input_fields.each do |name, field|
128
+ # Convert Desiru types to Grape types
129
+ grape_type = case field.type.to_s.downcase
130
+ when 'integer', 'int'
131
+ Integer
132
+ when 'float'
133
+ Float
134
+ when 'boolean', 'bool'
135
+ Grape::API::Boolean
136
+ when /^list/
137
+ Array
138
+ else
139
+ String # Default to String for unknown types (including 'string', 'str')
140
+ end
141
+
142
+ optional name, type: grape_type, desc: field.description
143
+ end
144
+ end
145
+
146
+ post path do
147
+ # Validate parameters
148
+ validation_errors = validate_params(desiru_module.signature, params)
149
+
150
+ error!({ errors: validation_errors }, 422) if validation_errors.any?
151
+
152
+ # Prepare inputs with symbolized keys
153
+ inputs = {}
154
+ desiru_module.signature.input_fields.each_key do |key|
155
+ value = params[key.to_s] || params[key.to_sym]
156
+ inputs[key] = value if value
157
+ end
158
+
159
+ begin
160
+ if async && params[:async] == true && desiru_module.respond_to?(:call_async)
161
+ # Handle async request
162
+ status 202
163
+ handle_async_request(desiru_module, inputs)
164
+ elsif params[:async] == true
165
+ # Module doesn't support async
166
+ error!({ error: 'This module does not support async execution' }, 400)
167
+ else
168
+ # Synchronous execution
169
+ result = desiru_module.call(inputs)
170
+ status 201
171
+ format_response(result)
172
+ end
173
+ rescue StandardError => e
174
+ error!({ error: e.message }, 500)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Job status endpoint if async is enabled
180
+ if async
181
+ namespace :jobs do
182
+ desc 'Get job status'
183
+ params do
184
+ requires :id, type: String, desc: 'Job ID'
185
+ end
186
+ get ':id' do
187
+ status = Desiru::AsyncStatus.new(params[:id])
188
+
189
+ response = {
190
+ job_id: params[:id],
191
+ status: status.status,
192
+ progress: status.progress
193
+ }
194
+
195
+ response[:result] = status.result if status.ready?
196
+
197
+ response
198
+ rescue StandardError
199
+ error!({ error: "Job not found" }, 404)
200
+ end
201
+ end
202
+ end
203
+
204
+ # Add streaming endpoint support if enabled
205
+ if stream
206
+ namespace :stream do
207
+ modules_config.each do |path, config|
208
+ desiru_module = config[:module]
209
+ description = "#{config[:description]} (streaming)"
210
+
211
+ desc description
212
+ params do
213
+ desiru_module.signature.input_fields.each do |name, field|
214
+ # Convert Desiru types to Grape types
215
+ grape_type = case field.type.to_s.downcase
216
+ when 'integer', 'int'
217
+ Integer
218
+ when 'float'
219
+ Float
220
+ when 'boolean', 'bool'
221
+ Grape::API::Boolean
222
+ when /^list/
223
+ Array
224
+ else
225
+ String # Default to String for unknown types (including 'string', 'str')
226
+ end
227
+
228
+ optional name, type: grape_type, desc: field.description
229
+ end
230
+ end
231
+
232
+ post path do
233
+ content_type 'text/event-stream'
234
+ headers['Cache-Control'] = 'no-cache'
235
+ headers['X-Accel-Buffering'] = 'no'
236
+ status 200
237
+
238
+ stream do |out|
239
+ inputs = {}
240
+ desiru_module.signature.input_fields.each_key do |key|
241
+ inputs[key] = params[key.to_s] if params.key?(key.to_s)
242
+ end
243
+
244
+ # For now, just send the result as a single event
245
+ # Future: integrate with actual streaming from LLM
246
+ result = desiru_module.call(inputs)
247
+
248
+ out << "event: result\n"
249
+ out << "data: #{JSON.generate(format_response(result))}\n\n"
250
+
251
+ out << "event: done\n"
252
+ out << "data: {\"status\": \"complete\"}\n\n"
253
+ rescue StandardError => e
254
+ out << "event: error\n"
255
+ out << "data: #{JSON.generate({ error: e.message })}\n\n"
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Mount the API in a Rack application
265
+ def to_rack_app
266
+ api = generate_api
267
+
268
+ Rack::Builder.new do
269
+ use Rack::Cors do
270
+ allow do
271
+ origins '*'
272
+ resource '*',
273
+ headers: :any,
274
+ methods: %i[get post put patch delete options head],
275
+ expose: ['Access-Control-Allow-Origin']
276
+ end
277
+ end
278
+
279
+ run api
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module Desiru
6
+ module API
7
+ # Rack middleware for tracking API requests and module executions
8
+ class PersistenceMiddleware
9
+ def initialize(app, enabled: true)
10
+ @app = app
11
+ @enabled = enabled
12
+ end
13
+
14
+ def call(env)
15
+ return @app.call(env) unless @enabled && persistence_available?
16
+
17
+ start_time = Time.now
18
+
19
+ # Create request record
20
+ request = Rack::Request.new(env)
21
+
22
+ # Call the app
23
+ status, headers, body = @app.call(env)
24
+
25
+ # Calculate response time
26
+ response_time = Time.now - start_time
27
+
28
+ # Store the request and response
29
+ store_request(request, status, headers, body, response_time)
30
+
31
+ # Return the response
32
+ [status, headers, body]
33
+ rescue StandardError => e
34
+ # Log error but don't fail the request
35
+ warn "PersistenceMiddleware error: #{e.message}"
36
+ [status, headers, body] || [500, {}, ['Internal Server Error']]
37
+ end
38
+
39
+ private
40
+
41
+ def persistence_available?
42
+ defined?(Desiru::Persistence) &&
43
+ Desiru::Persistence::Database.connection &&
44
+ Desiru::Persistence::Setup.initialized?
45
+ rescue StandardError
46
+ false
47
+ end
48
+
49
+ def store_request(request, status, _headers, body, response_time)
50
+ # Only track API endpoints
51
+ return unless request.path_info.start_with?('/api/')
52
+
53
+ api_requests = Desiru::Persistence[:api_requests]
54
+
55
+ api_request = api_requests.create(
56
+ method: request.request_method,
57
+ path: request.path_info,
58
+ remote_ip: request.ip,
59
+ headers: extract_headers(request),
60
+ params: extract_params(request),
61
+ status_code: status,
62
+ response_body: extract_body(body),
63
+ response_time: response_time
64
+ )
65
+
66
+ # Store module execution if available
67
+ store_module_execution(api_request.id, request, body) if api_request
68
+ rescue StandardError => e
69
+ warn "Failed to store request: #{e.message}"
70
+ end
71
+
72
+ def extract_headers(request)
73
+ headers = {}
74
+ request.each_header do |key, value|
75
+ next unless key.start_with?('HTTP_') || key == 'CONTENT_TYPE'
76
+
77
+ header_name = key.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
78
+ headers[header_name] = value
79
+ end
80
+ headers
81
+ end
82
+
83
+ def extract_params(request)
84
+ if request.content_type&.include?('application/json')
85
+ request.body.rewind
86
+ JSON.parse(request.body.read)
87
+ else
88
+ request.params
89
+ end
90
+ rescue StandardError
91
+ request.params
92
+ end
93
+
94
+ def extract_body(body)
95
+ return nil unless body.respond_to?(:each)
96
+
97
+ content = body.map { |part| part }
98
+
99
+ # Try to parse as JSON
100
+ JSON.parse(content.join)
101
+ rescue StandardError
102
+ content.join
103
+ end
104
+
105
+ def store_module_execution(api_request_id, request, body)
106
+ # Extract module info from path (e.g., /api/v1/summarize -> summarize)
107
+ module_path = request.path_info.gsub(%r{^/api/v\d+/}, '')
108
+ return unless module_path && !module_path.empty?
109
+
110
+ params = extract_params(request)
111
+ result = extract_body(body)
112
+
113
+ module_executions = Desiru::Persistence[:module_executions]
114
+
115
+ execution = module_executions.create_for_module(
116
+ module_path.capitalize,
117
+ params,
118
+ api_request_id: api_request_id
119
+ )
120
+
121
+ # Mark as completed if we have a result
122
+ if result.is_a?(Hash) && !result['error']
123
+ module_executions.complete(execution.id, result)
124
+ elsif result.is_a?(Hash) && result['error']
125
+ module_executions.fail(execution.id, result['error'])
126
+ end
127
+ rescue StandardError => e
128
+ warn "Failed to store module execution: #{e.message}"
129
+ end
130
+ end
131
+
132
+ # Extension for API integrations to add persistence
133
+ module PersistenceExtension
134
+ def with_persistence(enabled: true)
135
+ original_app = to_rack_app
136
+
137
+ Rack::Builder.new do
138
+ use PersistenceMiddleware, enabled: enabled
139
+ run original_app
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ # Add extension to API integrations
147
+ Desiru::API::GrapeIntegration.include(Desiru::API::PersistenceExtension) if defined?(Desiru::API::GrapeIntegration)
148
+ Desiru::API::SinatraIntegration.include(Desiru::API::PersistenceExtension) if defined?(Desiru::API::SinatraIntegration)