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,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
module Optimizers
|
5
|
+
# COPRO (Cooperative Prompt Optimization) optimizer
|
6
|
+
# Generates and refines instructions for each module using coordinate ascent
|
7
|
+
class COPRO < Base
|
8
|
+
def initialize(config = {})
|
9
|
+
super
|
10
|
+
@max_iterations = config[:max_iterations] || 10
|
11
|
+
@num_candidates = config[:num_candidates] || 5
|
12
|
+
@temperature = config[:temperature] || 0.7
|
13
|
+
@improvement_threshold = config[:improvement_threshold] || 0.01
|
14
|
+
end
|
15
|
+
|
16
|
+
def compile(program, trainset, valset = nil, **kwargs)
|
17
|
+
valset ||= trainset # Use trainset for validation if no valset provided
|
18
|
+
|
19
|
+
# Initialize best score
|
20
|
+
best_score = evaluate_program(program, valset, kwargs[:metric])
|
21
|
+
best_program = program.dup
|
22
|
+
|
23
|
+
Desiru.logger.info("[COPRO] Initial score: #{best_score}")
|
24
|
+
|
25
|
+
# Iterate through optimization rounds
|
26
|
+
@max_iterations.times do |iteration|
|
27
|
+
Desiru.logger.info("[COPRO] Starting iteration #{iteration + 1}/#{@max_iterations}")
|
28
|
+
|
29
|
+
# Try to improve each predictor
|
30
|
+
improved = false
|
31
|
+
|
32
|
+
program.predictors.each do |name, predictor|
|
33
|
+
Desiru.logger.info("[COPRO] Optimizing predictor: #{name}")
|
34
|
+
|
35
|
+
# Generate instruction candidates
|
36
|
+
candidates = generate_instruction_candidates(predictor, trainset, name)
|
37
|
+
|
38
|
+
# Evaluate each candidate
|
39
|
+
best_candidate_score = best_score
|
40
|
+
best_candidate_instruction = nil
|
41
|
+
|
42
|
+
candidates.each do |instruction|
|
43
|
+
# Create program with new instruction
|
44
|
+
candidate_program = create_program_with_instruction(
|
45
|
+
best_program,
|
46
|
+
name,
|
47
|
+
instruction
|
48
|
+
)
|
49
|
+
|
50
|
+
# Evaluate
|
51
|
+
score = evaluate_program(candidate_program, valset, kwargs[:metric])
|
52
|
+
|
53
|
+
if score > best_candidate_score
|
54
|
+
best_candidate_score = score
|
55
|
+
best_candidate_instruction = instruction
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Update if improved
|
60
|
+
next unless best_candidate_instruction && (best_candidate_score - best_score) > @improvement_threshold
|
61
|
+
|
62
|
+
Desiru.logger.info("[COPRO] Improved #{name}: #{best_score} -> #{best_candidate_score}")
|
63
|
+
best_program = create_program_with_instruction(
|
64
|
+
best_program,
|
65
|
+
name,
|
66
|
+
best_candidate_instruction
|
67
|
+
)
|
68
|
+
best_score = best_candidate_score
|
69
|
+
improved = true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Early stopping if no improvement
|
73
|
+
break unless improved
|
74
|
+
end
|
75
|
+
|
76
|
+
Desiru.logger.info("[COPRO] Final score: #{best_score}")
|
77
|
+
best_program
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def generate_instruction_candidates(predictor, trainset, predictor_name)
|
83
|
+
candidates = []
|
84
|
+
|
85
|
+
# Get examples of good performance
|
86
|
+
good_examples = select_good_examples(predictor, trainset)
|
87
|
+
|
88
|
+
# Generate initial instruction based on signature
|
89
|
+
signature = predictor.signature
|
90
|
+
base_instruction = generate_base_instruction(signature, predictor_name)
|
91
|
+
candidates << base_instruction
|
92
|
+
|
93
|
+
# Generate variations
|
94
|
+
(@num_candidates - 1).times do |i|
|
95
|
+
variation_prompt = build_variation_prompt(
|
96
|
+
base_instruction,
|
97
|
+
signature,
|
98
|
+
good_examples,
|
99
|
+
i
|
100
|
+
)
|
101
|
+
|
102
|
+
response = model.complete(
|
103
|
+
messages: [{ role: 'user', content: variation_prompt }],
|
104
|
+
temperature: @temperature
|
105
|
+
)
|
106
|
+
|
107
|
+
instruction = extract_instruction(response[:content])
|
108
|
+
candidates << instruction if instruction
|
109
|
+
end
|
110
|
+
|
111
|
+
candidates.compact.uniq
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_base_instruction(signature, predictor_name)
|
115
|
+
instruction = "You are solving a #{predictor_name} task.\n\n"
|
116
|
+
|
117
|
+
# Add input description
|
118
|
+
if signature.input_fields.any?
|
119
|
+
instruction += "Given the following inputs:\n"
|
120
|
+
signature.input_fields.each do |name, field|
|
121
|
+
instruction += "- #{name}: #{field.description || field.type}\n"
|
122
|
+
end
|
123
|
+
instruction += "\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add output description
|
127
|
+
if signature.output_fields.any?
|
128
|
+
instruction += "Produce the following outputs:\n"
|
129
|
+
signature.output_fields.each do |name, field|
|
130
|
+
instruction += "- #{name}: #{field.description || field.type}\n"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
instruction
|
135
|
+
end
|
136
|
+
|
137
|
+
def build_variation_prompt(base_instruction, signature, good_examples, variation_index)
|
138
|
+
prompt = "Improve the following instruction for better performance:\n\n"
|
139
|
+
prompt += "Current instruction:\n#{base_instruction}\n\n"
|
140
|
+
|
141
|
+
# Add task context
|
142
|
+
prompt += "Task signature: #{signature}\n\n"
|
143
|
+
|
144
|
+
# Add examples of good performance
|
145
|
+
if good_examples.any?
|
146
|
+
prompt += "Examples of successful completions:\n"
|
147
|
+
good_examples.take(3).each do |example|
|
148
|
+
prompt += format_example(example)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Request specific type of improvement
|
153
|
+
improvement_types = [
|
154
|
+
"Make the instruction more specific and detailed",
|
155
|
+
"Add helpful constraints or guidelines",
|
156
|
+
"Clarify any ambiguous requirements",
|
157
|
+
"Add examples or patterns to follow",
|
158
|
+
"Emphasize important aspects of the task"
|
159
|
+
]
|
160
|
+
|
161
|
+
prompt += "\n#{improvement_types[variation_index % improvement_types.length]}.\n"
|
162
|
+
prompt += "Provide only the improved instruction:"
|
163
|
+
|
164
|
+
prompt
|
165
|
+
end
|
166
|
+
|
167
|
+
def select_good_examples(predictor, trainset)
|
168
|
+
good_examples = []
|
169
|
+
|
170
|
+
trainset.each do |example|
|
171
|
+
# Run predictor on example inputs
|
172
|
+
result = predictor.call(example[:inputs])
|
173
|
+
|
174
|
+
# Check if output matches expected
|
175
|
+
good_examples << example if outputs_match?(result, example[:outputs])
|
176
|
+
rescue StandardError
|
177
|
+
# Skip failed examples
|
178
|
+
end
|
179
|
+
|
180
|
+
good_examples
|
181
|
+
end
|
182
|
+
|
183
|
+
def outputs_match?(actual, expected)
|
184
|
+
return false unless actual.is_a?(Hash) && expected.is_a?(Hash)
|
185
|
+
|
186
|
+
expected.all? do |key, expected_value|
|
187
|
+
actual_value = actual[key]
|
188
|
+
|
189
|
+
# Flexible matching for different types
|
190
|
+
case expected_value
|
191
|
+
when String
|
192
|
+
actual_value.to_s.strip.downcase == expected_value.strip.downcase
|
193
|
+
when Numeric
|
194
|
+
(actual_value.to_f - expected_value.to_f).abs < 0.001
|
195
|
+
else
|
196
|
+
actual_value == expected_value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def format_example(example)
|
202
|
+
formatted = "\nExample:\n"
|
203
|
+
|
204
|
+
if example[:inputs]
|
205
|
+
formatted += "Inputs: "
|
206
|
+
formatted += example[:inputs].map { |k, v| "#{k}=#{v}" }.join(", ")
|
207
|
+
formatted += "\n"
|
208
|
+
end
|
209
|
+
|
210
|
+
if example[:outputs]
|
211
|
+
formatted += "Outputs: "
|
212
|
+
formatted += example[:outputs].map { |k, v| "#{k}=#{v}" }.join(", ")
|
213
|
+
formatted += "\n"
|
214
|
+
end
|
215
|
+
|
216
|
+
formatted
|
217
|
+
end
|
218
|
+
|
219
|
+
def extract_instruction(response)
|
220
|
+
# Clean up the response
|
221
|
+
instruction = response.strip
|
222
|
+
|
223
|
+
# Remove any meta-commentary
|
224
|
+
instruction = instruction.sub(/^(Here's |This is )?the improved instruction:?\s*/i, '')
|
225
|
+
instruction = instruction.sub(/^Improved instruction:?\s*/i, '')
|
226
|
+
|
227
|
+
# Remove quotes if wrapped
|
228
|
+
instruction.gsub(/^["']|["']$/, '')
|
229
|
+
end
|
230
|
+
|
231
|
+
def create_program_with_instruction(program, predictor_name, instruction)
|
232
|
+
new_program = program.dup
|
233
|
+
|
234
|
+
# Get the predictor
|
235
|
+
predictor = new_program.predictors[predictor_name]
|
236
|
+
return new_program unless predictor
|
237
|
+
|
238
|
+
# Create new predictor with updated instruction
|
239
|
+
new_predictor = predictor.dup
|
240
|
+
new_predictor.instance_variable_set(:@instruction, instruction)
|
241
|
+
|
242
|
+
# Update the program
|
243
|
+
new_program.instance_variable_set("@#{predictor_name}", new_predictor)
|
244
|
+
|
245
|
+
new_program
|
246
|
+
end
|
247
|
+
|
248
|
+
def evaluate_program(program, dataset, metric)
|
249
|
+
scores = []
|
250
|
+
|
251
|
+
dataset.each do |example|
|
252
|
+
# Run program
|
253
|
+
prediction = program.forward(**example[:inputs])
|
254
|
+
|
255
|
+
# Calculate score
|
256
|
+
score = metric.call(prediction, example[:outputs])
|
257
|
+
scores << score
|
258
|
+
rescue StandardError => e
|
259
|
+
Desiru.logger.debug("[COPRO] Evaluation error: #{e.message}")
|
260
|
+
scores << 0.0
|
261
|
+
end
|
262
|
+
|
263
|
+
# Return average score
|
264
|
+
scores.empty? ? 0.0 : scores.sum.to_f / scores.length
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
module Optimizers
|
5
|
+
# KNNFewShot optimizer that uses k-Nearest Neighbors to find similar examples
|
6
|
+
# for few-shot learning. It finds the most similar training examples to each
|
7
|
+
# input and uses them as demonstrations.
|
8
|
+
class KNNFewShot < Base
|
9
|
+
def initialize(config = {})
|
10
|
+
super
|
11
|
+
@k = config[:k] || 3 # Number of nearest neighbors
|
12
|
+
@similarity_metric = config[:similarity_metric] || :cosine
|
13
|
+
@embedding_cache = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def compile(program, trainset, **_kwargs)
|
17
|
+
# Build index of training examples
|
18
|
+
build_example_index(trainset)
|
19
|
+
|
20
|
+
# Create optimized program with KNN-based few-shot selection
|
21
|
+
optimized_program = program.dup
|
22
|
+
|
23
|
+
# Wrap each predict module with KNN few-shot enhancement
|
24
|
+
optimized_program.predictors.each do |name, predictor|
|
25
|
+
optimized_predictor = create_knn_predictor(predictor, name)
|
26
|
+
optimized_program.instance_variable_set("@#{name}", optimized_predictor)
|
27
|
+
end
|
28
|
+
|
29
|
+
optimized_program
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def build_example_index(trainset)
|
35
|
+
@example_embeddings = []
|
36
|
+
@example_data = []
|
37
|
+
|
38
|
+
trainset.each do |example|
|
39
|
+
# Create text representation of the example
|
40
|
+
example_text = serialize_example(example)
|
41
|
+
|
42
|
+
# Generate embedding (simplified - in practice, use a real embedding model)
|
43
|
+
embedding = generate_embedding(example_text)
|
44
|
+
|
45
|
+
@example_embeddings << embedding
|
46
|
+
@example_data << example
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_knn_predictor(original_predictor, predictor_name)
|
51
|
+
knn_predictor = original_predictor.dup
|
52
|
+
example_embeddings = @example_embeddings
|
53
|
+
example_data = @example_data
|
54
|
+
k = @k
|
55
|
+
similarity_metric = @similarity_metric
|
56
|
+
|
57
|
+
# Override the forward method to include KNN examples
|
58
|
+
knn_predictor.define_singleton_method(:forward) do |**inputs|
|
59
|
+
# Find nearest neighbors for this input
|
60
|
+
input_text = inputs.map { |k, v| "#{k}: #{v}" }.join("\n")
|
61
|
+
input_embedding = generate_embedding(input_text)
|
62
|
+
|
63
|
+
nearest_examples = find_nearest_neighbors(
|
64
|
+
input_embedding,
|
65
|
+
example_embeddings,
|
66
|
+
example_data,
|
67
|
+
k,
|
68
|
+
similarity_metric
|
69
|
+
)
|
70
|
+
|
71
|
+
# Format examples for few-shot learning
|
72
|
+
demonstrations = format_demonstrations(nearest_examples, predictor_name)
|
73
|
+
|
74
|
+
# Enhance the prompt with demonstrations
|
75
|
+
enhanced_prompt = build_enhanced_prompt(inputs, demonstrations)
|
76
|
+
|
77
|
+
# Call original predictor with enhanced prompt
|
78
|
+
super(**inputs, few_shot_examples: enhanced_prompt)
|
79
|
+
end
|
80
|
+
|
81
|
+
knn_predictor
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_embedding(text)
|
85
|
+
# Cache embeddings to avoid recomputation
|
86
|
+
return @embedding_cache[text] if @embedding_cache.key?(text)
|
87
|
+
|
88
|
+
# Simplified embedding generation
|
89
|
+
# In practice, use a real embedding model like OpenAI's text-embedding-ada-002
|
90
|
+
words = text.downcase.split(/\W+/)
|
91
|
+
embedding = Array.new(100, 0.0)
|
92
|
+
|
93
|
+
words.each do |word|
|
94
|
+
# Simple hash-based pseudo-embedding
|
95
|
+
hash_value = word.hash
|
96
|
+
100.times do |i|
|
97
|
+
embedding[i] += Math.sin(hash_value * (i + 1)) / Math.sqrt(words.length + 1)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Normalize
|
102
|
+
magnitude = Math.sqrt(embedding.sum { |x| x * x })
|
103
|
+
embedding = embedding.map { |x| x / (magnitude + 1e-10) }
|
104
|
+
|
105
|
+
@embedding_cache[text] = embedding
|
106
|
+
embedding
|
107
|
+
end
|
108
|
+
|
109
|
+
def find_nearest_neighbors(query_embedding, embeddings, data, num_neighbors, metric)
|
110
|
+
# Calculate distances
|
111
|
+
distances = embeddings.map.with_index do |embedding, idx|
|
112
|
+
distance = case metric
|
113
|
+
when :cosine
|
114
|
+
cosine_distance(query_embedding, embedding)
|
115
|
+
when :euclidean
|
116
|
+
euclidean_distance(query_embedding, embedding)
|
117
|
+
else
|
118
|
+
raise ArgumentError, "Unknown similarity metric: #{metric}"
|
119
|
+
end
|
120
|
+
{ distance: distance, index: idx }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Sort by distance and take top k
|
124
|
+
nearest = distances.sort_by { |d| d[:distance] }.take(num_neighbors)
|
125
|
+
nearest.map { |d| data[d[:index]] }
|
126
|
+
end
|
127
|
+
|
128
|
+
def cosine_distance(vec1, vec2)
|
129
|
+
dot_product = vec1.zip(vec2).sum { |a, b| a * b }
|
130
|
+
1.0 - dot_product # Convert similarity to distance
|
131
|
+
end
|
132
|
+
|
133
|
+
def euclidean_distance(vec1, vec2)
|
134
|
+
Math.sqrt(vec1.zip(vec2).sum { |a, b| (a - b)**2 })
|
135
|
+
end
|
136
|
+
|
137
|
+
def serialize_example(example)
|
138
|
+
parts = []
|
139
|
+
|
140
|
+
# Add inputs
|
141
|
+
if example[:inputs]
|
142
|
+
parts << "Inputs:"
|
143
|
+
example[:inputs].each { |k, v| parts << " #{k}: #{v}" }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Add expected outputs
|
147
|
+
if example[:outputs]
|
148
|
+
parts << "Outputs:"
|
149
|
+
example[:outputs].each { |k, v| parts << " #{k}: #{v}" }
|
150
|
+
end
|
151
|
+
|
152
|
+
parts.join("\n")
|
153
|
+
end
|
154
|
+
|
155
|
+
def format_demonstrations(examples, _predictor_name)
|
156
|
+
demonstrations = []
|
157
|
+
|
158
|
+
examples.each_with_index do |example, idx|
|
159
|
+
demo = "Example #{idx + 1}:\n"
|
160
|
+
|
161
|
+
if example[:inputs]
|
162
|
+
demo += "Input:\n"
|
163
|
+
example[:inputs].each { |k, v| demo += " #{k}: #{v}\n" }
|
164
|
+
end
|
165
|
+
|
166
|
+
if example[:outputs]
|
167
|
+
demo += "Output:\n"
|
168
|
+
example[:outputs].each { |k, v| demo += " #{k}: #{v}\n" }
|
169
|
+
end
|
170
|
+
|
171
|
+
demonstrations << demo
|
172
|
+
end
|
173
|
+
|
174
|
+
demonstrations.join("\n---\n")
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_enhanced_prompt(_inputs, demonstrations)
|
178
|
+
prompt = "Here are some similar examples:\n\n"
|
179
|
+
prompt += demonstrations
|
180
|
+
prompt += "\n\nNow, given the following input, provide the output:\n"
|
181
|
+
prompt
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -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
|