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,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::
|
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)
|