desiru 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +55 -0
  4. data/CLAUDE.md +22 -0
  5. data/Gemfile +36 -0
  6. data/Gemfile.lock +255 -0
  7. data/LICENSE +21 -0
  8. data/README.md +343 -0
  9. data/Rakefile +18 -0
  10. data/desiru.gemspec +44 -0
  11. data/examples/README.md +55 -0
  12. data/examples/async_processing.rb +135 -0
  13. data/examples/few_shot_learning.rb +66 -0
  14. data/examples/graphql_api.rb +190 -0
  15. data/examples/graphql_integration.rb +114 -0
  16. data/examples/rag_retrieval.rb +80 -0
  17. data/examples/simple_qa.rb +31 -0
  18. data/examples/typed_signatures.rb +45 -0
  19. data/lib/desiru/async_capable.rb +170 -0
  20. data/lib/desiru/cache.rb +116 -0
  21. data/lib/desiru/configuration.rb +40 -0
  22. data/lib/desiru/field.rb +171 -0
  23. data/lib/desiru/graphql/data_loader.rb +210 -0
  24. data/lib/desiru/graphql/executor.rb +115 -0
  25. data/lib/desiru/graphql/schema_generator.rb +301 -0
  26. data/lib/desiru/jobs/async_predict.rb +52 -0
  27. data/lib/desiru/jobs/base.rb +53 -0
  28. data/lib/desiru/jobs/batch_processor.rb +71 -0
  29. data/lib/desiru/jobs/optimizer_job.rb +45 -0
  30. data/lib/desiru/models/base.rb +112 -0
  31. data/lib/desiru/models/raix_adapter.rb +210 -0
  32. data/lib/desiru/module.rb +204 -0
  33. data/lib/desiru/modules/chain_of_thought.rb +106 -0
  34. data/lib/desiru/modules/predict.rb +142 -0
  35. data/lib/desiru/modules/retrieve.rb +199 -0
  36. data/lib/desiru/optimizers/base.rb +130 -0
  37. data/lib/desiru/optimizers/bootstrap_few_shot.rb +212 -0
  38. data/lib/desiru/program.rb +106 -0
  39. data/lib/desiru/registry.rb +74 -0
  40. data/lib/desiru/signature.rb +322 -0
  41. data/lib/desiru/version.rb +5 -0
  42. data/lib/desiru.rb +67 -0
  43. metadata +184 -0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Thread-safe in-memory cache with TTL support and LRU eviction
