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,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
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
# Assertion system for validating module outputs
|
5
|
+
module Assertions
|
6
|
+
# Error raised when an assertion fails
|
7
|
+
class AssertionError < StandardError
|
8
|
+
attr_reader :module_name, :retry_count
|
9
|
+
|
10
|
+
def initialize(message = nil, module_name: nil, retry_count: 0)
|
11
|
+
super(message)
|
12
|
+
@module_name = module_name
|
13
|
+
@retry_count = retry_count
|
14
|
+
end
|
15
|
+
|
16
|
+
# Assertions should trigger module retries
|
17
|
+
def retriable?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Assert that a condition is true, raising AssertionError if false
|
23
|
+
# @param condition [Boolean] The condition to check
|
24
|
+
# @param message [String] Optional error message
|
25
|
+
# @raise [AssertionError] if condition is false
|
26
|
+
def self.assert(condition, message = nil)
|
27
|
+
return if condition
|
28
|
+
|
29
|
+
message ||= 'Assertion failed'
|
30
|
+
raise AssertionError, message
|
31
|
+
end
|
32
|
+
|
33
|
+
# Suggest that a condition should be true, logging a warning if false
|
34
|
+
# @param condition [Boolean] The condition to check
|
35
|
+
# @param message [String] Optional warning message
|
36
|
+
def self.suggest(condition, message = nil)
|
37
|
+
return if condition
|
38
|
+
|
39
|
+
message ||= 'Suggestion failed'
|
40
|
+
Desiru.logger.warn("[SUGGESTION] #{message}")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Configuration for assertion behavior
|
44
|
+
class Configuration
|
45
|
+
attr_accessor :max_assertion_retries, :assertion_retry_delay, :log_assertions, :track_assertion_metrics
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@max_assertion_retries = 3
|
49
|
+
@assertion_retry_delay = 0.1 # seconds
|
50
|
+
@log_assertions = true
|
51
|
+
@track_assertion_metrics = false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get or set the assertion configuration
|
56
|
+
def self.configuration
|
57
|
+
@configuration ||= Configuration.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# Configure assertion behavior
|
61
|
+
def self.configure
|
62
|
+
yield(configuration) if block_given?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Module-level convenience methods
|
67
|
+
def self.assert(condition, message = nil)
|
68
|
+
Assertions.assert(condition, message)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.suggest(condition, message = nil)
|
72
|
+
Assertions.suggest(condition, message)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Desiru
|
7
|
+
# AsyncStatus provides a simple interface for checking job status
|
8
|
+
# Compatible with the REST API's job status endpoint
|
9
|
+
class AsyncStatus
|
10
|
+
attr_reader :job_id
|
11
|
+
|
12
|
+
def initialize(job_id)
|
13
|
+
@job_id = job_id
|
14
|
+
@redis = Redis.new(url: Desiru.configuration.redis_url || ENV.fetch('REDIS_URL', nil))
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
status_data = fetch_status
|
19
|
+
return 'pending' unless status_data
|
20
|
+
|
21
|
+
status_data[:status] || 'pending'
|
22
|
+
end
|
23
|
+
|
24
|
+
def progress
|
25
|
+
status_data = fetch_status
|
26
|
+
return 0 unless status_data
|
27
|
+
|
28
|
+
status_data[:progress] || 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def ready?
|
32
|
+
result_data = fetch_result
|
33
|
+
!result_data.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def result
|
37
|
+
result_data = fetch_result
|
38
|
+
return nil unless result_data
|
39
|
+
|
40
|
+
raise ModuleError, "Async job failed: #{result_data[:error]}" unless result_data[:success]
|
41
|
+
|
42
|
+
result_data[:result]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def fetch_status
|
48
|
+
raw = @redis.get("desiru:status:#{job_id}")
|
49
|
+
return nil unless raw
|
50
|
+
|
51
|
+
JSON.parse(raw, symbolize_names: true)
|
52
|
+
rescue JSON::ParserError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def fetch_result
|
57
|
+
raw = @redis.get("desiru:results:#{job_id}")
|
58
|
+
return nil unless raw
|
59
|
+
|
60
|
+
JSON.parse(raw, symbolize_names: true)
|
61
|
+
rescue JSON::ParserError
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/desiru/cache.rb
CHANGED
data/lib/desiru/configuration.rb
CHANGED
@@ -6,7 +6,7 @@ module Desiru
|
|
6
6
|
class Configuration
|
7
7
|
attr_accessor :default_model, :cache_enabled, :cache_ttl, :max_retries,
|
8
8
|
:retry_delay, :logger, :module_registry, :model_timeout,
|
9
|
-
:redis_url
|
9
|
+
:redis_url, :retry_count
|
10
10
|
|
11
11
|
def initialize
|
12
12
|
@default_model = nil
|
@@ -14,6 +14,7 @@ module Desiru
|
|
14
14
|
@cache_ttl = 3600 # 1 hour
|
15
15
|
@max_retries = 3
|
16
16
|
@retry_delay = 1
|
17
|
+
@retry_count = 3
|
17
18
|
@logger = default_logger
|
18
19
|
@module_registry = Desiru::Registry.instance
|
19
20
|
@model_timeout = 30
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
# Base error class moved here for organization
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :context, :original_error
|
7
|
+
|
8
|
+
def initialize(message = nil, context: {}, original_error: nil)
|
9
|
+
@context = context
|
10
|
+
@original_error = original_error
|
11
|
+
|
12
|
+
super(build_message(message))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def build_message(message)
|
18
|
+
parts = [message || self.class.name.split('::').last]
|
19
|
+
|
20
|
+
if context.any?
|
21
|
+
context_str = context.map { |k, v| "#{k}: #{v}" }.join(', ')
|
22
|
+
parts << "(#{context_str})"
|
23
|
+
end
|
24
|
+
|
25
|
+
parts << "caused by #{original_error.class}: #{original_error.message}" if original_error
|
26
|
+
|
27
|
+
parts.join(' ')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Configuration errors
|
32
|
+
class ConfigurationError < Error; end
|
33
|
+
|
34
|
+
# Signature and validation errors
|
35
|
+
class SignatureError < Error; end
|
36
|
+
class ValidationError < Error; end
|
37
|
+
|
38
|
+
# Module execution errors
|
39
|
+
class ModuleError < Error; end
|
40
|
+
class TimeoutError < ModuleError; end
|
41
|
+
|
42
|
+
# Network and API errors
|
43
|
+
class NetworkError < Error; end
|
44
|
+
|
45
|
+
class RateLimitError < NetworkError
|
46
|
+
attr_reader :retry_after
|
47
|
+
|
48
|
+
def initialize(message = nil, retry_after: nil, **)
|
49
|
+
@retry_after = retry_after
|
50
|
+
super(message, **)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class AuthenticationError < NetworkError; end
|
55
|
+
|
56
|
+
# Model/LLM specific errors
|
57
|
+
class ModelError < Error; end
|
58
|
+
|
59
|
+
class TokenLimitError < ModelError
|
60
|
+
attr_reader :token_count, :token_limit
|
61
|
+
|
62
|
+
def initialize(message = nil, token_count: nil, token_limit: nil, **)
|
63
|
+
@token_count = token_count
|
64
|
+
@token_limit = token_limit
|
65
|
+
super(message, **)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class InvalidResponseError < ModelError; end
|
70
|
+
class ModelNotAvailableError < ModelError; end
|
71
|
+
|
72
|
+
# Job and async related errors
|
73
|
+
class JobError < Error; end
|
74
|
+
class JobNotFoundError < JobError; end
|
75
|
+
class JobTimeoutError < JobError; end
|
76
|
+
class JobFailedError < JobError; end
|
77
|
+
|
78
|
+
# Persistence related errors
|
79
|
+
class PersistenceError < Error; end
|
80
|
+
class DatabaseConnectionError < PersistenceError; end
|
81
|
+
class RecordNotFoundError < PersistenceError; end
|
82
|
+
class RecordInvalidError < PersistenceError; end
|
83
|
+
|
84
|
+
# Optimizer related errors
|
85
|
+
class OptimizerError < Error; end
|
86
|
+
class OptimizationFailedError < OptimizerError; end
|
87
|
+
class InsufficientDataError < OptimizerError; end
|
88
|
+
|
89
|
+
# Cache related errors
|
90
|
+
class CacheError < Error; end
|
91
|
+
class CacheConnectionError < CacheError; end
|
92
|
+
|
93
|
+
# Error handling utilities
|
94
|
+
module ErrorHandling
|
95
|
+
# Wrap a block with error context
|
96
|
+
def with_error_context(context = {})
|
97
|
+
yield
|
98
|
+
rescue StandardError => e
|
99
|
+
# Add context to existing Desiru errors
|
100
|
+
raise Desiru::Error.new(e.message, context: context, original_error: e) unless e.is_a?(Desiru::Error)
|
101
|
+
|
102
|
+
e.context.merge!(context)
|
103
|
+
raise e
|
104
|
+
|
105
|
+
# Wrap other errors with context
|
106
|
+
end
|
107
|
+
|
108
|
+
# Retry with exponential backoff
|
109
|
+
def with_retry(max_attempts: 3, backoff: :exponential, retriable_errors: [NetworkError, TimeoutError])
|
110
|
+
attempt = 0
|
111
|
+
|
112
|
+
begin
|
113
|
+
attempt += 1
|
114
|
+
yield(attempt)
|
115
|
+
rescue *retriable_errors => e
|
116
|
+
raise unless attempt < max_attempts
|
117
|
+
|
118
|
+
delay = calculate_backoff(attempt, backoff)
|
119
|
+
Desiru.logger.warn "Retrying after #{delay}s (attempt #{attempt}/#{max_attempts}): #{e.message}"
|
120
|
+
sleep delay
|
121
|
+
retry
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Log and swallow errors (use sparingly)
|
126
|
+
def safe_execute(default = nil, log_level: :error)
|
127
|
+
yield
|
128
|
+
rescue StandardError => e
|
129
|
+
Desiru.logger.send(log_level, "Error in safe_execute: #{e.class} - #{e.message}")
|
130
|
+
Desiru.logger.debug e.backtrace.join("\n") if log_level == :error
|
131
|
+
default
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def calculate_backoff(attempt, strategy)
|
137
|
+
case strategy
|
138
|
+
when :exponential
|
139
|
+
[2**(attempt - 1), 60].min # Max 60 seconds
|
140
|
+
when :linear
|
141
|
+
attempt * 2
|
142
|
+
when Numeric
|
143
|
+
strategy
|
144
|
+
else
|
145
|
+
1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Include error handling in base classes
|
151
|
+
class Module
|
152
|
+
include ErrorHandling
|
153
|
+
end
|
154
|
+
|
155
|
+
module Jobs
|
156
|
+
class Base
|
157
|
+
include ErrorHandling
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/desiru/field.rb
CHANGED
@@ -3,14 +3,16 @@
|
|
3
3
|
module Desiru
|
4
4
|
# Represents a field in a signature with type information and metadata
|
5
5
|
class Field
|
6
|
-
attr_reader :name, :type, :description, :optional, :default, :validator, :literal_values, :element_type
|
6
|
+
attr_reader :name, :type, :description, :optional, :default, :validator, :literal_values, :element_type,
|
7
|
+
:original_type
|
7
8
|
|
8
9
|
alias optional? optional
|
9
10
|
|
10
11
|
def initialize(name, type = :string, description: nil, optional: false, default: nil, validator: nil,
|
11
|
-
literal_values: nil, element_type: nil)
|
12
|
+
literal_values: nil, element_type: nil, original_type: nil)
|
12
13
|
@name = name.to_sym
|
13
14
|
@type = normalize_type(type)
|
15
|
+
@original_type = original_type || type.to_s
|
14
16
|
@description = description
|
15
17
|
@optional = optional
|
16
18
|
@default = default
|
@@ -19,7 +21,7 @@ module Desiru
|
|
19
21
|
@validator = validator || default_validator
|
20
22
|
end
|
21
23
|
|
22
|
-
def
|
24
|
+
def valid?(value)
|
23
25
|
return true if optional && value.nil?
|
24
26
|
return true if value.nil? && !default.nil?
|
25
27
|
|
@@ -46,7 +48,7 @@ module Desiru
|
|
46
48
|
when 'false', 'no', '0', 'f'
|
47
49
|
false
|
48
50
|
else
|
49
|
-
|
51
|
+
!value.nil?
|
50
52
|
end
|
51
53
|
when :literal
|
52
54
|
# For literal types, ensure the value is a string and matches one of the allowed values
|
@@ -63,8 +65,9 @@ module Desiru
|
|
63
65
|
array_value.map do |elem|
|
64
66
|
coerced_elem = elem.to_s
|
65
67
|
unless element_type[:literal_values].include?(coerced_elem)
|
68
|
+
allowed = element_type[:literal_values].join(', ')
|
66
69
|
raise ValidationError,
|
67
|
-
"Array element '#{coerced_elem}' is not one of allowed values: #{
|
70
|
+
"Array element '#{coerced_elem}' is not one of allowed values: #{allowed}"
|
68
71
|
end
|
69
72
|
|
70
73
|
coerced_elem
|
@@ -146,25 +149,25 @@ module Desiru
|
|
146
149
|
def default_validator
|
147
150
|
case type
|
148
151
|
when :string
|
149
|
-
->(
|
152
|
+
->(value) { value.is_a?(String) }
|
150
153
|
when :int
|
151
|
-
->(
|
154
|
+
->(value) { value.is_a?(Integer) }
|
152
155
|
when :float
|
153
|
-
->(
|
156
|
+
->(value) { value.is_a?(Float) || value.is_a?(Integer) }
|
154
157
|
when :bool
|
155
|
-
->(
|
158
|
+
->(value) { value.is_a?(TrueClass) || value.is_a?(FalseClass) }
|
156
159
|
when :literal
|
157
|
-
->(
|
160
|
+
->(value) { value.is_a?(String) && literal_values.include?(value) }
|
158
161
|
when :list
|
159
162
|
if element_type && element_type[:type] == :literal
|
160
|
-
->(
|
163
|
+
->(value) { value.is_a?(Array) && value.all? { |elem| element_type[:literal_values].include?(elem.to_s) } }
|
161
164
|
else
|
162
|
-
->(
|
165
|
+
->(value) { value.is_a?(Array) }
|
163
166
|
end
|
164
167
|
when :hash
|
165
|
-
->(
|
168
|
+
->(value) { value.is_a?(Hash) }
|
166
169
|
else
|
167
|
-
->(
|
170
|
+
->(_value) { true }
|
168
171
|
end
|
169
172
|
end
|
170
173
|
end
|