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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +34 -0
  3. data/.rubocop.yml +7 -4
  4. data/.ruby-version +1 -0
  5. data/CLAUDE.md +4 -0
  6. data/Gemfile +21 -2
  7. data/Gemfile.lock +87 -12
  8. data/README.md +295 -2
  9. data/Rakefile +1 -0
  10. data/db/migrations/001_create_initial_tables.rb +96 -0
  11. data/db/migrations/002_create_job_results.rb +39 -0
  12. data/desiru.db +0 -0
  13. data/desiru.gemspec +2 -5
  14. data/docs/background_processing_roadmap.md +87 -0
  15. data/docs/job_scheduling.md +167 -0
  16. data/dspy-analysis-swarm.yml +60 -0
  17. data/dspy-feature-analysis.md +121 -0
  18. data/examples/README.md +69 -0
  19. data/examples/api_with_persistence.rb +122 -0
  20. data/examples/assertions_example.rb +232 -0
  21. data/examples/async_processing.rb +2 -0
  22. data/examples/few_shot_learning.rb +1 -2
  23. data/examples/graphql_api.rb +4 -2
  24. data/examples/graphql_integration.rb +3 -3
  25. data/examples/graphql_optimization_summary.md +143 -0
  26. data/examples/graphql_performance_benchmark.rb +247 -0
  27. data/examples/persistence_example.rb +102 -0
  28. data/examples/react_agent.rb +203 -0
  29. data/examples/rest_api.rb +173 -0
  30. data/examples/rest_api_advanced.rb +333 -0
  31. data/examples/scheduled_job_example.rb +116 -0
  32. data/examples/simple_qa.rb +1 -2
  33. data/examples/sinatra_api.rb +109 -0
  34. data/examples/typed_signatures.rb +1 -2
  35. data/graphql_optimization_summary.md +53 -0
  36. data/lib/desiru/api/grape_integration.rb +284 -0
  37. data/lib/desiru/api/persistence_middleware.rb +148 -0
  38. data/lib/desiru/api/sinatra_integration.rb +217 -0
  39. data/lib/desiru/api.rb +42 -0
  40. data/lib/desiru/assertions.rb +74 -0
  41. data/lib/desiru/async_status.rb +65 -0
  42. data/lib/desiru/cache.rb +1 -1
  43. data/lib/desiru/configuration.rb +2 -1
  44. data/lib/desiru/errors.rb +160 -0
  45. data/lib/desiru/field.rb +17 -14
  46. data/lib/desiru/graphql/batch_loader.rb +85 -0
  47. data/lib/desiru/graphql/data_loader.rb +242 -75
  48. data/lib/desiru/graphql/enum_builder.rb +75 -0
  49. data/lib/desiru/graphql/executor.rb +37 -4
  50. data/lib/desiru/graphql/schema_generator.rb +62 -158
  51. data/lib/desiru/graphql/type_builder.rb +138 -0
  52. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  53. data/lib/desiru/jobs/async_predict.rb +1 -1
  54. data/lib/desiru/jobs/base.rb +67 -0
  55. data/lib/desiru/jobs/batch_processor.rb +6 -6
  56. data/lib/desiru/jobs/retriable.rb +119 -0
  57. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  58. data/lib/desiru/jobs/scheduler.rb +219 -0
  59. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  60. data/lib/desiru/models/anthropic.rb +164 -0
  61. data/lib/desiru/models/base.rb +37 -3
  62. data/lib/desiru/models/open_ai.rb +151 -0
  63. data/lib/desiru/models/open_router.rb +161 -0
  64. data/lib/desiru/module.rb +59 -9
  65. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  66. data/lib/desiru/modules/majority.rb +51 -0
  67. data/lib/desiru/modules/multi_chain_comparison.rb +204 -0
  68. data/lib/desiru/modules/predict.rb +8 -1
  69. data/lib/desiru/modules/program_of_thought.rb +139 -0
  70. data/lib/desiru/modules/react.rb +273 -0
  71. data/lib/desiru/modules/retrieve.rb +4 -2
  72. data/lib/desiru/optimizers/base.rb +2 -4
  73. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  74. data/lib/desiru/optimizers/copro.rb +268 -0
  75. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  76. data/lib/desiru/persistence/database.rb +71 -0
  77. data/lib/desiru/persistence/models/api_request.rb +38 -0
  78. data/lib/desiru/persistence/models/job_result.rb +138 -0
  79. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  80. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  81. data/lib/desiru/persistence/models/training_example.rb +25 -0
  82. data/lib/desiru/persistence/models.rb +11 -0
  83. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  84. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  85. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  86. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  87. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  88. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  89. data/lib/desiru/persistence/repository.rb +29 -0
  90. data/lib/desiru/persistence/setup.rb +77 -0
  91. data/lib/desiru/persistence.rb +49 -0
  92. data/lib/desiru/registry.rb +3 -5
  93. data/lib/desiru/signature.rb +91 -24
  94. data/lib/desiru/version.rb +1 -1
  95. data/lib/desiru.rb +23 -8
  96. data/missing-features-analysis.md +192 -0
  97. metadata +63 -45
  98. 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
- # require_relative 'async_capable'
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
- # include AsyncCapable
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.validate_inputs(inputs)
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.validate_outputs(result)
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 config[:retry_on_failure] && @retry_count < Desiru.configuration.max_retries
57
+ if should_retry?(e)
57
58
  @retry_count += 1
58
- Desiru.configuration.logger&.warn("Retrying module execution (attempt #{@retry_count}/#{Desiru.configuration.max_retries})")
59
- sleep(Desiru.configuration.retry_delay)
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
- Desiru.configuration.logger&.error("Module execution failed: #{error.message}")
137
- raise ModuleError, "Module execution failed: #{error.message}"
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
- Format your response as:
25
- reasoning: [Your step-by-step thought process]
26
- [output fields]: [Your final answers]
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
- Respond with only the requested output fields in a clear format.
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