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
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'graphql'
|
4
4
|
require_relative 'data_loader'
|
5
|
+
require_relative 'batch_loader'
|
6
|
+
require_relative 'type_builder'
|
5
7
|
|
6
8
|
module Desiru
|
7
9
|
module GraphQL
|
@@ -12,11 +14,16 @@ module Desiru
|
|
12
14
|
def initialize
|
13
15
|
@signatures = {}
|
14
16
|
@modules = {}
|
15
|
-
@type_cache = {}
|
17
|
+
@type_cache = {} # Instance cache for schema-specific types
|
16
18
|
@schema_class = nil
|
17
19
|
@data_loader = DataLoader.new
|
18
20
|
end
|
19
21
|
|
22
|
+
# Clear the global type cache (useful for testing or reloading)
|
23
|
+
def self.clear_type_cache!
|
24
|
+
TypeBuilder.clear_type_cache!
|
25
|
+
end
|
26
|
+
|
20
27
|
# Register a signature with a name for GraphQL query/mutation
|
21
28
|
def register_signature(name, signature)
|
22
29
|
@signatures[name] = signature
|
@@ -36,47 +43,68 @@ module Desiru
|
|
36
43
|
|
37
44
|
# Generate a GraphQL schema from registered signatures
|
38
45
|
def generate_schema
|
39
|
-
|
46
|
+
# Always rebuild if signatures have changed
|
47
|
+
return @schema_class if @schema_class && @last_signature_count == @signatures.size
|
40
48
|
|
49
|
+
@last_signature_count = @signatures.size
|
41
50
|
query_class = build_query_type
|
42
51
|
|
43
52
|
@schema_class = Class.new(::GraphQL::Schema) do
|
44
53
|
query(query_class) if query_class
|
54
|
+
|
55
|
+
# Enable GraphQL's built-in dataloader
|
56
|
+
use ::GraphQL::Dataloader
|
57
|
+
|
58
|
+
# Enable lazy execution for batch loading
|
59
|
+
lazy_resolve(::GraphQL::Execution::Lazy, :value)
|
45
60
|
end
|
46
61
|
|
47
62
|
@schema_class
|
48
63
|
end
|
49
64
|
|
65
|
+
def add_query_field(query_class, field_name, field_def)
|
66
|
+
# Add field directly without resolver class
|
67
|
+
query_class.field field_name, field_def[:type],
|
68
|
+
null: false,
|
69
|
+
description: field_def[:description] do
|
70
|
+
# Add arguments
|
71
|
+
field_def[:arguments].each do |arg_name, arg_def|
|
72
|
+
argument arg_name, arg_def[:type], required: arg_def[:required]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Define the resolver method for this field
|
77
|
+
query_class.define_method field_name do |**args|
|
78
|
+
if field_def[:module_instance].respond_to?(:batch_forward)
|
79
|
+
# Get the dataloader for this request
|
80
|
+
dataloader = context.dataloader
|
81
|
+
# Load through the dataloader
|
82
|
+
dataloader.with(
|
83
|
+
Desiru::GraphQL::ModuleLoader,
|
84
|
+
field_name,
|
85
|
+
field_def[:modules]
|
86
|
+
).load(args)
|
87
|
+
else
|
88
|
+
# Direct execution
|
89
|
+
field_def[:resolver].call(args, context)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
50
94
|
private
|
51
95
|
|
52
96
|
def build_query_type
|
53
97
|
return nil if @signatures.empty?
|
54
98
|
|
55
99
|
query_fields = build_query_fields
|
100
|
+
query_class_builder = self
|
56
101
|
|
57
102
|
Class.new(::GraphQL::Schema::Object) do
|
58
103
|
graphql_name 'Query'
|
59
104
|
description 'Desiru query operations'
|
60
105
|
|
61
106
|
query_fields.each do |field_name, field_def|
|
62
|
-
|
63
|
-
resolver_class = Class.new(::GraphQL::Schema::Resolver) do
|
64
|
-
# Set the return type
|
65
|
-
type field_def[:type], null: false
|
66
|
-
|
67
|
-
# Add arguments
|
68
|
-
field_def[:arguments].each do |arg_name, arg_def|
|
69
|
-
argument arg_name, arg_def[:type], required: arg_def[:required]
|
70
|
-
end
|
71
|
-
|
72
|
-
# Define resolve method
|
73
|
-
define_method :resolve do |**args|
|
74
|
-
field_def[:resolver].call(args)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Add field with resolver
|
79
|
-
field field_name, resolver: resolver_class, description: field_def[:description]
|
107
|
+
query_class_builder.add_query_field(self, field_name, field_def)
|
80
108
|
end
|
81
109
|
end
|
82
110
|
end
|
@@ -85,12 +113,12 @@ module Desiru
|
|
85
113
|
fields = {}
|
86
114
|
|
87
115
|
@signatures.each do |operation_name, signature|
|
88
|
-
output_type = build_output_type(signature)
|
116
|
+
output_type = TypeBuilder.build_output_type(signature)
|
89
117
|
|
90
118
|
arguments = {}
|
91
119
|
signature.input_fields.each do |field_name, field|
|
92
120
|
arguments[camelcase_field_name(field_name)] = {
|
93
|
-
type: graphql_type_for_field(field),
|
121
|
+
type: TypeBuilder.graphql_type_for_field(field),
|
94
122
|
required: !field.optional
|
95
123
|
}
|
96
124
|
end
|
@@ -99,7 +127,9 @@ module Desiru
|
|
99
127
|
type: output_type,
|
100
128
|
description: "Generated from signature: #{signature.raw_signature}",
|
101
129
|
arguments: arguments,
|
102
|
-
resolver: ->(args) { execute_signature(operation_name, signature, args) }
|
130
|
+
resolver: ->(args, context) { execute_signature(operation_name, signature, args, context) },
|
131
|
+
module_instance: @modules[operation_name],
|
132
|
+
modules: @modules
|
103
133
|
}
|
104
134
|
end
|
105
135
|
|
@@ -111,151 +141,25 @@ module Desiru
|
|
111
141
|
nil
|
112
142
|
end
|
113
143
|
|
114
|
-
def
|
115
|
-
type_name = "Output#{signature.object_id}"
|
116
|
-
return @type_cache[type_name] if @type_cache[type_name]
|
117
|
-
|
118
|
-
output_field_defs = {}
|
119
|
-
signature.output_fields.each do |field_name, field|
|
120
|
-
output_field_defs[camelcase_field_name(field_name)] = {
|
121
|
-
type: graphql_type_for_field(field),
|
122
|
-
null: field.optional,
|
123
|
-
description: field.description
|
124
|
-
}
|
125
|
-
end
|
126
|
-
|
127
|
-
output_type = Class.new(::GraphQL::Schema::Object) do
|
128
|
-
graphql_name type_name
|
129
|
-
description 'Generated output type'
|
130
|
-
|
131
|
-
output_field_defs.each do |field_name, field_def|
|
132
|
-
field field_name, field_def[:type],
|
133
|
-
null: field_def[:null],
|
134
|
-
description: field_def[:description]
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
@type_cache[type_name] = output_type
|
139
|
-
end
|
140
|
-
|
141
|
-
def graphql_type_for_field(field)
|
142
|
-
base_type = case field.type
|
143
|
-
when :string
|
144
|
-
::GraphQL::Types::String
|
145
|
-
when :int, :integer
|
146
|
-
::GraphQL::Types::Int
|
147
|
-
when :float
|
148
|
-
::GraphQL::Types::Float
|
149
|
-
when :bool, :boolean
|
150
|
-
::GraphQL::Types::Boolean
|
151
|
-
when :list
|
152
|
-
# Handle list types
|
153
|
-
element_type = graphql_type_for_element(field.element_type)
|
154
|
-
[element_type]
|
155
|
-
when :literal
|
156
|
-
# Create enum type for literal values
|
157
|
-
create_enum_type(field)
|
158
|
-
else
|
159
|
-
::GraphQL::Types::String
|
160
|
-
end
|
161
|
-
|
162
|
-
if field.optional
|
163
|
-
base_type
|
164
|
-
else
|
165
|
-
# Arrays are already wrapped, so handle them differently
|
166
|
-
base_type.is_a?(Array) ? [base_type.first, { null: false }] : base_type.to_non_null_type
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def graphql_type_for_element(element_type)
|
171
|
-
case element_type
|
172
|
-
when Hash
|
173
|
-
# Handle typed arrays like List[Literal['yes', 'no']]
|
174
|
-
if element_type[:type] == :literal
|
175
|
-
create_enum_type_from_values(element_type[:values])
|
176
|
-
else
|
177
|
-
::GraphQL::Types::String
|
178
|
-
end
|
179
|
-
else
|
180
|
-
# Simple types
|
181
|
-
case element_type
|
182
|
-
when :string then ::GraphQL::Types::String
|
183
|
-
when :int, :integer then ::GraphQL::Types::Int
|
184
|
-
when :float then ::GraphQL::Types::Float
|
185
|
-
when :bool, :boolean then ::GraphQL::Types::Boolean
|
186
|
-
else ::GraphQL::Types::String
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def create_enum_type(field)
|
192
|
-
enum_name = "#{field.name.to_s.capitalize}Enum"
|
193
|
-
return @type_cache[enum_name] if @type_cache[enum_name]
|
194
|
-
|
195
|
-
# Extract literal values from the field's validator
|
196
|
-
values = extract_literal_values(field)
|
197
|
-
|
198
|
-
enum_type = Class.new(::GraphQL::Schema::Enum) do
|
199
|
-
graphql_name enum_name
|
200
|
-
description "Enum for #{field.name}"
|
201
|
-
|
202
|
-
values.each do |val|
|
203
|
-
value val.upcase.gsub(/[^A-Z0-9_]/, '_'), value: val
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
@type_cache[enum_name] = enum_type
|
208
|
-
end
|
209
|
-
|
210
|
-
def create_enum_type_from_values(values)
|
211
|
-
enum_name = "Literal#{values.map(&:capitalize).join}Enum"
|
212
|
-
return @type_cache[enum_name] if @type_cache[enum_name]
|
213
|
-
|
214
|
-
enum_type = Class.new(::GraphQL::Schema::Enum) do
|
215
|
-
graphql_name enum_name
|
216
|
-
|
217
|
-
values.each do |val|
|
218
|
-
value val.upcase.gsub(/[^A-Z0-9_]/, '_'), value: val
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
@type_cache[enum_name] = enum_type
|
223
|
-
end
|
224
|
-
|
225
|
-
def extract_literal_values(field)
|
226
|
-
# Try to extract values from the field's validator
|
227
|
-
if field.respond_to?(:validator) && field.validator.respond_to?(:instance_variable_get)
|
228
|
-
field.validator.instance_variable_get(:@values) || []
|
229
|
-
elsif field.respond_to?(:element_type) && field.element_type.is_a?(Hash)
|
230
|
-
field.element_type[:values] || []
|
231
|
-
else
|
232
|
-
[]
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def execute_signature(operation_name, signature, args)
|
144
|
+
def execute_signature(operation_name, signature, args, context = {})
|
237
145
|
# Convert GraphQL arguments from camelCase to snake_case
|
238
146
|
inputs = transform_graphql_args(args)
|
239
147
|
|
148
|
+
# Get data loader from context if available
|
149
|
+
context[:data_loader] || @data_loader
|
150
|
+
|
240
151
|
# Check if we have a registered module for this operation
|
241
152
|
if @modules[operation_name]
|
242
|
-
|
243
|
-
loader = @data_loader.for(@modules[operation_name].class)
|
244
|
-
promise = loader.load(inputs)
|
245
|
-
|
246
|
-
# In a real GraphQL implementation, this would be handled by the executor
|
247
|
-
# For now, we'll resolve immediately
|
248
|
-
result = promise.value
|
153
|
+
module_instance = @modules[operation_name]
|
249
154
|
|
250
|
-
#
|
251
|
-
transform_module_result(result)
|
155
|
+
# Direct execution - batching will be handled by the executor
|
252
156
|
else
|
253
157
|
# Fallback: create a module instance on the fly
|
254
158
|
module_class = infer_module_class(signature)
|
255
159
|
module_instance = module_class.new(signature)
|
256
|
-
result = module_instance.call(inputs)
|
257
|
-
transform_module_result(result)
|
258
160
|
end
|
161
|
+
result = module_instance.call(inputs)
|
162
|
+
transform_module_result(result)
|
259
163
|
end
|
260
164
|
|
261
165
|
def transform_graphql_args(args)
|
@@ -294,7 +198,7 @@ module Desiru
|
|
294
198
|
# Remove trailing '?' for optional fields
|
295
199
|
clean_name = field_name.to_s.gsub('?', '')
|
296
200
|
parts = clean_name.split('_')
|
297
|
-
parts[0] + parts[1
|
201
|
+
parts[0] + parts[1..].map(&:capitalize).join
|
298
202
|
end
|
299
203
|
end
|
300
204
|
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
require_relative 'enum_builder'
|
5
|
+
require_relative 'type_cache_warmer'
|
6
|
+
|
7
|
+
module Desiru
|
8
|
+
module GraphQL
|
9
|
+
# Handles GraphQL type generation and caching
|
10
|
+
module TypeBuilder
|
11
|
+
extend self
|
12
|
+
|
13
|
+
@type_cache = {}
|
14
|
+
@cache_mutex = Mutex.new
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :type_cache, :cache_mutex
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_type_cache!
|
21
|
+
@cache_mutex.synchronize do
|
22
|
+
@type_cache.clear
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_output_type(signature)
|
27
|
+
# Create a stable cache key based on signature structure
|
28
|
+
cache_key = generate_type_cache_key('Output', signature.output_fields)
|
29
|
+
|
30
|
+
# Check cache first
|
31
|
+
@cache_mutex.synchronize do
|
32
|
+
return @type_cache[cache_key] if @type_cache[cache_key]
|
33
|
+
end
|
34
|
+
|
35
|
+
output_field_defs = build_field_definitions(signature.output_fields)
|
36
|
+
|
37
|
+
output_type = Class.new(::GraphQL::Schema::Object) do
|
38
|
+
graphql_name "Output#{cache_key.hash.abs}"
|
39
|
+
description 'Generated output type'
|
40
|
+
|
41
|
+
output_field_defs.each do |field_name, field_def|
|
42
|
+
field field_name, field_def[:type],
|
43
|
+
null: field_def[:null],
|
44
|
+
description: field_def[:description]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Store in cache
|
49
|
+
@cache_mutex.synchronize do
|
50
|
+
@type_cache[cache_key] = output_type
|
51
|
+
end
|
52
|
+
|
53
|
+
output_type
|
54
|
+
end
|
55
|
+
|
56
|
+
def graphql_type_for_field(field)
|
57
|
+
case field.type
|
58
|
+
when :int, :integer
|
59
|
+
::GraphQL::Types::Int
|
60
|
+
when :float
|
61
|
+
::GraphQL::Types::Float
|
62
|
+
when :bool, :boolean
|
63
|
+
::GraphQL::Types::Boolean
|
64
|
+
when :list
|
65
|
+
# Handle list types
|
66
|
+
element_type = graphql_type_for_element(field.element_type)
|
67
|
+
[element_type]
|
68
|
+
when :literal
|
69
|
+
# Create enum type for literal values
|
70
|
+
EnumBuilder.create_enum_type(field, @type_cache, @cache_mutex)
|
71
|
+
else
|
72
|
+
::GraphQL::Types::String
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def build_field_definitions(fields)
|
79
|
+
field_defs = {}
|
80
|
+
fields.each do |field_name, field|
|
81
|
+
field_defs[camelcase_field_name(field_name)] = {
|
82
|
+
type: graphql_type_for_field(field),
|
83
|
+
null: field.optional,
|
84
|
+
description: field.description
|
85
|
+
}
|
86
|
+
end
|
87
|
+
field_defs
|
88
|
+
end
|
89
|
+
|
90
|
+
def graphql_type_for_element(element_type)
|
91
|
+
case element_type
|
92
|
+
when Hash
|
93
|
+
# Handle typed arrays like List[Literal['yes', 'no']]
|
94
|
+
if element_type[:type] == :literal
|
95
|
+
EnumBuilder.create_enum_from_values(element_type[:values], @type_cache, @cache_mutex)
|
96
|
+
else
|
97
|
+
::GraphQL::Types::String
|
98
|
+
end
|
99
|
+
else
|
100
|
+
# Simple types
|
101
|
+
element_to_graphql_type(element_type)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def element_to_graphql_type(element_type)
|
106
|
+
case element_type
|
107
|
+
when :int, :integer then ::GraphQL::Types::Int
|
108
|
+
when :float then ::GraphQL::Types::Float
|
109
|
+
when :bool, :boolean then ::GraphQL::Types::Boolean
|
110
|
+
else ::GraphQL::Types::String
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def camelcase_field_name(field_name)
|
115
|
+
# Convert snake_case to camelCase for GraphQL conventions
|
116
|
+
# Remove trailing '?' for optional fields
|
117
|
+
clean_name = field_name.to_s.gsub('?', '')
|
118
|
+
parts = clean_name.split('_')
|
119
|
+
parts[0] + parts[1..].map(&:capitalize).join
|
120
|
+
end
|
121
|
+
|
122
|
+
def generate_type_cache_key(prefix, fields)
|
123
|
+
# Generate a stable cache key based on field structure
|
124
|
+
# Use a more efficient approach with less string concatenation
|
125
|
+
field_data = fields.map do |name, field|
|
126
|
+
components = [name.to_s, field.type.to_s]
|
127
|
+
components << (field.optional ? 'T' : 'F')
|
128
|
+
components << field.element_type.to_s if field.respond_to?(:element_type)
|
129
|
+
components
|
130
|
+
end.sort
|
131
|
+
|
132
|
+
# Use hash for more compact cache keys
|
133
|
+
content_hash = field_data.hash.abs
|
134
|
+
"#{prefix}:#{content_hash}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
module GraphQL
|
5
|
+
# Utility for warming the GraphQL type cache with common types
|
6
|
+
module TypeCacheWarmer
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Pre-generate commonly used types to improve cold-start performance
|
10
|
+
def warm_common_types!
|
11
|
+
warm_common_output_types!
|
12
|
+
warm_common_enums!
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get statistics about the type cache
|
16
|
+
def cache_stats
|
17
|
+
cache = TypeBuilder.instance_variable_get(:@type_cache)
|
18
|
+
mutex = TypeBuilder.instance_variable_get(:@cache_mutex)
|
19
|
+
|
20
|
+
mutex.synchronize do
|
21
|
+
{
|
22
|
+
total_types: cache.size,
|
23
|
+
output_types: cache.keys.count { |k| k.start_with?('Output:') },
|
24
|
+
enum_types: cache.keys.count { |k| k.include?('Enum') },
|
25
|
+
cache_keys: cache.keys
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def warm_common_output_types!
|
33
|
+
common_field_sets.each do |fields|
|
34
|
+
signature = create_mock_signature(fields)
|
35
|
+
TypeBuilder.build_output_type(signature)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def common_field_sets
|
40
|
+
[
|
41
|
+
# Single field types
|
42
|
+
{ id: create_field(:string, false) },
|
43
|
+
{ result: create_field(:string, true) },
|
44
|
+
{ output: create_field(:string, true) },
|
45
|
+
{ message: create_field(:string, true) },
|
46
|
+
{
|
47
|
+
id: create_field(:string, false),
|
48
|
+
result: create_field(:string, true),
|
49
|
+
timestamp: create_field(:float, true)
|
50
|
+
},
|
51
|
+
{
|
52
|
+
output: create_field(:string, true),
|
53
|
+
confidence: create_field(:float, true),
|
54
|
+
reasoning: create_field(:string, true)
|
55
|
+
},
|
56
|
+
{
|
57
|
+
success: create_field(:bool, false),
|
58
|
+
message: create_field(:string, true),
|
59
|
+
data: create_field(:string, true)
|
60
|
+
}
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_field(type, optional)
|
65
|
+
Struct.new(:type, :optional, :description).new(type, optional, nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_mock_signature(fields)
|
69
|
+
Struct.new(:output_fields).new(fields)
|
70
|
+
end
|
71
|
+
|
72
|
+
def warm_common_enums!
|
73
|
+
# Common enum value sets
|
74
|
+
common_enum_values = [
|
75
|
+
%w[pending processing completed failed],
|
76
|
+
%w[low medium high critical],
|
77
|
+
%w[yes no maybe],
|
78
|
+
%w[active inactive suspended],
|
79
|
+
%w[draft published archived]
|
80
|
+
]
|
81
|
+
|
82
|
+
cache = TypeBuilder.instance_variable_get(:@type_cache)
|
83
|
+
mutex = TypeBuilder.instance_variable_get(:@cache_mutex)
|
84
|
+
|
85
|
+
common_enum_values.each do |values|
|
86
|
+
EnumBuilder.create_enum_from_values(values, cache, mutex)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -9,7 +9,7 @@ module Desiru
|
|
9
9
|
|
10
10
|
def perform(job_id, module_class_name, signature_str, inputs, options = {})
|
11
11
|
update_status(job_id, 'running', message: 'Initializing module')
|
12
|
-
|
12
|
+
|
13
13
|
module_class = Object.const_get(module_class_name)
|
14
14
|
|
15
15
|
# Extract module initialization parameters
|
data/lib/desiru/jobs/base.rb
CHANGED
@@ -18,7 +18,11 @@ module Desiru
|
|
18
18
|
protected
|
19
19
|
|
20
20
|
def store_result(job_id, result, ttl: 3600)
|
21
|
+
# Store in Redis for fast access
|
21
22
|
redis.setex(result_key(job_id), ttl, result.to_json)
|
23
|
+
|
24
|
+
# Also persist to database for long-term storage
|
25
|
+
persist_result_to_db(job_id, result)
|
22
26
|
end
|
23
27
|
|
24
28
|
def fetch_result(job_id)
|
@@ -43,11 +47,74 @@ module Desiru
|
|
43
47
|
status_data[:message] = message if message
|
44
48
|
|
45
49
|
redis.setex(status_key(job_id), 86_400, status_data.to_json)
|
50
|
+
|
51
|
+
# Also persist to database
|
52
|
+
persist_status_to_db(job_id, status, progress: progress, message: message)
|
46
53
|
end
|
47
54
|
|
48
55
|
def status_key(job_id)
|
49
56
|
"desiru:status:#{job_id}"
|
50
57
|
end
|
58
|
+
|
59
|
+
# Database persistence methods
|
60
|
+
def persist_result_to_db(job_id, result)
|
61
|
+
return unless persistence_enabled?
|
62
|
+
|
63
|
+
job_repo.mark_completed(job_id, result)
|
64
|
+
rescue StandardError => e
|
65
|
+
Desiru.logger.warn("Failed to persist job result to database: #{e.message}")
|
66
|
+
end
|
67
|
+
|
68
|
+
def persist_error_to_db(job_id, error, backtrace = nil)
|
69
|
+
return unless persistence_enabled?
|
70
|
+
|
71
|
+
job_repo.mark_failed(job_id, error, backtrace: backtrace)
|
72
|
+
rescue StandardError => e
|
73
|
+
Desiru.logger.warn("Failed to persist job error to database: #{e.message}")
|
74
|
+
end
|
75
|
+
|
76
|
+
def persist_status_to_db(job_id, status, progress: nil, message: nil)
|
77
|
+
return unless persistence_enabled?
|
78
|
+
|
79
|
+
case status
|
80
|
+
when 'processing'
|
81
|
+
job_repo.mark_processing(job_id)
|
82
|
+
# Also update progress if provided
|
83
|
+
job_repo.update_progress(job_id, progress, message: message) if progress
|
84
|
+
when 'completed'
|
85
|
+
# Already handled by persist_result_to_db
|
86
|
+
when 'failed'
|
87
|
+
# Already handled by persist_error_to_db
|
88
|
+
else
|
89
|
+
job_repo.update_progress(job_id, progress, message: message) if progress
|
90
|
+
end
|
91
|
+
rescue StandardError => e
|
92
|
+
Desiru.logger.warn("Failed to persist job status to database: #{e.message}")
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_job_record(job_id, inputs: nil, expires_at: nil)
|
96
|
+
return unless persistence_enabled?
|
97
|
+
|
98
|
+
job_repo.create_for_job(
|
99
|
+
job_id,
|
100
|
+
self.class.name,
|
101
|
+
self.class.get_sidekiq_options['queue'] || 'default',
|
102
|
+
inputs: inputs,
|
103
|
+
expires_at: expires_at
|
104
|
+
)
|
105
|
+
rescue StandardError => e
|
106
|
+
Desiru.logger.warn("Failed to create job record in database: #{e.message}")
|
107
|
+
end
|
108
|
+
|
109
|
+
def persistence_enabled?
|
110
|
+
Desiru::Persistence.enabled?
|
111
|
+
rescue StandardError
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
def job_repo
|
116
|
+
@job_repo ||= Desiru::Persistence.repositories[:job_results]
|
117
|
+
end
|
51
118
|
end
|
52
119
|
end
|
53
120
|
end
|
@@ -10,7 +10,7 @@ module Desiru
|
|
10
10
|
def perform(batch_id, module_class_name, signature_str, inputs_array, options = {})
|
11
11
|
total_items = inputs_array.size
|
12
12
|
update_status(batch_id, 'running', progress: 0, message: "Processing #{total_items} items")
|
13
|
-
|
13
|
+
|
14
14
|
module_class = Object.const_get(module_class_name)
|
15
15
|
|
16
16
|
# Extract module initialization parameters
|
@@ -34,9 +34,9 @@ module Desiru
|
|
34
34
|
|
35
35
|
inputs_array.each_with_index do |inputs, index|
|
36
36
|
progress = ((index + 1).to_f / total_items * 100).round
|
37
|
-
update_status(batch_id, 'running', progress: progress,
|
38
|
-
|
39
|
-
|
37
|
+
update_status(batch_id, 'running', progress: progress,
|
38
|
+
message: "Processing item #{index + 1} of #{total_items}")
|
39
|
+
|
40
40
|
result = module_instance.call(**inputs)
|
41
41
|
results << {
|
42
42
|
index: index,
|
@@ -53,8 +53,8 @@ module Desiru
|
|
53
53
|
end
|
54
54
|
|
55
55
|
final_status = errors.empty? ? 'completed' : 'completed_with_errors'
|
56
|
-
update_status(batch_id, final_status, progress: 100,
|
57
|
-
|
56
|
+
update_status(batch_id, final_status, progress: 100,
|
57
|
+
message: "Processed #{results.size} successfully, #{errors.size} failed")
|
58
58
|
|
59
59
|
store_result(batch_id, {
|
60
60
|
success: errors.empty?,
|