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,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open_router'
|
4
|
+
|
5
|
+
module Desiru
|
6
|
+
module Models
|
7
|
+
# OpenRouter model adapter - provides access to multiple models through a single API
|
8
|
+
class OpenRouter < Base
|
9
|
+
DEFAULT_MODEL = 'anthropic/claude-3-haiku'
|
10
|
+
|
11
|
+
def initialize(config = {})
|
12
|
+
super
|
13
|
+
@api_key = config[:api_key] || ENV.fetch('OPENROUTER_API_KEY', nil)
|
14
|
+
raise ArgumentError, 'OpenRouter API key is required' unless @api_key
|
15
|
+
|
16
|
+
# Configure OpenRouter client
|
17
|
+
::OpenRouter.configure do |c|
|
18
|
+
c.access_token = @api_key
|
19
|
+
c.site_name = config[:site_name] || 'Desiru'
|
20
|
+
c.site_url = config[:site_url] || 'https://github.com/obie/desiru'
|
21
|
+
end
|
22
|
+
|
23
|
+
@client = ::OpenRouter::Client.new
|
24
|
+
@models_cache = nil
|
25
|
+
@models_fetched_at = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def models
|
29
|
+
# Cache models for 1 hour
|
30
|
+
fetch_models if @models_cache.nil? || @models_fetched_at.nil? || (Time.now - @models_fetched_at) > 3600
|
31
|
+
@models_cache
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def perform_completion(messages, options)
|
37
|
+
model = options[:model] || @config[:model] || DEFAULT_MODEL
|
38
|
+
temperature = options[:temperature] || @config[:temperature] || 0.7
|
39
|
+
max_tokens = options[:max_tokens] || @config[:max_tokens] || 4096
|
40
|
+
|
41
|
+
# Prepare request parameters
|
42
|
+
params = {
|
43
|
+
model: model,
|
44
|
+
messages: messages,
|
45
|
+
temperature: temperature,
|
46
|
+
max_tokens: max_tokens
|
47
|
+
}
|
48
|
+
|
49
|
+
# Add provider-specific options if needed
|
50
|
+
params[:provider] = options[:provider] if options[:provider]
|
51
|
+
|
52
|
+
# Add response format if specified
|
53
|
+
params[:response_format] = options[:response_format] if options[:response_format]
|
54
|
+
|
55
|
+
# Add tools if provided (for models that support function calling)
|
56
|
+
if options[:tools]
|
57
|
+
params[:tools] = options[:tools]
|
58
|
+
params[:tool_choice] = options[:tool_choice] if options[:tool_choice]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Make API call
|
62
|
+
response = @client.complete(params)
|
63
|
+
|
64
|
+
# Format response
|
65
|
+
format_response(response, model)
|
66
|
+
rescue StandardError => e
|
67
|
+
handle_api_error(e)
|
68
|
+
end
|
69
|
+
|
70
|
+
def stream_complete(prompt, **options, &block)
|
71
|
+
messages = prepare_messages(prompt, options[:messages])
|
72
|
+
model = options[:model] || @config[:model] || DEFAULT_MODEL
|
73
|
+
temperature = options[:temperature] || @config[:temperature] || 0.7
|
74
|
+
max_tokens = options[:max_tokens] || @config[:max_tokens] || 4096
|
75
|
+
|
76
|
+
# Prepare streaming request
|
77
|
+
params = {
|
78
|
+
model: model,
|
79
|
+
messages: messages,
|
80
|
+
temperature: temperature,
|
81
|
+
max_tokens: max_tokens,
|
82
|
+
stream: true
|
83
|
+
}
|
84
|
+
|
85
|
+
# Stream response
|
86
|
+
@client.complete(params) do |chunk|
|
87
|
+
if chunk.dig('choices', 0, 'delta', 'content')
|
88
|
+
content = chunk.dig('choices', 0, 'delta', 'content')
|
89
|
+
block.call(content) if block_given?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
rescue StandardError => e
|
93
|
+
handle_api_error(e)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def fetch_models
|
99
|
+
# OpenRouter provides models at https://openrouter.ai/api/v1/models
|
100
|
+
response = @client.models
|
101
|
+
|
102
|
+
@models_cache = {}
|
103
|
+
response['data'].each do |model|
|
104
|
+
@models_cache[model['id']] = {
|
105
|
+
name: model['name'] || model['id'],
|
106
|
+
context_length: model['context_length'],
|
107
|
+
pricing: model['pricing'],
|
108
|
+
top_provider: model['top_provider']
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
@models_fetched_at = Time.now
|
113
|
+
@models_cache
|
114
|
+
rescue StandardError => e
|
115
|
+
Desiru.logger.warn("Failed to fetch OpenRouter models: #{e.message}")
|
116
|
+
# Fallback to commonly used models
|
117
|
+
@models_cache = {
|
118
|
+
'anthropic/claude-3-haiku' => { name: 'Claude 3 Haiku' },
|
119
|
+
'anthropic/claude-3-sonnet' => { name: 'Claude 3 Sonnet' },
|
120
|
+
'openai/gpt-4o-mini' => { name: 'GPT-4o Mini' },
|
121
|
+
'openai/gpt-4o' => { name: 'GPT-4o' },
|
122
|
+
'google/gemini-pro' => { name: 'Gemini Pro' }
|
123
|
+
}
|
124
|
+
@models_fetched_at = Time.now
|
125
|
+
@models_cache
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_response(response, model)
|
129
|
+
# OpenRouter uses OpenAI-compatible response format
|
130
|
+
content = response.dig('choices', 0, 'message', 'content') || ''
|
131
|
+
usage = response['usage'] || {}
|
132
|
+
|
133
|
+
{
|
134
|
+
content: content,
|
135
|
+
raw: response,
|
136
|
+
model: model,
|
137
|
+
usage: {
|
138
|
+
prompt_tokens: usage['prompt_tokens'] || 0,
|
139
|
+
completion_tokens: usage['completion_tokens'] || 0,
|
140
|
+
total_tokens: usage['total_tokens'] || 0
|
141
|
+
}
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def handle_api_error(error)
|
146
|
+
case error
|
147
|
+
when ::Faraday::UnauthorizedError
|
148
|
+
raise AuthenticationError, 'Invalid OpenRouter API key'
|
149
|
+
when ::Faraday::BadRequestError
|
150
|
+
raise InvalidRequestError, "Invalid request: #{error.message}"
|
151
|
+
when ::Faraday::TooManyRequestsError
|
152
|
+
raise RateLimitError, 'OpenRouter API rate limit exceeded'
|
153
|
+
when ::Faraday::PaymentRequiredError
|
154
|
+
raise APIError, 'OpenRouter payment required - check your account balance'
|
155
|
+
else
|
156
|
+
raise APIError, "OpenRouter API error: #{error.message}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/lib/desiru/module.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'async_capable'
|
4
|
+
require_relative 'assertions'
|
4
5
|
|
5
6
|
module Desiru
|
6
7
|
# Base class for all Desiru modules
|
7
8
|
# Implements the core module pattern with service-oriented design
|
8
9
|
class Module
|
9
10
|
extend Forwardable
|
10
|
-
|
11
|
+
include AsyncCapable
|
11
12
|
|
12
13
|
attr_reader :signature, :model, :config, :demos, :metadata
|
13
14
|
|
@@ -40,23 +41,23 @@ module Desiru
|
|
40
41
|
|
41
42
|
begin
|
42
43
|
# Validate inputs first, then coerce
|
43
|
-
signature.
|
44
|
+
signature.valid_inputs?(inputs)
|
44
45
|
coerced_inputs = signature.coerce_inputs(inputs)
|
45
46
|
|
46
47
|
# Execute the module logic
|
47
48
|
result = forward(**coerced_inputs)
|
48
49
|
|
49
50
|
# Validate outputs first, then coerce
|
50
|
-
signature.
|
51
|
+
signature.valid_outputs?(result)
|
51
52
|
coerced_outputs = signature.coerce_outputs(result)
|
52
53
|
|
53
54
|
# Return result object
|
54
55
|
ModuleResult.new(coerced_outputs, metadata: execution_metadata)
|
55
56
|
rescue StandardError => e
|
56
|
-
if
|
57
|
+
if should_retry?(e)
|
57
58
|
@retry_count += 1
|
58
|
-
|
59
|
-
sleep(
|
59
|
+
log_retry(e)
|
60
|
+
sleep(retry_delay_for(e))
|
60
61
|
retry
|
61
62
|
else
|
62
63
|
handle_error(e)
|
@@ -110,6 +111,44 @@ module Desiru
|
|
110
111
|
|
111
112
|
private
|
112
113
|
|
114
|
+
def should_retry?(error)
|
115
|
+
return false unless config[:retry_on_failure]
|
116
|
+
|
117
|
+
# Handle assertion errors specifically
|
118
|
+
return error.retriable? && @retry_count < max_retries_for(error) if error.is_a?(Assertions::AssertionError)
|
119
|
+
|
120
|
+
# Default retry logic for other errors
|
121
|
+
@retry_count < Desiru.configuration.max_retries
|
122
|
+
end
|
123
|
+
|
124
|
+
def max_retries_for(error)
|
125
|
+
if error.is_a?(Assertions::AssertionError)
|
126
|
+
Assertions.configuration.max_assertion_retries
|
127
|
+
else
|
128
|
+
Desiru.configuration.max_retries
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def retry_delay_for(error)
|
133
|
+
if error.is_a?(Assertions::AssertionError)
|
134
|
+
Assertions.configuration.assertion_retry_delay
|
135
|
+
else
|
136
|
+
Desiru.configuration.retry_delay
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def log_retry(error)
|
141
|
+
if error.is_a?(Assertions::AssertionError)
|
142
|
+
Desiru.configuration.logger&.warn(
|
143
|
+
"[ASSERTION RETRY] #{error.message} (attempt #{@retry_count}/#{max_retries_for(error)})"
|
144
|
+
)
|
145
|
+
else
|
146
|
+
Desiru.configuration.logger&.warn(
|
147
|
+
"Retrying module execution (attempt #{@retry_count}/#{Desiru.configuration.max_retries})"
|
148
|
+
)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
113
152
|
def validate_model!
|
114
153
|
return if model.nil? # Will use default
|
115
154
|
|
@@ -133,8 +172,19 @@ module Desiru
|
|
133
172
|
end
|
134
173
|
|
135
174
|
def handle_error(error)
|
136
|
-
|
137
|
-
|
175
|
+
if error.is_a?(Assertions::AssertionError)
|
176
|
+
# Update the assertion error with module context
|
177
|
+
error.instance_variable_set(:@module_name, self.class.name)
|
178
|
+
error.instance_variable_set(:@retry_count, @retry_count)
|
179
|
+
|
180
|
+
Desiru.configuration.logger&.error(
|
181
|
+
"[ASSERTION FAILED] #{error.message} in #{self.class.name} after #{@retry_count} retries"
|
182
|
+
)
|
183
|
+
raise error
|
184
|
+
else
|
185
|
+
Desiru.configuration.logger&.error("Module execution failed: #{error.message}")
|
186
|
+
raise ModuleError, "Module execution failed: #{error.message}"
|
187
|
+
end
|
138
188
|
end
|
139
189
|
end
|
140
190
|
|
@@ -21,9 +21,9 @@ module Desiru
|
|
21
21
|
|
22
22
|
Before providing the final answer, you must show your reasoning process. Think through the problem step by step.
|
23
23
|
|
24
|
-
|
25
|
-
reasoning:
|
26
|
-
|
24
|
+
Always format your response with each field on its own line like this:
|
25
|
+
reasoning: Your step-by-step thought process here
|
26
|
+
#{@original_signature.output_fields.keys.map { |field| "#{field}: Your #{field} here" }.join("\n")}
|
27
27
|
|
28
28
|
#{format_descriptions}
|
29
29
|
PROMPT
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
module Modules
|
5
|
+
# Function-style module for majority voting
|
6
|
+
# Returns the most common response from multiple completions
|
7
|
+
def self.majority(module_instance, **inputs)
|
8
|
+
raise ArgumentError, "First argument must be a Desiru module instance" unless module_instance.respond_to?(:call)
|
9
|
+
|
10
|
+
# Number of completions to generate
|
11
|
+
num_completions = inputs.delete(:num_completions) || 5
|
12
|
+
|
13
|
+
# Generate multiple completions
|
14
|
+
results = []
|
15
|
+
num_completions.times do
|
16
|
+
result = module_instance.call(**inputs)
|
17
|
+
results << result
|
18
|
+
end
|
19
|
+
|
20
|
+
# Find the majority answer
|
21
|
+
# For simplicity, we'll compare the first output field
|
22
|
+
output_fields = module_instance.signature.output_fields.keys
|
23
|
+
main_field = output_fields.first
|
24
|
+
|
25
|
+
# Count occurrences of each answer
|
26
|
+
answer_counts = Hash.new(0)
|
27
|
+
answer_to_result = {}
|
28
|
+
|
29
|
+
results.each do |result|
|
30
|
+
answer = result[main_field]
|
31
|
+
answer_counts[answer] += 1
|
32
|
+
answer_to_result[answer] ||= result
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return the result with the most common answer
|
36
|
+
majority_answer = answer_counts.max_by { |_, count| count }&.first
|
37
|
+
winning_result = answer_to_result[majority_answer] || results.first
|
38
|
+
|
39
|
+
# Add voting metadata if requested
|
40
|
+
if output_fields.include?(:voting_data)
|
41
|
+
winning_result[:voting_data] = {
|
42
|
+
votes: answer_counts,
|
43
|
+
num_completions: num_completions,
|
44
|
+
consensus_rate: answer_counts[majority_answer].to_f / num_completions
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
winning_result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Desiru
|
4
|
+
module Modules
|
5
|
+
# MultiChainComparison module that generates multiple chain-of-thought
|
6
|
+
# reasoning paths and compares them to produce the best answer
|
7
|
+
class MultiChainComparison < Desiru::Module
|
8
|
+
def initialize(signature = nil, model: nil, **kwargs)
|
9
|
+
super
|
10
|
+
@num_chains = kwargs[:num_chains] || 3
|
11
|
+
@comparison_strategy = kwargs[:comparison_strategy] || :vote
|
12
|
+
@temperature = kwargs[:temperature] || 0.7
|
13
|
+
end
|
14
|
+
|
15
|
+
def forward(**inputs)
|
16
|
+
# Generate multiple reasoning chains
|
17
|
+
chains = generate_chains(inputs)
|
18
|
+
|
19
|
+
# Compare chains to determine best answer
|
20
|
+
best_result = case @comparison_strategy
|
21
|
+
when :vote
|
22
|
+
vote_on_chains(chains)
|
23
|
+
when :llm_judge
|
24
|
+
llm_judge_chains(chains, inputs)
|
25
|
+
when :confidence
|
26
|
+
select_by_confidence(chains)
|
27
|
+
else
|
28
|
+
chains.first # Fallback to first chain
|
29
|
+
end
|
30
|
+
|
31
|
+
# Include comparison metadata if requested
|
32
|
+
if signature.output_fields.key?(:comparison_data)
|
33
|
+
best_result[:comparison_data] = {
|
34
|
+
num_chains: chains.length,
|
35
|
+
strategy: @comparison_strategy,
|
36
|
+
all_chains: chains.map { |c| c[:reasoning] }
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
best_result
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def generate_chains(inputs)
|
46
|
+
chains = []
|
47
|
+
|
48
|
+
@num_chains.times do |i|
|
49
|
+
chain_prompt = build_chain_prompt(inputs, i)
|
50
|
+
|
51
|
+
response = model.complete(
|
52
|
+
messages: [{ role: 'user', content: chain_prompt }],
|
53
|
+
temperature: @temperature
|
54
|
+
)
|
55
|
+
|
56
|
+
chain_result = parse_chain_response(response[:content])
|
57
|
+
chains << chain_result
|
58
|
+
end
|
59
|
+
|
60
|
+
chains
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_chain_prompt(inputs, chain_index)
|
64
|
+
prompt = "Please solve this problem step by step (Approach #{chain_index + 1}):\n\n"
|
65
|
+
|
66
|
+
# Add inputs
|
67
|
+
inputs.each do |key, value|
|
68
|
+
prompt += "#{key}: #{value}\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
prompt += "\nProvide your reasoning step by step, then give your final answer.\n"
|
72
|
+
prompt += "Format your response as:\n"
|
73
|
+
prompt += "REASONING: [Your step-by-step reasoning]\n"
|
74
|
+
prompt += "ANSWER: [Your final answer]\n"
|
75
|
+
|
76
|
+
# Add output field descriptions
|
77
|
+
if signature.output_fields.any?
|
78
|
+
prompt += "\nMake sure your answer includes:\n"
|
79
|
+
signature.output_fields.each do |name, field|
|
80
|
+
next if %i[reasoning comparison_data].include?(name)
|
81
|
+
|
82
|
+
prompt += "- #{name}: #{field.description || field.type}\n"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
prompt
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_chain_response(response)
|
90
|
+
result = {}
|
91
|
+
|
92
|
+
# Extract reasoning
|
93
|
+
reasoning_match = response.match(/REASONING:\s*(.+?)(?=ANSWER:|$)/mi)
|
94
|
+
result[:reasoning] = reasoning_match ? reasoning_match[1].strip : response
|
95
|
+
|
96
|
+
# Extract answer
|
97
|
+
answer_match = response.match(/ANSWER:\s*(.+)/mi)
|
98
|
+
answer_text = answer_match ? answer_match[1].strip : ""
|
99
|
+
|
100
|
+
# Try to parse structured answer
|
101
|
+
if answer_text.include?(':') || answer_text.include?('{')
|
102
|
+
result.merge!(parse_structured_answer(answer_text))
|
103
|
+
else
|
104
|
+
# Single value answer
|
105
|
+
main_output_field = signature.output_fields.keys.find { |k| !%i[reasoning comparison_data].include?(k) }
|
106
|
+
result[main_output_field] = answer_text if main_output_field
|
107
|
+
end
|
108
|
+
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_structured_answer(answer_text)
|
113
|
+
parsed = {}
|
114
|
+
|
115
|
+
# Try to parse as key-value pairs
|
116
|
+
answer_text.scan(/(\w+):\s*([^\n,}]+)/).each do |key, value|
|
117
|
+
key_sym = key.downcase.to_sym
|
118
|
+
parsed[key_sym] = value.strip if signature.output_fields.key?(key_sym)
|
119
|
+
end
|
120
|
+
|
121
|
+
parsed
|
122
|
+
end
|
123
|
+
|
124
|
+
def vote_on_chains(chains)
|
125
|
+
# Count votes for each unique answer
|
126
|
+
votes = Hash.new(0)
|
127
|
+
answer_to_chain = {}
|
128
|
+
|
129
|
+
chains.each do |chain|
|
130
|
+
# Get the main answer field (first non-metadata field)
|
131
|
+
answer_key = signature.output_fields.keys.find { |k| !%i[reasoning comparison_data].include?(k) }
|
132
|
+
answer_value = chain[answer_key]
|
133
|
+
|
134
|
+
if answer_value
|
135
|
+
votes[answer_value] += 1
|
136
|
+
answer_to_chain[answer_value] ||= chain
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the chain with the most common answer
|
141
|
+
winning_answer = votes.max_by { |_, count| count }&.first
|
142
|
+
answer_to_chain[winning_answer] || chains.first
|
143
|
+
end
|
144
|
+
|
145
|
+
def llm_judge_chains(chains, original_inputs)
|
146
|
+
judge_prompt = "Given the following problem and multiple solution attempts, select the best answer:\n\n"
|
147
|
+
|
148
|
+
# Add original inputs
|
149
|
+
judge_prompt += "Original Problem:\n"
|
150
|
+
original_inputs.each do |key, value|
|
151
|
+
judge_prompt += "#{key}: #{value}\n"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Add all chains
|
155
|
+
judge_prompt += "\nSolution Attempts:\n"
|
156
|
+
chains.each_with_index do |chain, i|
|
157
|
+
judge_prompt += "\n--- Attempt #{i + 1} ---\n"
|
158
|
+
judge_prompt += "Reasoning: #{chain[:reasoning]}\n"
|
159
|
+
|
160
|
+
answer_key = signature.output_fields.keys.find { |k| !%i[reasoning comparison_data].include?(k) }
|
161
|
+
judge_prompt += "Answer: #{chain[answer_key]}\n" if chain[answer_key]
|
162
|
+
end
|
163
|
+
|
164
|
+
judge_prompt += "\nSelect the best attempt (1-#{chains.length}) and explain why:"
|
165
|
+
|
166
|
+
response = model.complete(
|
167
|
+
messages: [{ role: 'user', content: judge_prompt }],
|
168
|
+
temperature: 0.1 # Low temperature for more consistent judgment
|
169
|
+
)
|
170
|
+
|
171
|
+
# Extract selected chain index
|
172
|
+
selection_match = response[:content].match(/(?:attempt|option|choice)\s*#?(\d+)/i)
|
173
|
+
selected_index = selection_match ? selection_match[1].to_i - 1 : 0
|
174
|
+
selected_index = selected_index.clamp(0, chains.length - 1)
|
175
|
+
|
176
|
+
chains[selected_index]
|
177
|
+
end
|
178
|
+
|
179
|
+
def select_by_confidence(chains)
|
180
|
+
# Ask model to rate confidence for each chain
|
181
|
+
chains_with_confidence = chains.map do |chain|
|
182
|
+
confidence_prompt = "Rate your confidence (0-100) in this reasoning and answer:\n"
|
183
|
+
confidence_prompt += "Reasoning: #{chain[:reasoning]}\n"
|
184
|
+
|
185
|
+
answer_key = signature.output_fields.keys.find { |k| !%i[reasoning comparison_data].include?(k) }
|
186
|
+
confidence_prompt += "Answer: #{chain[answer_key]}\n" if chain[answer_key]
|
187
|
+
|
188
|
+
confidence_prompt += "\nRespond with just a number between 0 and 100:"
|
189
|
+
|
190
|
+
response = model.complete(
|
191
|
+
messages: [{ role: 'user', content: confidence_prompt }],
|
192
|
+
temperature: 0.1
|
193
|
+
)
|
194
|
+
|
195
|
+
confidence = response[:content].scan(/\d+/).first&.to_i || 50
|
196
|
+
chain.merge(confidence: confidence)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Select chain with highest confidence
|
200
|
+
chains_with_confidence.max_by { |c| c[:confidence] }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -14,6 +14,8 @@ module Desiru
|
|
14
14
|
demos: demos
|
15
15
|
)
|
16
16
|
|
17
|
+
Desiru.logger.info("Predict response: #{response}")
|
18
|
+
|
17
19
|
parse_response(response[:content])
|
18
20
|
end
|
19
21
|
|
@@ -32,7 +34,12 @@ module Desiru
|
|
32
34
|
|
33
35
|
#{format_signature}
|
34
36
|
|
35
|
-
|
37
|
+
Format your response with each output field on its own line using the pattern:
|
38
|
+
field_name: value
|
39
|
+
|
40
|
+
For example, if the output field is "answer", write:
|
41
|
+
answer: Your answer here
|
42
|
+
|
36
43
|
#{format_descriptions}
|
37
44
|
PROMPT
|
38
45
|
end
|