desiru 0.1.0 → 0.2.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.local.json +11 -0
  3. data/.env.example +34 -0
  4. data/.rubocop.yml +7 -4
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +73 -0
  7. data/CLAUDE.local.md +3 -0
  8. data/CLAUDE.md +10 -1
  9. data/Gemfile +21 -2
  10. data/Gemfile.lock +88 -13
  11. data/README.md +301 -2
  12. data/Rakefile +1 -0
  13. data/db/migrations/001_create_initial_tables.rb +96 -0
  14. data/db/migrations/002_create_job_results.rb +39 -0
  15. data/desiru-development-swarm.yml +185 -0
  16. data/desiru.db +0 -0
  17. data/desiru.gemspec +2 -5
  18. data/docs/background_processing_roadmap.md +87 -0
  19. data/docs/job_scheduling.md +167 -0
  20. data/dspy-analysis-swarm.yml +60 -0
  21. data/dspy-feature-analysis.md +121 -0
  22. data/examples/README.md +69 -0
  23. data/examples/api_with_persistence.rb +122 -0
  24. data/examples/assertions_example.rb +232 -0
  25. data/examples/async_processing.rb +2 -0
  26. data/examples/few_shot_learning.rb +1 -2
  27. data/examples/graphql_api.rb +4 -2
  28. data/examples/graphql_integration.rb +3 -3
  29. data/examples/graphql_optimization_summary.md +143 -0
  30. data/examples/graphql_performance_benchmark.rb +247 -0
  31. data/examples/persistence_example.rb +102 -0
  32. data/examples/react_agent.rb +203 -0
  33. data/examples/rest_api.rb +173 -0
  34. data/examples/rest_api_advanced.rb +333 -0
  35. data/examples/scheduled_job_example.rb +116 -0
  36. data/examples/simple_qa.rb +1 -2
  37. data/examples/sinatra_api.rb +109 -0
  38. data/examples/typed_signatures.rb +1 -2
  39. data/graphql_optimization_summary.md +53 -0
  40. data/lib/desiru/api/grape_integration.rb +284 -0
  41. data/lib/desiru/api/persistence_middleware.rb +148 -0
  42. data/lib/desiru/api/sinatra_integration.rb +217 -0
  43. data/lib/desiru/api.rb +42 -0
  44. data/lib/desiru/assertions.rb +74 -0
  45. data/lib/desiru/async_status.rb +65 -0
  46. data/lib/desiru/cache.rb +1 -1
  47. data/lib/desiru/configuration.rb +2 -1
  48. data/lib/desiru/core/compiler.rb +231 -0
  49. data/lib/desiru/core/example.rb +96 -0
  50. data/lib/desiru/core/prediction.rb +108 -0
  51. data/lib/desiru/core/trace.rb +330 -0
  52. data/lib/desiru/core/traceable.rb +61 -0
  53. data/lib/desiru/core.rb +12 -0
  54. data/lib/desiru/errors.rb +160 -0
  55. data/lib/desiru/field.rb +17 -14
  56. data/lib/desiru/graphql/batch_loader.rb +85 -0
  57. data/lib/desiru/graphql/data_loader.rb +242 -75
  58. data/lib/desiru/graphql/enum_builder.rb +75 -0
  59. data/lib/desiru/graphql/executor.rb +37 -4
  60. data/lib/desiru/graphql/schema_generator.rb +62 -158
  61. data/lib/desiru/graphql/type_builder.rb +138 -0
  62. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  63. data/lib/desiru/jobs/async_predict.rb +1 -1
  64. data/lib/desiru/jobs/base.rb +67 -0
  65. data/lib/desiru/jobs/batch_processor.rb +6 -6
  66. data/lib/desiru/jobs/retriable.rb +119 -0
  67. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  68. data/lib/desiru/jobs/scheduler.rb +219 -0
  69. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  70. data/lib/desiru/models/anthropic.rb +164 -0
  71. data/lib/desiru/models/base.rb +37 -3
  72. data/lib/desiru/models/open_ai.rb +151 -0
  73. data/lib/desiru/models/open_router.rb +161 -0
  74. data/lib/desiru/module.rb +67 -9
  75. data/lib/desiru/modules/best_of_n.rb +306 -0
  76. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  77. data/lib/desiru/modules/majority.rb +51 -0
  78. data/lib/desiru/modules/multi_chain_comparison.rb +256 -0
  79. data/lib/desiru/modules/predict.rb +15 -1
  80. data/lib/desiru/modules/program_of_thought.rb +338 -0
  81. data/lib/desiru/modules/react.rb +273 -0
  82. data/lib/desiru/modules/retrieve.rb +4 -2
  83. data/lib/desiru/optimizers/base.rb +32 -4
  84. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  85. data/lib/desiru/optimizers/copro.rb +268 -0
  86. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  87. data/lib/desiru/optimizers/mipro_v2.rb +889 -0
  88. data/lib/desiru/persistence/database.rb +71 -0
  89. data/lib/desiru/persistence/models/api_request.rb +38 -0
  90. data/lib/desiru/persistence/models/job_result.rb +138 -0
  91. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  92. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  93. data/lib/desiru/persistence/models/training_example.rb +25 -0
  94. data/lib/desiru/persistence/models.rb +11 -0
  95. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  96. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  97. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  98. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  99. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  100. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  101. data/lib/desiru/persistence/repository.rb +29 -0
  102. data/lib/desiru/persistence/setup.rb +77 -0
  103. data/lib/desiru/persistence.rb +49 -0
  104. data/lib/desiru/registry.rb +3 -5
  105. data/lib/desiru/signature.rb +91 -24
  106. data/lib/desiru/version.rb +1 -1
  107. data/lib/desiru.rb +33 -8
  108. data/missing-features-analysis.md +192 -0
  109. metadata +75 -45
  110. data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -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)
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'sinatra/json'
5
+ require 'json'
6
+ require 'rack/cors'
7
+
8
+ module Desiru
9
+ module API
10
+ # Sinatra integration for Desiru - lightweight REST API generation
11
+ class SinatraIntegration
12
+ attr_reader :modules, :async_enabled, :stream_enabled
13
+
14
+ def initialize(async_enabled: true, stream_enabled: false)
15
+ @modules = {}
16
+ @async_enabled = async_enabled
17
+ @stream_enabled = stream_enabled
18
+ end
19
+
20
+ # Register a Desiru module with an endpoint path
21
+ def register_module(path, desiru_module, description: nil)
22
+ @modules[path] = {
23
+ module: desiru_module,
24
+ description: description || "Endpoint for #{desiru_module.class.name}"
25
+ }
26
+ end
27
+
28
+ # Generate a Sinatra application with all registered modules
29
+ def generate_api
30
+ modules_config = @modules
31
+ async = @async_enabled
32
+ stream = @stream_enabled
33
+
34
+ Class.new(Sinatra::Base) do
35
+ set :show_exceptions, false
36
+ set :raise_errors, true
37
+
38
+ # Helpers for parameter validation and response formatting
39
+ helpers do
40
+ def validate_type(value, type_string)
41
+ case type_string.to_s.downcase
42
+ when 'string', 'str'
43
+ value.is_a?(String)
44
+ when 'integer', 'int'
45
+ value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A-?\d+\z/))
46
+ when 'float'
47
+ value.is_a?(Numeric)
48
+ when 'boolean', 'bool'
49
+ [true, false, 'true', 'false'].include?(value)
50
+ when /^list/
51
+ value.is_a?(Array)
52
+ else
53
+ true # Unknown types and literals pass validation
54
+ end
55
+ end
56
+
57
+ def coerce_value(value, type_string)
58
+ case type_string.to_s.downcase
59
+ when 'integer', 'int'
60
+ value.to_i
61
+ when 'float'
62
+ value.to_f
63
+ when 'boolean', 'bool'
64
+ ['true', true].include?(value)
65
+ else
66
+ value
67
+ end
68
+ end
69
+
70
+ def format_response(result)
71
+ if result.is_a?(Desiru::ModuleResult)
72
+ result.data
73
+ elsif result.is_a?(Hash)
74
+ result
75
+ else
76
+ { result: result }
77
+ end
78
+ end
79
+
80
+ def handle_async_request(desiru_module, inputs)
81
+ result = desiru_module.call_async(inputs)
82
+
83
+ {
84
+ job_id: result.job_id,
85
+ status: result.status,
86
+ progress: result.progress,
87
+ status_url: "/api/v1/jobs/#{result.job_id}"
88
+ }
89
+ end
90
+
91
+ def parse_json_body
92
+ request.body.rewind
93
+ JSON.parse(request.body.read)
94
+ rescue JSON::ParserError
95
+ halt 400, json(error: 'Invalid JSON')
96
+ end
97
+ end
98
+
99
+ # Content type handling
100
+ before do
101
+ content_type :json
102
+ end
103
+
104
+ # Health check endpoint
105
+ get '/api/v1/health' do
106
+ json status: 'ok', timestamp: Time.now.iso8601
107
+ end
108
+
109
+ # Generate endpoints for each registered module
110
+ modules_config.each do |path, config|
111
+ desiru_module = config[:module]
112
+
113
+ # Main module endpoint
114
+ post "/api/v1#{path}" do
115
+ params = parse_json_body
116
+
117
+ # Convert string keys to symbols for module call
118
+ symbolized_params = {}
119
+ params.each { |k, v| symbolized_params[k.to_sym] = v }
120
+
121
+ begin
122
+ result = desiru_module.call(symbolized_params)
123
+ json format_response(result)
124
+ rescue Desiru::ModuleError => e
125
+ halt 400, json(error: e.message)
126
+ rescue StandardError => e
127
+ halt 500, json(error: e.message)
128
+ end
129
+ end
130
+
131
+ # Async endpoint
132
+ if async && desiru_module.respond_to?(:call_async)
133
+ post "/api/v1/async#{path}" do
134
+ params = parse_json_body
135
+
136
+ begin
137
+ result = handle_async_request(desiru_module, params)
138
+ status 202
139
+ json result
140
+ rescue StandardError => e
141
+ halt 500, json(error: e.message)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Streaming endpoint (Server-Sent Events)
147
+ next unless stream && desiru_module.respond_to?(:call_stream)
148
+
149
+ post "/api/v1/stream#{path}" do
150
+ content_type 'text/event-stream'
151
+ stream do |out|
152
+ params = parse_json_body
153
+
154
+ begin
155
+ desiru_module.call_stream(params) do |chunk|
156
+ out << "event: chunk\n"
157
+ out << "data: #{JSON.generate(chunk)}\n\n"
158
+ end
159
+
160
+ # Send final result
161
+ result = desiru_module.call(params)
162
+ out << "event: result\n"
163
+ out << "data: #{JSON.generate(format_response(result))}\n\n"
164
+ out << "event: done\n"
165
+ out << "data: #{JSON.generate({ status: 'complete' })}\n\n"
166
+ rescue StandardError => e
167
+ out << "event: error\n"
168
+ out << "data: #{JSON.generate(error: e.message)}\n\n"
169
+ ensure
170
+ out.close
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Job status endpoint for async requests
177
+ if async
178
+ get '/api/v1/jobs/:job_id' do
179
+ job_id = params[:job_id]
180
+
181
+ if Desiru.respond_to?(:check_job_status)
182
+ status = Desiru.check_job_status(job_id)
183
+
184
+ if status
185
+ json status
186
+ else
187
+ halt 404, json(error: 'Job not found')
188
+ end
189
+ else
190
+ halt 501, json(error: 'Async job tracking not implemented')
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ # Mount the API in a Rack application with CORS
198
+ def to_rack_app
199
+ api = generate_api
200
+
201
+ Rack::Builder.new do
202
+ use Rack::Cors do
203
+ allow do
204
+ origins '*'
205
+ resource '*',
206
+ headers: :any,
207
+ methods: %i[get post put patch delete options head],
208
+ expose: ['Access-Control-Allow-Origin']
209
+ end
210
+ end
211
+
212
+ run api
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
data/lib/desiru/api.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api/grape_integration'
4
+ require_relative 'api/sinatra_integration'
5
+ require_relative 'api/persistence_middleware'
6
+
7
+ module Desiru
8
+ module API
9
+ # Convenience method to create a new API integration
10
+ # @param framework [Symbol] :grape or :sinatra (default: :grape)
11
+ def self.create(framework: :grape, async_enabled: true, stream_enabled: false, &)
12
+ klass = case framework
13
+ when :grape
14
+ GrapeIntegration
15
+ when :sinatra
16
+ SinatraIntegration
17
+ else
18
+ raise ArgumentError, "Unknown framework: #{framework}. Use :grape or :sinatra"
19
+ end
20
+
21
+ integration = klass.new(
22
+ async_enabled: async_enabled,
23
+ stream_enabled: stream_enabled
24
+ )
25
+
26
+ # Allow DSL-style configuration
27
+ integration.instance_eval(&) if block_given?
28
+
29
+ integration
30
+ end
31
+
32
+ # Convenience method to create a new Grape API (backward compatibility)
33
+ def self.grape(async_enabled: true, stream_enabled: false, &)
34
+ create(framework: :grape, async_enabled: async_enabled, stream_enabled: stream_enabled, &)
35
+ end
36
+
37
+ # Convenience method to create a new Sinatra API
38
+ def self.sinatra(async_enabled: true, stream_enabled: false, &)
39
+ create(framework: :sinatra, async_enabled: async_enabled, stream_enabled: stream_enabled, &)
40
+ end
41
+ end
42
+ end