desiru 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.local.json +11 -0
  3. data/.env.example +34 -0
  4. data/.rubocop.yml +7 -4
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +73 -0
  7. data/CLAUDE.local.md +3 -0
  8. data/CLAUDE.md +10 -1
  9. data/Gemfile +21 -2
  10. data/Gemfile.lock +88 -13
  11. data/README.md +301 -2
  12. data/Rakefile +1 -0
  13. data/db/migrations/001_create_initial_tables.rb +96 -0
  14. data/db/migrations/002_create_job_results.rb +39 -0
  15. data/desiru-development-swarm.yml +185 -0
  16. data/desiru.db +0 -0
  17. data/desiru.gemspec +2 -5
  18. data/docs/background_processing_roadmap.md +87 -0
  19. data/docs/job_scheduling.md +167 -0
  20. data/dspy-analysis-swarm.yml +60 -0
  21. data/dspy-feature-analysis.md +121 -0
  22. data/examples/README.md +69 -0
  23. data/examples/api_with_persistence.rb +122 -0
  24. data/examples/assertions_example.rb +232 -0
  25. data/examples/async_processing.rb +2 -0
  26. data/examples/few_shot_learning.rb +1 -2
  27. data/examples/graphql_api.rb +4 -2
  28. data/examples/graphql_integration.rb +3 -3
  29. data/examples/graphql_optimization_summary.md +143 -0
  30. data/examples/graphql_performance_benchmark.rb +247 -0
  31. data/examples/persistence_example.rb +102 -0
  32. data/examples/react_agent.rb +203 -0
  33. data/examples/rest_api.rb +173 -0
  34. data/examples/rest_api_advanced.rb +333 -0
  35. data/examples/scheduled_job_example.rb +116 -0
  36. data/examples/simple_qa.rb +1 -2
  37. data/examples/sinatra_api.rb +109 -0
  38. data/examples/typed_signatures.rb +1 -2
  39. data/graphql_optimization_summary.md +53 -0
  40. data/lib/desiru/api/grape_integration.rb +284 -0
  41. data/lib/desiru/api/persistence_middleware.rb +148 -0
  42. data/lib/desiru/api/sinatra_integration.rb +217 -0
  43. data/lib/desiru/api.rb +42 -0
  44. data/lib/desiru/assertions.rb +74 -0
  45. data/lib/desiru/async_status.rb +65 -0
  46. data/lib/desiru/cache.rb +1 -1
  47. data/lib/desiru/configuration.rb +2 -1
  48. data/lib/desiru/core/compiler.rb +231 -0
  49. data/lib/desiru/core/example.rb +96 -0
  50. data/lib/desiru/core/prediction.rb +108 -0
  51. data/lib/desiru/core/trace.rb +330 -0
  52. data/lib/desiru/core/traceable.rb +61 -0
  53. data/lib/desiru/core.rb +12 -0
  54. data/lib/desiru/errors.rb +160 -0
  55. data/lib/desiru/field.rb +17 -14
  56. data/lib/desiru/graphql/batch_loader.rb +85 -0
  57. data/lib/desiru/graphql/data_loader.rb +242 -75
  58. data/lib/desiru/graphql/enum_builder.rb +75 -0
  59. data/lib/desiru/graphql/executor.rb +37 -4
  60. data/lib/desiru/graphql/schema_generator.rb +62 -158
  61. data/lib/desiru/graphql/type_builder.rb +138 -0
  62. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  63. data/lib/desiru/jobs/async_predict.rb +1 -1
  64. data/lib/desiru/jobs/base.rb +67 -0
  65. data/lib/desiru/jobs/batch_processor.rb +6 -6
  66. data/lib/desiru/jobs/retriable.rb +119 -0
  67. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  68. data/lib/desiru/jobs/scheduler.rb +219 -0
  69. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  70. data/lib/desiru/models/anthropic.rb +164 -0
  71. data/lib/desiru/models/base.rb +37 -3
  72. data/lib/desiru/models/open_ai.rb +151 -0
  73. data/lib/desiru/models/open_router.rb +161 -0
  74. data/lib/desiru/module.rb +67 -9
  75. data/lib/desiru/modules/best_of_n.rb +306 -0
  76. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  77. data/lib/desiru/modules/majority.rb +51 -0
  78. data/lib/desiru/modules/multi_chain_comparison.rb +256 -0
  79. data/lib/desiru/modules/predict.rb +15 -1
  80. data/lib/desiru/modules/program_of_thought.rb +338 -0
  81. data/lib/desiru/modules/react.rb +273 -0
  82. data/lib/desiru/modules/retrieve.rb +4 -2
  83. data/lib/desiru/optimizers/base.rb +32 -4
  84. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  85. data/lib/desiru/optimizers/copro.rb +268 -0
  86. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  87. data/lib/desiru/optimizers/mipro_v2.rb +889 -0
  88. data/lib/desiru/persistence/database.rb +71 -0
  89. data/lib/desiru/persistence/models/api_request.rb +38 -0
  90. data/lib/desiru/persistence/models/job_result.rb +138 -0
  91. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  92. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  93. data/lib/desiru/persistence/models/training_example.rb +25 -0
  94. data/lib/desiru/persistence/models.rb +11 -0
  95. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  96. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  97. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  98. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  99. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  100. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  101. data/lib/desiru/persistence/repository.rb +29 -0
  102. data/lib/desiru/persistence/setup.rb +77 -0
  103. data/lib/desiru/persistence.rb +49 -0
  104. data/lib/desiru/registry.rb +3 -5
  105. data/lib/desiru/signature.rb +91 -24
  106. data/lib/desiru/version.rb +1 -1
  107. data/lib/desiru.rb +33 -8
  108. data/missing-features-analysis.md +192 -0
  109. metadata +75 -45
  110. data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require 'logger'
