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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Desiru
6
+ # Module registry for managing and discovering Desiru modules
7
+ # Implements service locator pattern for module management
8
+ class Registry
9
+ include Singleton
10
+
11
+ def initialize
12
+ @modules = {}
13
+ @module_versions = Hash.new { |h, k| h[k] = {} }
14
+ @module_metadata = {}
15
+ end
16
+
17
+ def register(name, klass, version: '1.0.0', metadata: {})
18
+ validate_module!(klass)
19
+
20
+ name = name.to_sym
21
+ @modules[name] = klass
22
+ @module_versions[name][version] = klass
23
+ @module_metadata[name] = metadata.merge(
24
+ registered_at: Time.now,
25
+ version: version,
26
+ class: klass.name
27
+ )
28
+
29
+ Desiru.configuration.logger&.info("Registered module: #{name} v#{version}")
30
+ end
31
+
32
+ def get(name, version: nil)
33
+ name = name.to_sym
34
+
35
+ if version
36
+ @module_versions[name][version] || raise(ModuleError, "Module #{name} v#{version} not found")
37
+ else
38
+ @modules[name] || raise(ModuleError, "Module #{name} not found")
39
+ end
40
+ end
41
+
42
+ def list
43
+ @modules.keys
44
+ end
45
+
46
+ def metadata(name)
47
+ @module_metadata[name.to_sym]
48
+ end
49
+
50
+ def unregister(name)
51
+ name = name.to_sym
52
+ @modules.delete(name)
53
+ @module_versions.delete(name)
54
+ @module_metadata.delete(name)
55
+ end
56
+
57
+ def clear!
58
+ @modules.clear
59
+ @module_versions.clear
60
+ @module_metadata.clear
61
+ end
62
+
63
+ private
64
+
65
+ def validate_module!(klass)
66
+ raise ModuleError, 'Module must be a class' unless klass.respond_to?(:new)
67
+
68
+ # Check if it's a Desiru module
69
+ return if klass.ancestors.include?(Desiru::Module)
70
+
71
+ raise ModuleError, 'Module must inherit from Desiru::Module'
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Represents input/output specifications for modules
5
+ # Supports DSL-style signature strings like "question -> answer"
6
+ class Signature
7
+ # FieldHash allows access by both string and symbol keys
8
+ class FieldHash < Hash
9
+ def [](key)
10
+ super(key.to_sym)
11
+ end
12
+
13
+ def []=(key, value)
14
+ super(key.to_sym, value)
15
+ end
16
+
17
+ def keys
18
+ super.map(&:to_s)
19
+ end
20
+
21
+ def has_key?(key)
22
+ super(key.to_sym)
23
+ end
24
+
25
+ alias include? has_key?
26
+ alias key? has_key?
27
+ end
28
+
29
+ attr_reader :raw_signature
30
+
31
+ def input_fields
32
+ @input_fields_wrapper
33
+ end
34
+
35
+ def output_fields
36
+ @output_fields_wrapper
37
+ end
38
+
39
+ # Aliases for test compatibility
40
+ alias inputs input_fields
41
+ alias outputs output_fields
42
+
43
+ def initialize(signature_string, descriptions: {})
44
+ @raw_signature = signature_string
45
+ @descriptions = descriptions
46
+ @input_fields = {}
47
+ @output_fields = {}
48
+ @input_fields_wrapper = FieldHash.new
49
+ @output_fields_wrapper = FieldHash.new
50
+
51
+ parse_signature!
52
+ end
53
+
54
+ def validate_inputs(inputs)
55
+ missing = required_input_fields - inputs.keys.map(&:to_sym)
56
+ raise SignatureError, "Missing required inputs: #{missing.join(', ')}" if missing.any?
57
+
58
+ inputs.each do |name, value|
59
+ field = @input_fields[name.to_sym]
60
+ next unless field
61
+
62
+ # Field.validate will raise ValidationError if validation fails
63
+ field.validate(value)
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ def validate_outputs(outputs)
70
+ missing = required_output_fields - outputs.keys.map(&:to_sym)
71
+ raise ValidationError, "Missing required outputs: #{missing.join(', ')}" if missing.any?
72
+
73
+ outputs.each do |name, value|
74
+ field = @output_fields[name.to_sym]
75
+ next unless field
76
+
77
+ # Field.validate will raise ValidationError if validation fails
78
+ field.validate(value)
79
+ end
80
+
81
+ true
82
+ end
83
+
84
+ def coerce_inputs(inputs)
85
+ result = {}
86
+
87
+ @input_fields.each do |name, field|
88
+ value = inputs[name] || inputs[name.to_s]
89
+ result[name] = field.coerce(value)
90
+ end
91
+
92
+ result
93
+ end
94
+
95
+ def coerce_outputs(outputs)
96
+ result = {}
97
+
98
+ @output_fields.each do |name, field|
99
+ value = outputs[name] || outputs[name.to_s]
100
+ result[name] = field.coerce(value)
101
+ end
102
+
103
+ result
104
+ end
105
+
106
+ def to_h
107
+ {
108
+ signature: raw_signature,
109
+ inputs: @input_fields.transform_values(&:to_h),
110
+ outputs: @output_fields.transform_values(&:to_h)
111
+ }
112
+ end
113
+
114
+ def to_s
115
+ raw_signature
116
+ end
117
+
118
+ private
119
+
120
+ def parse_signature!
121
+ parts = raw_signature.split('->').map(&:strip)
122
+ raise ArgumentError, "Invalid signature format: #{raw_signature}" unless parts.size == 2
123
+
124
+ parse_fields(parts[0], @input_fields)
125
+ parse_fields(parts[1], @output_fields)
126
+ end
127
+
128
+ def parse_fields(fields_string, target_hash)
129
+ return if fields_string.empty?
130
+
131
+ # Split fields properly, handling commas inside brackets
132
+ fields = []
133
+ current_field = String.new
134
+ bracket_count = 0
135
+
136
+ fields_string.chars.each do |char|
137
+ if char == '['
138
+ bracket_count += 1
139
+ elsif char == ']'
140
+ bracket_count -= 1
141
+ elsif char == ',' && bracket_count == 0
142
+ fields << current_field.strip
143
+ current_field = String.new
144
+ next
145
+ end
146
+ current_field << char
147
+ end
148
+ fields << current_field.strip unless current_field.empty?
149
+
150
+ fields.each do |field_str|
151
+ # Parse field with type annotation
152
+ if field_str.include?(':')
153
+ name, type_info = field_str.split(':', 2).map(&:strip)
154
+ # Check for optional marker on field name or type
155
+ optional = name.end_with?('?') || type_info.include?('?')
156
+ # Clean field name by removing trailing ?
157
+ name = name.gsub(/\?$/, '')
158
+ # Remove optional marker before parsing type
159
+ clean_type_info = type_info.gsub('?', '').strip
160
+ type_data = parse_type(clean_type_info)
161
+ else
162
+ name = field_str
163
+ # Check if field name ends with ?
164
+ optional = name.end_with?('?')
165
+ name = name.gsub(/\?$/, '')
166
+ type_data = { type: :string }
167
+ end
168
+
169
+ description = @descriptions[name.to_sym] || @descriptions[name.to_s]
170
+
171
+ # Create field with parsed type data
172
+ field_args = {
173
+ description: description,
174
+ optional: optional
175
+ }
176
+
177
+ # Add literal values if present
178
+ field_args[:literal_values] = type_data[:literal_values] if type_data[:literal_values]
179
+
180
+ # Add element type for typed arrays
181
+ field_args[:element_type] = type_data[:element_type] if type_data[:element_type]
182
+
183
+ field = Field.new(
184
+ name,
185
+ type_data[:type],
186
+ **field_args
187
+ )
188
+
189
+ target_hash[name.to_sym] = field
190
+
191
+ # Also add to wrapper for dual access
192
+ if target_hash == @input_fields
193
+ @input_fields_wrapper[name.to_sym] = field
194
+ elsif target_hash == @output_fields
195
+ @output_fields_wrapper[name.to_sym] = field
196
+ end
197
+ end
198
+ end
199
+
200
+ def parse_type(type_string)
201
+ # Handle Literal types - need to match balanced brackets
202
+ if type_string.start_with?('Literal[')
203
+ # Find the matching closing bracket
204
+ bracket_count = 0
205
+ end_index = 0
206
+
207
+ type_string.chars.each_with_index do |char, index|
208
+ if char == '['
209
+ bracket_count += 1
210
+ elsif char == ']'
211
+ bracket_count -= 1
212
+ if bracket_count == 0
213
+ end_index = index
214
+ break
215
+ end
216
+ end
217
+ end
218
+
219
+ if end_index > 0
220
+ literal_content = type_string[8...end_index] # Extract content between 'Literal[' and ']'
221
+ values = parse_literal_values(literal_content)
222
+ return { type: :literal, literal_values: values }
223
+ end
224
+ end
225
+
226
+ # Handle List/Array types with element types
227
+ if type_string.start_with?('List[', 'Array[')
228
+ # Find the matching closing bracket
229
+ bracket_count = 0
230
+ end_index = 0
231
+ start_index = type_string.index('[')
232
+
233
+ type_string.chars.each_with_index do |char, index|
234
+ next if index < start_index
235
+
236
+ if char == '['
237
+ bracket_count += 1
238
+ elsif char == ']'
239
+ bracket_count -= 1
240
+ if bracket_count == 0
241
+ end_index = index
242
+ break
243
+ end
244
+ end
245
+ end
246
+
247
+ if end_index > 0
248
+ element_type_str = type_string[(start_index + 1)...end_index]
249
+ element_type_data = parse_type(element_type_str) # Recursive for nested types
250
+ return { type: :list, element_type: element_type_data }
251
+ end
252
+ end
253
+
254
+ # Handle Union types (for future implementation)
255
+ if type_string.start_with?('Union[')
256
+ # Placeholder for union type parsing
257
+ return { type: :union, union_types: [] } # To be implemented
258
+ end
259
+
260
+ # Handle basic types
261
+ # First check if it's a list/array with simple element type (e.g., list[str])
262
+ if type_string.downcase.start_with?('list[', 'array[')
263
+ # Even if we couldn't parse the brackets properly above, it's still a list
264
+ return { type: :list }
265
+ end
266
+
267
+ clean_type = type_string.gsub(/[?\[\]]/, '').downcase
268
+ type_sym = case clean_type
269
+ when 'str', 'string' then :string
270
+ when 'int', 'integer' then :int
271
+ when 'float', 'number', 'double' then :float
272
+ when 'bool', 'boolean' then :bool
273
+ when 'list', 'array' then :list
274
+ when 'hash', 'dict', 'dictionary' then :hash
275
+ else clean_type.to_sym
276
+ end
277
+
278
+ { type: type_sym }
279
+ end
280
+
281
+ def parse_literal_values(literal_content)
282
+ # Parse comma-separated values, handling quoted strings
283
+ values = []
284
+ current_value = String.new
285
+ in_quotes = false
286
+ quote_char = nil
287
+
288
+ literal_content.each_char.with_index do |char, index|
289
+ if !in_quotes && ['"', "'"].include?(char)
290
+ in_quotes = true
291
+ quote_char = char
292
+ elsif in_quotes && char == quote_char
293
+ # Check if it's escaped
294
+ if index > 0 && literal_content[index - 1] != '\\'
295
+ in_quotes = false
296
+ quote_char = nil
297
+ else
298
+ current_value << char
299
+ end
300
+ elsif !in_quotes && char == ','
301
+ values << current_value.strip.gsub(/^['"]|['"]$/, '')
302
+ current_value = String.new
303
+ else
304
+ current_value << char
305
+ end
306
+ end
307
+
308
+ # Add the last value
309
+ values << current_value.strip.gsub(/^['"]|['"]$/, '') unless current_value.empty?
310
+
311
+ values
312
+ end
313
+
314
+ def required_input_fields
315
+ @input_fields.reject { |_, field| field.optional }.keys
316
+ end
317
+
318
+ def required_output_fields
319
+ @output_fields.reject { |_, field| field.optional }.keys
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ VERSION = '0.1.0'
5
+ end
data/lib/desiru.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'singleton'
5
+
6
+ # Main namespace for Desiru - Declarative Self-Improving Ruby
7
+ module Desiru
8
+ class Error < StandardError; end
9
+ class ConfigurationError < Error; end
10
+ class SignatureError < Error; end
11
+ class ModuleError < Error; end
12
+ class ValidationError < Error; end
13
+ class TimeoutError < Error; end
14
+
15
+ class << self
16
+ attr_writer :configuration
17
+
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def configure
23
+ yield(configuration)
24
+ end
25
+
26
+ def reset_configuration!
27
+ @configuration = Configuration.new
28
+ end
29
+ end
30
+ end
31
+
32
+ # Core components
33
+ require_relative 'desiru/version'
34
+ require_relative 'desiru/configuration'
35
+ require_relative 'desiru/field'
36
+ require_relative 'desiru/signature'
37
+ require_relative 'desiru/module'
38
+ require_relative 'desiru/program'
39
+ require_relative 'desiru/registry'
40
+ require_relative 'desiru/cache'
41
+
42
+ # Model adapters
43
+ require_relative 'desiru/models/base'
44
+ require_relative 'desiru/models/raix_adapter'
45
+
46
+ # Built-in modules
47
+ require_relative 'desiru/modules/predict'
48
+ require_relative 'desiru/modules/chain_of_thought'
49
+ require_relative 'desiru/modules/retrieve'
50
+
51
+ # Optimizers
52
+ require_relative 'desiru/optimizers/base'
53
+ require_relative 'desiru/optimizers/bootstrap_few_shot'
54
+
55
+ # Background jobs
56
+ require_relative 'desiru/jobs/base'
57
+ require_relative 'desiru/jobs/async_predict'
58
+ require_relative 'desiru/jobs/batch_processor'
59
+ require_relative 'desiru/jobs/optimizer_job'
60
+
61
+ # GraphQL integration (optional, requires 'graphql' gem)
62
+ begin
63
+ require 'graphql'
64
+ require_relative 'desiru/graphql/schema_generator'
65
+ rescue LoadError
66
+ # GraphQL integration is optional
67
+ end
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: desiru
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Obie Fernandez
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: forwardable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: redis
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sidekiq
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '7.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: singleton
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bundler
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rake
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '13.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rspec
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
110
+ description: Desiru brings DSPy's declarative programming paradigm for language models
111
+ to Ruby, enabling reliable, maintainable, and portable AI programming.
112
+ email:
113
+ - obiefernandez@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".rspec"
119
+ - ".rubocop.yml"
120
+ - CLAUDE.md
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - LICENSE
124
+ - README.md
125
+ - Rakefile
126
+ - desiru.gemspec
127
+ - examples/README.md
128
+ - examples/async_processing.rb
129
+ - examples/few_shot_learning.rb
130
+ - examples/graphql_api.rb
131
+ - examples/graphql_integration.rb
132
+ - examples/rag_retrieval.rb
133
+ - examples/simple_qa.rb
134
+ - examples/typed_signatures.rb
135
+ - lib/desiru.rb
136
+ - lib/desiru/async_capable.rb
137
+ - lib/desiru/cache.rb
138
+ - lib/desiru/configuration.rb
139
+ - lib/desiru/field.rb
140
+ - lib/desiru/graphql/data_loader.rb
141
+ - lib/desiru/graphql/executor.rb
142
+ - lib/desiru/graphql/schema_generator.rb
143
+ - lib/desiru/jobs/async_predict.rb
144
+ - lib/desiru/jobs/base.rb
145
+ - lib/desiru/jobs/batch_processor.rb
146
+ - lib/desiru/jobs/optimizer_job.rb
147
+ - lib/desiru/models/base.rb
148
+ - lib/desiru/models/raix_adapter.rb
149
+ - lib/desiru/module.rb
150
+ - lib/desiru/modules/chain_of_thought.rb
151
+ - lib/desiru/modules/predict.rb
152
+ - lib/desiru/modules/retrieve.rb
153
+ - lib/desiru/optimizers/base.rb
154
+ - lib/desiru/optimizers/bootstrap_few_shot.rb
155
+ - lib/desiru/program.rb
156
+ - lib/desiru/registry.rb
157
+ - lib/desiru/signature.rb
158
+ - lib/desiru/version.rb
159
+ homepage: https://github.com/obie/desiru
160
+ licenses:
161
+ - MIT
162
+ metadata:
163
+ homepage_uri: https://github.com/obie/desiru
164
+ source_code_uri: https://github.com/obie/desiru
165
+ changelog_uri: https://github.com/obie/desiru/blob/main/CHANGELOG.md
166
+ rubygems_mfa_required: 'false'
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 3.4.2
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.6.8
182
+ specification_version: 4
183
+ summary: Declarative Self-Improving Ruby - A Ruby port of DSPy
184
+ test_files: []