5
+ class Cache
6
+ Entry = Struct.new(:value, :expires_at, :accessed_at, keyword_init: true)
7
+
8
+ def initialize(max_size: 1000, cleanup_interval: 300)
9
+ @store = {}
10
+ @mutex = Mutex.new
11
+ @max_size = max_size
12
+ @cleanup_interval = cleanup_interval
13
+ @last_cleanup = Time.now
14
+ end
15
+
16
+ # Get a value from cache or set it using the provided block
17
+ def get_or_set(key, ttl: 3600)
18
+ @mutex.synchronize do
19
+ cleanup_if_needed
20
+
21
+ if (entry = @store[key])
22
+ if entry.expires_at > Time.now
23
+ entry.accessed_at = Time.now
24
+ return entry.value
25
+ else
26
+ @store.delete(key)
27
+ end
28
+ end
29
+
30
+ # Evict LRU if at capacity
31
+ evict_lru if @store.size >= @max_size
32
+
33
+ value = yield
34
+ @store[key] = Entry.new(
35
+ value: value,
36
+ expires_at: Time.now + ttl,
37
+ accessed_at: Time.now
38
+ )
39
+ value
40
+ end
41
+ end
42
+
43
+ # Get a value from cache without setting
44
+ def get(key)
45
+ @mutex.synchronize do
46
+ if (entry = @store[key])
47
+ if entry.expires_at > Time.now
48
+ entry.accessed_at = Time.now
49
+ entry.value
50
+ else
51
+ @store.delete(key)
52
+ nil
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Set a value in cache
59
+ def set(key, value, ttl: 3600)
60
+ @mutex.synchronize do
61
+ cleanup_if_needed
62
+ evict_lru if @store.size >= @max_size && !@store.key?(key)
63
+
64
+ @store[key] = Entry.new(
65
+ value: value,
66
+ expires_at: Time.now + ttl,
67
+ accessed_at: Time.now
68
+ )
69
+ value
70
+ end
71
+ end
72
+
73
+ # Delete a key from cache
74
+ def delete(key)
75
+ @mutex.synchronize do
76
+ @store.delete(key)
77
+ end
78
+ end
79
+
80
+ # Clear all entries
81
+ def clear
82
+ @mutex.synchronize do
83
+ @store.clear
84
+ end
85
+ end
86
+
87
+ # Get the current size
88
+ def size
89
+ @mutex.synchronize do
90
+ @store.size
91
+ end
92
+ end
93
+
94
+ # Manually trigger cleanup of expired entries
95
+ def cleanup_expired
96
+ @mutex.synchronize do
97
+ @store.delete_if { |_, entry| entry.expires_at <= Time.now }
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def cleanup_if_needed
104
+ return unless Time.now - @last_cleanup > @cleanup_interval
105
+
106
+ @store.delete_if { |_, entry| entry.expires_at <= Time.now }
107
+ @last_cleanup = Time.now
108
+ end
109
+
110
+ def evict_lru
111
+ # Find least recently used entry
112
+ lru_key = @store.min_by { |_, entry| entry.accessed_at }&.first
113
+ @store.delete(lru_key) if lru_key
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Global configuration for Desiru
5
+ # Follows singleton pattern for service-oriented design
6
+ class Configuration
7
+ attr_accessor :default_model, :cache_enabled, :cache_ttl, :max_retries,
8
+ :retry_delay, :logger, :module_registry, :model_timeout,
9
+ :redis_url
10
+
11
+ def initialize
12
+ @default_model = nil
13
+ @cache_enabled = true
14
+ @cache_ttl = 3600 # 1 hour
15
+ @max_retries = 3
16
+ @retry_delay = 1
17
+ @logger = default_logger
18
+ @module_registry = Desiru::Registry.instance
19
+ @model_timeout = 30
20
+ @redis_url = nil # Defaults to REDIS_URL env var if not set
21
+ end
22
+
23
+ def validate!
24
+ raise ConfigurationError, 'default_model must be set' unless default_model
25
+ raise ConfigurationError, 'default_model must respond to :complete' unless default_model.respond_to?(:complete)
26
+ end
27
+
28
+ private
29
+
30
+ def default_logger
31
+ require 'logger'
32
+ Logger.new($stdout).tap do |logger|
33
+ logger.level = Logger::INFO
34
+ logger.formatter = proc do |severity, datetime, _progname, msg|
35
+ "[Desiru] #{datetime}: #{severity} -- #{msg}\n"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Represents a field in a signature with type information and metadata
5
+ class Field
6
+ attr_reader :name, :type, :description, :optional, :default, :validator, :literal_values, :element_type
7
+
8
+ alias optional? optional
9
+
10
+ def initialize(name, type = :string, description: nil, optional: false, default: nil, validator: nil,
11
+ literal_values: nil, element_type: nil)
12
+ @name = name.to_sym
13
+ @type = normalize_type(type)
14
+ @description = description
15
+ @optional = optional
16
+ @default = default
17
+ @literal_values = literal_values&.map(&:freeze)&.freeze if literal_values
18
+ @element_type = element_type
19
+ @validator = validator || default_validator
20
+ end
21
+
22
+ def validate(value)
23
+ return true if optional && value.nil?
24
+ return true if value.nil? && !default.nil?
25
+
26
+ raise ValidationError, validation_error_message(value) unless validator.call(value)
27
+
28
+ true
29
+ end
30
+
31
+ def coerce(value)
32
+ return default if value.nil? && !default.nil?
33
+ return value if value.nil? && optional
34
+
35
+ case type
36
+ when :string
37
+ value.to_s
38
+ when :int, :integer
39
+ value.to_i
40
+ when :float
41
+ value.to_f
42
+ when :bool, :boolean
43
+ case value.to_s.downcase
44
+ when 'true', 'yes', '1', 't'
45
+ true
46
+ when 'false', 'no', '0', 'f'
47
+ false
48
+ else
49
+ !!value
50
+ end
51
+ when :literal
52
+ # For literal types, ensure the value is a string and matches one of the allowed values
53
+ coerced = value.to_s
54
+ unless literal_values.include?(coerced)
55
+ raise ValidationError, "Value '#{coerced}' is not one of allowed values: #{literal_values.join(', ')}"
56
+ end
57
+
58
+ coerced
59
+ when :list, :array
60
+ array_value = Array(value)
61
+ # If we have an element type, coerce each element
62
+ if element_type && element_type[:type] == :literal
63
+ array_value.map do |elem|
64
+ coerced_elem = elem.to_s
65
+ unless element_type[:literal_values].include?(coerced_elem)
66
+ raise ValidationError,
67
+ "Array element '#{coerced_elem}' is not one of allowed values: #{element_type[:literal_values].join(', ')}"
68
+ end
69
+
70
+ coerced_elem
71
+ end
72
+ else
73
+ array_value
74
+ end
75
+ when :hash, :dict
76
+ value.is_a?(Hash) ? value : {}
77
+ else
78
+ value
79
+ end
80
+ end
81
+
82
+ def to_h
83
+ result = {
84
+ name: name,
85
+ type: type,
86
+ description: description,
87
+ optional: optional,
88
+ default: default
89
+ }
90
+ result[:literal_values] = literal_values if literal_values
91
+ result[:element_type] = element_type if element_type
92
+ result.compact
93
+ end
94
+
95
+ private
96
+
97
+ def validation_error_message(value)
98
+ case type
99
+ when :string
100
+ "#{name} must be a string, got #{value.class}"
101
+ when :int, :integer
102
+ "#{name} must be an integer, got #{value.class}"
103
+ when :float
104
+ "#{name} must be a float, got #{value.class}"
105
+ when :bool, :boolean
106
+ "#{name} must be a boolean (true/false), got #{value.class}"
107
+ when :list, :array
108
+ if element_type && element_type[:type] == :literal
109
+ "#{name} must be an array of literal values: #{element_type[:literal_values].join(', ')}"
110
+ else
111
+ "#{name} must be a list, got #{value.class}"
112
+ end
113
+ when :literal
114
+ "#{name} must be one of: #{literal_values.join(', ')}"
115
+ when :hash, :dict
116
+ "#{name} must be a hash, got #{value.class}"
117
+ else
118
+ "#{name} validation failed for value: #{value}"
119
+ end
120
+ end
121
+
122
+ def normalize_type(type)
123
+ # If type is already a symbol, return it
124
+ return type if type.is_a?(Symbol)
125
+
126
+ case type.to_s.downcase
127
+ when 'str', 'string'
128
+ :string
129
+ when 'int', 'integer'
130
+ :int
131
+ when 'float', 'number', 'double'
132
+ :float
133
+ when 'bool', 'boolean'
134
+ :bool
135
+ when 'list', 'array'
136
+ :list
137
+ when 'hash', 'dict', 'dictionary'
138
+ :hash
139
+ when 'literal'
140
+ :literal
141
+ else
142
+ type.to_sym
143
+ end
144
+ end
145
+
146
+ def default_validator
147
+ case type
148
+ when :string
149
+ ->(v) { v.is_a?(String) }
150
+ when :int
151
+ ->(v) { v.is_a?(Integer) }
152
+ when :float
153
+ ->(v) { v.is_a?(Float) || v.is_a?(Integer) }
154
+ when :bool
155
+ ->(v) { v.is_a?(TrueClass) || v.is_a?(FalseClass) }
156
+ when :literal
157
+ ->(v) { v.is_a?(String) && literal_values.include?(v) }
158
+ when :list
159
+ if element_type && element_type[:type] == :literal
160
+ ->(v) { v.is_a?(Array) && v.all? { |elem| element_type[:literal_values].include?(elem.to_s) } }
161
+ else
162
+ ->(v) { v.is_a?(Array) }
163
+ end
164
+ when :hash
165
+ ->(v) { v.is_a?(Hash) }
166
+ else
167
+ ->(_v) { true }
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module GraphQL
5
+ # DataLoader pattern implementation for batching Desiru module calls
6
+ # Prevents N+1 query problems when multiple GraphQL fields request similar data
7
+ class DataLoader
8
+ def initialize
9
+ @loaders = {}
10
+ @results_cache = {}
11
+ @pending_loads = Hash.new { |h, k| h[k] = [] }
12
+ end
13
+
14
+ # Get or create a loader for a specific module
15
+ def for(module_class, **options)
16
+ key = loader_key(module_class, options)
17
+ @loaders[key] ||= BatchLoader.new(module_class, **options)
18
+ end
19
+
20
+ # Execute all pending loads in batch
21
+ def perform_loads
22
+ @pending_loads.each do |loader_key, batch|
23
+ next if batch.empty?
24
+
25
+ loader = @loaders[loader_key]
26
+ results = loader.load_batch(batch.map(&:first))
27
+
28
+ batch.each_with_index do |(_inputs, promise), idx|
29
+ promise.fulfill(results[idx])
30
+ end
31
+ end
32
+
33
+ @pending_loads.clear
34
+ end
35
+
36
+ # Clear all caches
37
+ def clear!
38
+ @results_cache.clear
39
+ @pending_loads.clear
40
+ @loaders.values.each(&:clear_cache!)
41
+ end
42
+
43
+ private
44
+
45
+ def loader_key(module_class, options)
46
+ "#{module_class.name}:#{options.hash}"
47
+ end
48
+
49
+ # Individual batch loader for a specific module
50
+ class BatchLoader
51
+ attr_reader :module_class, :batch_size, :cache
52
+
53
+ def initialize(module_class, batch_size: 100, cache: true)
54
+ @module_class = module_class
55
+ @batch_size = batch_size
56
+ @cache = cache
57
+ @cache_store = {} if cache
58
+ end
59
+
60
+ # Load a batch of inputs
61
+ def load_batch(inputs_array)
62
+ return load_from_cache(inputs_array) if cache && all_cached?(inputs_array)
63
+
64
+ # Group inputs by signature to optimize processing
65
+ grouped = group_by_signature(inputs_array)
66
+ results = []
67
+
68
+ grouped.each do |_signature_key, inputs_group|
69
+ module_instance = create_module_instance(inputs_group.first)
70
+
71
+ # Process in chunks to respect batch_size
72
+ inputs_group.each_slice(batch_size) do |chunk|
73
+ chunk_results = process_chunk(module_instance, chunk)
74
+ results.concat(chunk_results)
75
+
76
+ # Cache results if enabled
77
+ cache_results(chunk, chunk_results) if cache
78
+ end
79
+ end
80
+
81
+ results
82
+ end
83
+
84
+ # Load a single input (returns a promise for lazy evaluation)
85
+ def load(inputs)
86
+ Promise.new do |promise|
87
+ if cache && @cache_store.key?(cache_key(inputs))
88
+ promise.fulfill(@cache_store[cache_key(inputs)])
89
+ else
90
+ # Queue for batch loading
91
+ queue_for_loading(inputs, promise)
92
+ end
93
+ end
94
+ end
95
+
96
+ def clear_cache!
97
+ @cache_store.clear if cache
98
+ end
99
+
100
+ private
101
+
102
+ def all_cached?(inputs_array)
103
+ inputs_array.all? { |inputs| @cache_store.key?(cache_key(inputs)) }
104
+ end
105
+
106
+ def load_from_cache(inputs_array)
107
+ inputs_array.map { |inputs| @cache_store[cache_key(inputs)] }
108
+ end
109
+
110
+ def cache_results(inputs_array, results)
111
+ inputs_array.each_with_index do |inputs, idx|
112
+ @cache_store[cache_key(inputs)] = results[idx]
113
+ end
114
+ end
115
+
116
+ def cache_key(inputs)
117
+ inputs.sort.to_h.hash
118
+ end
119
+
120
+ def group_by_signature(inputs_array)
121
+ inputs_array.group_by do |inputs|
122
+ # Group by input keys to process similar queries together
123
+ inputs.keys.sort.join(':')
124
+ end
125
+ end
126
+
127
+ def create_module_instance(sample_inputs)
128
+ # Infer signature from inputs
129
+ signature = infer_signature(sample_inputs)
130
+ module_class.new(signature)
131
+ end
132
+
133
+ def infer_signature(inputs)
134
+ # Create a signature based on input structure
135
+ input_fields = inputs.map { |k, v| "#{k}: #{type_for_value(v)}" }.join(', ')
136
+ output_fields = "result: hash" # Default output, can be customized
137
+ "#{input_fields} -> #{output_fields}"
138
+ end
139
+
140
+ def type_for_value(value)
141
+ case value
142
+ when String then 'string'
143
+ when Integer then 'int'
144
+ when Float then 'float'
145
+ when TrueClass, FalseClass then 'bool'
146
+ when Array then 'list'
147
+ when Hash then 'hash'
148
+ else 'string'
149
+ end
150
+ end
151
+
152
+ def process_chunk(module_instance, chunk)
153
+ if module_instance.respond_to?(:batch_forward)
154
+ # If module supports batch processing
155
+ module_instance.batch_forward(chunk)
156
+ else
157
+ # Fall back to individual processing
158
+ chunk.map { |inputs| module_instance.call(inputs) }
159
+ end
160
+ end
161
+
162
+ def queue_for_loading(inputs, promise)
163
+ # This would integrate with the parent DataLoader's pending loads
164
+ # For now, process immediately
165
+ result = module_class.new(infer_signature(inputs)).call(inputs)
166
+ promise.fulfill(result)
167
+ @cache_store[cache_key(inputs)] = result if cache
168
+ end
169
+ end
170
+
171
+ # Promise implementation for lazy loading
172
+ class Promise
173
+ def initialize(&block)
174
+ @fulfilled = false
175
+ @value = nil
176
+ @callbacks = []
177
+ block.call(self) if block
178
+ end
179
+
180
+ def fulfill(value)
181
+ return if @fulfilled
182
+
183
+ @value = value
184
+ @fulfilled = true
185
+ @callbacks.each { |cb| cb.call(value) }
186
+ @callbacks.clear
187
+ end
188
+
189
+ def then(&block)
190
+ if @fulfilled
191
+ block.call(@value)
192
+ else
193
+ @callbacks << block
194
+ end
195
+ self
196
+ end
197
+
198
+ def value
199
+ raise "Promise not yet fulfilled" unless @fulfilled
200
+
201
+ @value
202
+ end
203
+
204
+ def fulfilled?
205
+ @fulfilled
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module Desiru
6
+ module GraphQL
7
+ # Custom GraphQL executor with batch loading support
8
+ class Executor
9
+ attr_reader :schema, :data_loader
10
+
11
+ def initialize(schema, data_loader: nil)
12
+ @schema = schema
13
+ @data_loader = data_loader || DataLoader.new
14
+ end
15
+
16
+ # Execute a GraphQL query with batch loading
17
+ def execute(query_string, variables: {}, context: {}, operation_name: nil)
18
+ # Add data loader to context
19
+ context[:data_loader] = @data_loader
20
+
21
+ # Wrap execution with batch loading
22
+ result = nil
23
+ batch_execute do
24
+ result = @schema.execute(
25
+ query_string,
26
+ variables: variables,
27
+ context: context,
28
+ operation_name: operation_name
29
+ )
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ # Execute multiple queries in a single batch
36
+ def execute_batch(queries)
37
+ results = []
38
+
39
+ batch_execute do
40
+ queries.each do |query_params|
41
+ query_params[:context] ||= {}
42
+ query_params[:context][:data_loader] = @data_loader
43
+
44
+ results << @schema.execute(
45
+ query_params[:query],
46
+ variables: query_params[:variables] || {},
47
+ context: query_params[:context],
48
+ operation_name: query_params[:operation_name]
49
+ )
50
+ end
51
+ end
52
+
53
+ results
54
+ end
55
+
56
+ private
57
+
58
+ def batch_execute
59
+ # Start batch loading context
60
+ @data_loader.clear! if @data_loader.respond_to?(:clear!)
61
+
62
+ # Execute the GraphQL queries
63
+ result = yield
64
+
65
+ # Perform all pending batch loads
66
+ @data_loader.perform_loads if @data_loader.respond_to?(:perform_loads)
67
+
68
+ result
69
+ end
70
+ end
71
+
72
+ # GraphQL field extension for lazy loading
73
+ class LazyFieldExtension < ::GraphQL::Schema::FieldExtension
74
+ def resolve(object:, arguments:, context:)
75
+ result = yield(object, arguments)
76
+
77
+ # If result is a promise, handle it appropriately
78
+ if result.respond_to?(:then) && result.respond_to?(:fulfilled?)
79
+ if result.fulfilled?
80
+ result.value
81
+ else
82
+ # Create a lazy resolver
83
+ ::GraphQL::Execution::Lazy.new do
84
+ result.value
85
+ end
86
+ end
87
+ else
88
+ result
89
+ end
90
+ end
91
+ end
92
+
93
+ # Middleware for automatic batch loading
94
+ class BatchLoaderMiddleware
95
+ def initialize(app)
96
+ @app = app
97
+ end
98
+
99
+ def call(env)
100
+ # Extract GraphQL context
101
+ context = env['graphql.context'] || {}
102
+
103
+ # Ensure data loader is available
104
+ context[:data_loader] ||= DataLoader.new
105
+ env['graphql.context'] = context
106
+
107
+ # Execute with batch loading
108
+ @app.call(env)
109
+ ensure
110
+ # Clean up after request
111
+ context[:data_loader]&.clear!
112
+ end
113
+ end
114
+ end
115
+ end