5
+
6
+ module Desiru
7
+ module Persistence
8
+ # Database connection and migration management
9
+ module Database
10
+ class << self
11
+ attr_reader :connection
12
+
13
+ def connect(database_url = nil)
14
+ url = database_url || Persistence.database_url
15
+
16
+ @connection = Sequel.connect(
17
+ url,
18
+ logger: logger,
19
+ max_connections: max_connections
20
+ )
21
+
22
+ # Enable foreign keys for SQLite
23
+ @connection.run('PRAGMA foreign_keys = ON') if sqlite?
24
+
25
+ @connection
26
+ end
27
+
28
+ def disconnect
29
+ @connection&.disconnect
30
+ @connection = nil
31
+ end
32
+
33
+ def migrate!
34
+ raise 'Not connected to database' unless @connection
35
+
36
+ Sequel.extension :migration
37
+ migrations_path = File.expand_path('../../../db/migrations', __dir__)
38
+ Sequel::Migrator.run(@connection, migrations_path)
39
+
40
+ # Initialize persistence layer after migrations
41
+ require_relative 'setup'
42
+ Setup.initialize!(@connection)
43
+ end
44
+
45
+ def transaction(&)
46
+ raise 'Not connected to database' unless @connection
47
+
48
+ @connection.transaction(&)
49
+ end
50
+
51
+ private
52
+
53
+ def logger
54
+ return nil unless ENV['DESIRU_DEBUG'] || ENV['DEBUG']
55
+
56
+ Logger.new($stdout).tap do |logger|
57
+ logger.level = Logger::INFO
58
+ end
59
+ end
60
+
61
+ def max_connections
62
+ ENV['DESIRU_DB_MAX_CONNECTIONS']&.to_i || 10
63
+ end
64
+
65
+ def sqlite?
66
+ @connection&.adapter_scheme == :sqlite
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Models
6
+ # Tracks REST API requests
7
+ class ApiRequest < Base
8
+ set_dataset :api_requests
9
+ one_to_many :module_executions
10
+
11
+ json_column :headers
12
+ json_column :params
13
+ json_column :response_body
14
+
15
+ def validate
16
+ super
17
+ # Validate method column separately due to name conflict with Ruby's method method
18
+ if self[:method].nil? || self[:method].to_s.empty?
19
+ errors.add(:method, 'is required')
20
+ elsif !%w[GET POST PUT PATCH DELETE].include?(self[:method])
21
+ errors.add(:method, 'must be GET, POST, PUT, PATCH, or DELETE')
22
+ end
23
+ validates_presence %i[path status_code]
24
+ end
25
+
26
+ def success?
27
+ status_code >= 200 && status_code < 300
28
+ end
29
+
30
+ def duration_ms
31
+ return nil unless response_time
32
+
33
+ (response_time * 1000).round
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Models
6
+ # Model for storing background job results
7
+ class JobResult < Base
8
+ set_dataset :job_results
9
+
10
+ # Status constants
11
+ STATUS_PENDING = 'pending'
12
+ STATUS_PROCESSING = 'processing'
13
+ STATUS_COMPLETED = 'completed'
14
+ STATUS_FAILED = 'failed'
15
+
16
+ # Validations
17
+ def validate
18
+ super
19
+ validates_presence %i[job_id job_class queue status enqueued_at]
20
+ validates_unique :job_id if db&.table_exists?(:job_results)
21
+ validates_includes %w[pending processing completed failed], :status
22
+ end
23
+
24
+ # Scopes
25
+ dataset_module do
26
+ def pending
27
+ where(status: STATUS_PENDING)
28
+ end
29
+
30
+ def processing
31
+ where(status: STATUS_PROCESSING)
32
+ end
33
+
34
+ def completed
35
+ where(status: STATUS_COMPLETED)
36
+ end
37
+
38
+ def failed
39
+ where(status: STATUS_FAILED)
40
+ end
41
+
42
+ def expired
43
+ where { expires_at < Time.now }
44
+ end
45
+
46
+ def active
47
+ where { (expires_at > Time.now) | (expires_at =~ nil) }
48
+ end
49
+
50
+ def by_job_class(job_class)
51
+ where(job_class: job_class)
52
+ end
53
+
54
+ def recent(limit = 10)
55
+ order(Sequel.desc(:created_at)).limit(limit)
56
+ end
57
+ end
58
+
59
+ # Instance methods
60
+ def pending?
61
+ status == STATUS_PENDING
62
+ end
63
+
64
+ def processing?
65
+ status == STATUS_PROCESSING
66
+ end
67
+
68
+ def completed?
69
+ status == STATUS_COMPLETED
70
+ end
71
+
72
+ def failed?
73
+ status == STATUS_FAILED
74
+ end
75
+
76
+ def expired?
77
+ expires_at && expires_at < Time.now
78
+ end
79
+
80
+ def duration
81
+ return nil unless started_at && finished_at
82
+
83
+ finished_at - started_at
84
+ end
85
+
86
+ # JSON field accessors
87
+ def inputs_data
88
+ return {} unless inputs
89
+
90
+ JSON.parse(inputs, symbolize_names: true)
91
+ rescue JSON::ParserError
92
+ {}
93
+ end
94
+
95
+ def result_data
96
+ return {} unless result
97
+
98
+ JSON.parse(result, symbolize_names: true)
99
+ rescue JSON::ParserError
100
+ {}
101
+ end
102
+
103
+ def mark_as_processing!
104
+ update(
105
+ status: STATUS_PROCESSING,
106
+ started_at: Time.now,
107
+ progress: 0
108
+ )
109
+ end
110
+
111
+ def mark_as_completed!(result_data, message: nil)
112
+ update(
113
+ status: STATUS_COMPLETED,
114
+ finished_at: Time.now,
115
+ progress: 100,
116
+ result: result_data.to_json,
117
+ message: message
118
+ )
119
+ end
120
+
121
+ def mark_as_failed!(error, backtrace: nil)
122
+ update(
123
+ status: STATUS_FAILED,
124
+ finished_at: Time.now,
125
+ error_message: error.to_s,
126
+ error_backtrace: backtrace&.join("\n")
127
+ )
128
+ end
129
+
130
+ def update_progress(progress, message: nil)
131
+ updates = { progress: progress }
132
+ updates[:message] = message if message
133
+ update(updates)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Models
6
+ # Tracks module execution history
7
+ class ModuleExecution < Base
8
+ set_dataset :module_executions
9
+ many_to_one :api_request
10
+
11
+ json_column :inputs
12
+ json_column :outputs
13
+ json_column :metadata
14
+
15
+ def validate
16
+ super
17
+ validates_presence %i[module_name status started_at]
18
+ validates_includes %w[pending running completed failed], :status
19
+ end
20
+
21
+ def duration
22
+ return nil unless started_at && finished_at
23
+
24
+ finished_at - started_at
25
+ end
26
+
27
+ def success?
28
+ status == 'completed'
29
+ end
30
+
31
+ def failed?
32
+ status == 'failed'
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Models
6
+ # Stores optimization results and metrics
7
+ class OptimizationResult < Base
8
+ set_dataset :optimization_results
9
+ json_column :parameters
10
+ json_column :metrics
11
+ json_column :best_prompts
12
+
13
+ def validate
14
+ super
15
+ validates_presence %i[module_name optimizer_type score]
16
+ validates_numeric :score
17
+ validates_min_length 1, :training_size if training_size
18
+ end
19
+
20
+ def improvement_percentage
21
+ return nil unless baseline_score && score.positive?
22
+
23
+ ((score - baseline_score) / baseline_score * 100).round(2)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Models
6
+ # Stores training examples for modules
7
+ class TrainingExample < Base
8
+ set_dataset :training_examples
9
+ json_column :inputs
10
+ json_column :expected_outputs
11
+ json_column :metadata
12
+
13
+ def validate
14
+ super
15
+ validates_presence %i[module_name inputs]
16
+ validates_includes %w[training validation test], :dataset_type if dataset_type
17
+ end
18
+
19
+ def used?
20
+ used_count&.positive?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ # Namespace for Sequel models
6
+ module Models
7
+ # Base class will be defined during setup
8
+ Base = nil # rubocop:disable Naming/ConstantName
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_repository'
4
+
5
+ module Desiru
6
+ module Persistence
7
+ module Repositories
8
+ # Repository for API request records
9
+ class ApiRequestRepository < BaseRepository
10
+ def initialize
11
+ super(Models::ApiRequest)
12
+ end
13
+
14
+ def find_by_path(path)
15
+ dataset.where(path: path).all
16
+ end
17
+
18
+ def recent(limit = 10)
19
+ dataset
20
+ .order(Sequel.desc(:created_at))
21
+ .limit(limit)
22
+ .all
23
+ end
24
+
25
+ def by_status_code_range(min, max)
26
+ dataset.where(status_code: min..max).all
27
+ end
28
+
29
+ def successful
30
+ by_status_code_range(200, 299)
31
+ end
32
+
33
+ def failed
34
+ dataset.where { status_code >= 400 }.all
35
+ end
36
+
37
+ def average_response_time(path = nil)
38
+ scope = dataset
39
+ scope = scope.where(path: path) if path
40
+ scope = scope.exclude(response_time: nil)
41
+
42
+ avg = scope.avg(:response_time)
43
+ avg&.round(3)
44
+ end
45
+
46
+ def requests_per_minute(minutes_ago = 60)
47
+ since = Time.now - (minutes_ago * 60)
48
+ count = dataset.where { created_at >= since }.count
49
+
50
+ (count.to_f / minutes_ago).round(2)
51
+ end
52
+
53
+ def top_paths(limit = 10)
54
+ dataset
55
+ .group_and_count(:path)
56
+ .order(Sequel.desc(:count))
57
+ .limit(limit)
58
+ .map { |row| { path: row[:path], count: row[:count] } }
59
+ end
60
+
61
+ def create_from_rack_request(request, response)
62
+ create(
63
+ method: request.request_method,
64
+ path: request.path_info,
65
+ remote_ip: request.ip,
66
+ headers: extract_headers(request),
67
+ params: request.params,
68
+ status_code: response.status,
69
+ response_body: extract_response_body(response),
70
+ response_time: response.headers['X-Runtime']&.to_f
71
+ )
72
+ end
73
+
74
+ private
75
+
76
+ def extract_headers(request)
77
+ headers = {}
78
+ request.each_header do |key, value|
79
+ next unless key.start_with?('HTTP_')
80
+
81
+ header_name = key.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
82
+ headers[header_name] = value
83
+ end
84
+ headers
85
+ end
86
+
87
+ def extract_response_body(response)
88
+ return nil unless response.body.respond_to?(:each)
89
+
90
+ body = response.body.map { |part| part }
91
+ body.join
92
+ rescue StandardError
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ module Persistence
5
+ module Repositories
6
+ # Base repository with common CRUD operations
7
+ class BaseRepository
8
+ attr_reader :model_class
9
+
10
+ def initialize(model_class)
11
+ @model_class = model_class
12
+ end
13
+
14
+ def all
15
+ dataset.all
16
+ end
17
+
18
+ def find(id)
19
+ dataset.first(id: id)
20
+ end
21
+
22
+ def find_by(conditions)
23
+ dataset.where(conditions).first
24
+ end
25
+
26
+ def where(conditions)
27
+ dataset.where(conditions).all
28
+ end
29
+
30
+ def create(attributes)
31
+ model_class.create(attributes)
32
+ end
33
+
34
+ def update(id, attributes)
35
+ record = find(id)
36
+ return nil unless record
37
+
38
+ record.update(attributes)
39
+ record
40
+ end
41
+
42
+ def delete?(id)
43
+ record = find(id)
44
+ return false unless record
45
+
46
+ record.destroy
47
+ true
48
+ end
49
+
50
+ def count
51
+ dataset.count
52
+ end
53
+
54
+ def exists?(conditions)
55
+ dataset.where(conditions).any?
56
+ end
57
+
58
+ def paginate(page: 1, per_page: 20)
59
+ dataset
60
+ .limit(per_page)
61
+ .offset((page - 1) * per_page)
62
+ .all
63
+ end
64
+
65
+ protected
66
+
67
+ def dataset
68
+ model_class.dataset
69
+ end
70
+
71
+ def transaction(&)
72
+ Database.transaction(&)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_repository'
4
+
5
+ module Desiru
6
+ module Persistence
7
+ module Repositories
8
+ # Repository for job result persistence
9
+ class JobResultRepository < BaseRepository
10
+ def initialize
11
+ super(Models::JobResult)
12
+ end
13
+
14
+ def create_for_job(job_id, job_class, queue, inputs: nil, expires_at: nil)
15
+ create(
16
+ job_id: job_id,
17
+ job_class: job_class,
18
+ queue: queue,
19
+ status: Models::JobResult::STATUS_PENDING,
20
+ inputs: inputs&.to_json,
21
+ enqueued_at: Time.now,
22
+ expires_at: expires_at
23
+ )
24
+ end
25
+
26
+ def find_by_job_id(job_id)
27
+ find_by(job_id: job_id)
28
+ end
29
+
30
+ def mark_processing(job_id)
31
+ job_result = find_by_job_id(job_id)
32
+ return nil unless job_result
33
+
34
+ job_result.mark_as_processing!
35
+ job_result
36
+ end
37
+
38
+ def mark_completed(job_id, result, message: nil)
39
+ job_result = find_by_job_id(job_id)
40
+ return nil unless job_result
41
+
42
+ job_result.mark_as_completed!(result, message: message)
43
+ job_result
44
+ end
45
+
46
+ def mark_failed(job_id, error, backtrace: nil, increment_retry: true)
47
+ job_result = find_by_job_id(job_id)
48
+ return nil unless job_result
49
+
50
+ updates = {
51
+ status: Models::JobResult::STATUS_FAILED,
52
+ finished_at: Time.now,
53
+ error_message: error.to_s,
54
+ error_backtrace: backtrace&.join("\n")
55
+ }
56
+
57
+ updates[:retry_count] = job_result.retry_count + 1 if increment_retry
58
+
59
+ job_result.update(updates)
60
+ job_result
61
+ end
62
+
63
+ def update_progress(job_id, progress, message: nil)
64
+ job_result = find_by_job_id(job_id)
65
+ return nil unless job_result
66
+
67
+ job_result.update_progress(progress, message: message)
68
+ job_result
69
+ end
70
+
71
+ def cleanup_expired
72
+ dataset.expired.delete
73
+ end
74
+
75
+ def recent_by_class(job_class, limit: 10)
76
+ dataset.by_job_class(job_class).recent(limit).all
77
+ end
78
+
79
+ def statistics(job_class: nil, since: nil)
80
+ scope = dataset
81
+ scope = scope.by_job_class(job_class) if job_class
82
+ scope = scope.where { created_at >= since } if since
83
+
84
+ {
85
+ total: scope.count,
86
+ pending: scope.pending.count,
87
+ processing: scope.processing.count,
88
+ completed: scope.completed.count,
89
+ failed: scope.failed.count,
90
+ average_duration: calculate_average_duration(scope)
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ def calculate_average_duration(dataset)
97
+ completed = dataset.completed.where(Sequel.~(started_at: nil)).where(Sequel.~(finished_at: nil))
98
+ return 0 if completed.empty?
99
+
100
+ total_duration = 0
101
+ count = 0
102
+
103
+ completed.each do |job|
104
+ next unless job.started_at && job.finished_at
105
+
106
+ duration = job.finished_at - job.started_at
107
+ total_duration += duration
108
+ count += 1
109
+ end
110
+
111
+ count.positive? ? total_duration / count : 0
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end