soka 0.0.1.beta2
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +365 -0
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +213 -0
- data/LICENSE +21 -0
- data/README.md +650 -0
- data/Rakefile +10 -0
- data/examples/1_basic.rb +94 -0
- data/examples/2_event_handling.rb +120 -0
- data/examples/3_memory.rb +182 -0
- data/examples/4_hooks.rb +140 -0
- data/examples/5_error_handling.rb +85 -0
- data/examples/6_retry.rb +164 -0
- data/examples/7_tool_conditional.rb +180 -0
- data/examples/8_multi_provider.rb +112 -0
- data/lib/soka/agent.rb +130 -0
- data/lib/soka/agent_tool.rb +146 -0
- data/lib/soka/agent_tools/params_validator.rb +139 -0
- data/lib/soka/agents/dsl_methods.rb +140 -0
- data/lib/soka/agents/hook_manager.rb +68 -0
- data/lib/soka/agents/llm_builder.rb +32 -0
- data/lib/soka/agents/retry_handler.rb +74 -0
- data/lib/soka/agents/tool_builder.rb +78 -0
- data/lib/soka/configuration.rb +60 -0
- data/lib/soka/engines/base.rb +67 -0
- data/lib/soka/engines/concerns/prompt_template.rb +130 -0
- data/lib/soka/engines/concerns/response_processor.rb +103 -0
- data/lib/soka/engines/react.rb +136 -0
- data/lib/soka/engines/reasoning_context.rb +92 -0
- data/lib/soka/llm.rb +85 -0
- data/lib/soka/llms/anthropic.rb +124 -0
- data/lib/soka/llms/base.rb +114 -0
- data/lib/soka/llms/concerns/response_parser.rb +47 -0
- data/lib/soka/llms/concerns/streaming_handler.rb +78 -0
- data/lib/soka/llms/gemini.rb +106 -0
- data/lib/soka/llms/openai.rb +97 -0
- data/lib/soka/memory.rb +83 -0
- data/lib/soka/result.rb +136 -0
- data/lib/soka/test_helpers.rb +162 -0
- data/lib/soka/thoughts_memory.rb +112 -0
- data/lib/soka/version.rb +5 -0
- data/lib/soka.rb +49 -0
- data/sig/soka.rbs +4 -0
- metadata +158 -0
data/examples/6_retry.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'soka'
|
6
|
+
require 'dotenv/load'
|
7
|
+
|
8
|
+
# Configure Soka
|
9
|
+
Soka.setup do |config|
|
10
|
+
config.ai do |ai|
|
11
|
+
ai.provider = :gemini
|
12
|
+
ai.model = 'gemini-2.5-flash-lite'
|
13
|
+
ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Flaky API tool that fails randomly
|
18
|
+
class FlakyApiTool < Soka::AgentTool
|
19
|
+
desc 'Call unreliable API'
|
20
|
+
|
21
|
+
params do
|
22
|
+
requires :action, String, desc: 'Action to perform'
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
super
|
27
|
+
@call_count = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(action:)
|
31
|
+
@call_count += 1
|
32
|
+
puts " š API call attempt ##{@call_count} for: #{action}"
|
33
|
+
|
34
|
+
# Fail 60% of the time on first 2 attempts
|
35
|
+
if @call_count <= 2 && rand < 0.6
|
36
|
+
raise StandardError, "Network timeout on attempt #{@call_count}"
|
37
|
+
end
|
38
|
+
|
39
|
+
"Success: #{action} completed after #{@call_count} attempts"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Rate limited tool
|
44
|
+
class RateLimitedTool < Soka::AgentTool
|
45
|
+
desc 'API with rate limiting'
|
46
|
+
|
47
|
+
params do
|
48
|
+
requires :query, String, desc: 'Query to execute'
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
super
|
53
|
+
@last_call = Time.now - 10
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(query:)
|
57
|
+
time_since_last = Time.now - @last_call
|
58
|
+
|
59
|
+
if time_since_last < 1
|
60
|
+
raise StandardError, 'Rate limit exceeded. Please wait 1 second between calls.'
|
61
|
+
end
|
62
|
+
|
63
|
+
@last_call = Time.now
|
64
|
+
"Query result for: #{query}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Agent with retry configuration
|
69
|
+
class RetryAgent < Soka::Agent
|
70
|
+
tool FlakyApiTool
|
71
|
+
tool RateLimitedTool
|
72
|
+
|
73
|
+
# The framework has built-in retry with exponential backoff
|
74
|
+
end
|
75
|
+
|
76
|
+
# Agent with custom configuration
|
77
|
+
class CustomRetryAgent < Soka::Agent
|
78
|
+
tool FlakyApiTool
|
79
|
+
|
80
|
+
# Configure max iterations (which affects retry behavior)
|
81
|
+
max_iterations 2
|
82
|
+
end
|
83
|
+
|
84
|
+
# Main program
|
85
|
+
puts '=== Soka Retry Example ==='
|
86
|
+
puts "Demonstrating retry mechanisms\n\n"
|
87
|
+
|
88
|
+
# Example 1: Basic retry with exponential backoff
|
89
|
+
puts 'Example 1: Retry with exponential backoff'
|
90
|
+
puts '-' * 50
|
91
|
+
|
92
|
+
agent = RetryAgent.new
|
93
|
+
puts "Calling flaky API (may fail and retry)..."
|
94
|
+
|
95
|
+
result = agent.run('Call the flaky API to fetch user data')
|
96
|
+
puts "\nFinal result: #{result.final_answer}"
|
97
|
+
puts "Success after retries: #{!result.failed?}"
|
98
|
+
|
99
|
+
puts "\n" + '=' * 50 + "\n"
|
100
|
+
|
101
|
+
# Example 2: Simple retry configuration
|
102
|
+
puts 'Example 2: Simple retry configuration'
|
103
|
+
puts '-' * 50
|
104
|
+
|
105
|
+
simple_agent = CustomRetryAgent.new
|
106
|
+
puts "Using simple retry (max 2 attempts)..."
|
107
|
+
|
108
|
+
# Reset the tool's call count
|
109
|
+
simple_agent.tools.first.instance_variable_set(:@call_count, 0)
|
110
|
+
|
111
|
+
result = simple_agent.run('Perform critical operation')
|
112
|
+
puts "\nResult: #{result.final_answer}"
|
113
|
+
|
114
|
+
puts "\n" + '=' * 50 + "\n"
|
115
|
+
|
116
|
+
# Example 3: Rate limiting with retry
|
117
|
+
puts 'Example 3: Handling rate limits'
|
118
|
+
puts '-' * 50
|
119
|
+
|
120
|
+
agent = RetryAgent.new
|
121
|
+
puts "Making rapid API calls (will hit rate limit)..."
|
122
|
+
|
123
|
+
queries = ['Query 1', 'Query 2', 'Query 3']
|
124
|
+
queries.each do |query|
|
125
|
+
puts "\nExecuting: #{query}"
|
126
|
+
result = agent.run("Use the rate limited API for: #{query}")
|
127
|
+
puts "Result: #{result.final_answer[0..50]}..."
|
128
|
+
sleep(0.5) # Small delay between queries
|
129
|
+
end
|
130
|
+
|
131
|
+
puts "\n" + '=' * 50 + "\n"
|
132
|
+
|
133
|
+
# Example 4: No retry configuration
|
134
|
+
puts 'Example 4: Agent without retry (for comparison)'
|
135
|
+
puts '-' * 50
|
136
|
+
|
137
|
+
class NoRetryAgent < Soka::Agent
|
138
|
+
tool FlakyApiTool
|
139
|
+
end
|
140
|
+
|
141
|
+
no_retry_agent = NoRetryAgent.new
|
142
|
+
puts "Calling flaky API without retry..."
|
143
|
+
|
144
|
+
begin
|
145
|
+
# Reset call count
|
146
|
+
no_retry_agent.tools.first.instance_variable_set(:@call_count, 0)
|
147
|
+
result = no_retry_agent.run('Try once without retry')
|
148
|
+
puts "Success on first try!"
|
149
|
+
rescue => e
|
150
|
+
puts "Failed immediately: #{e.message}"
|
151
|
+
end
|
152
|
+
|
153
|
+
puts "\n=== Retry Benefits ==="
|
154
|
+
puts "1. Handles transient network failures"
|
155
|
+
puts "2. Deals with rate limiting gracefully"
|
156
|
+
puts "3. Improves reliability of AI agents"
|
157
|
+
puts "4. Configurable backoff strategies"
|
158
|
+
puts "5. Selective retry based on error types"
|
159
|
+
|
160
|
+
puts "\n=== Retry Strategies ==="
|
161
|
+
puts "1. Fixed delay: Wait same time between retries"
|
162
|
+
puts "2. Exponential backoff: Increasing delays (1s, 2s, 4s...)"
|
163
|
+
puts "3. Linear backoff: Linear increase (1s, 2s, 3s...)"
|
164
|
+
puts "4. Custom logic: Define your own retry behavior"
|
@@ -0,0 +1,180 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'soka'
|
6
|
+
require 'dotenv/load'
|
7
|
+
|
8
|
+
# Configure Soka
|
9
|
+
Soka.setup do |config|
|
10
|
+
config.ai do |ai|
|
11
|
+
ai.provider = :gemini
|
12
|
+
ai.model = 'gemini-2.5-flash-lite'
|
13
|
+
ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Development-only debugging tool
|
18
|
+
class DebugTool < Soka::AgentTool
|
19
|
+
desc 'Debug internal state (dev only)'
|
20
|
+
|
21
|
+
def call
|
22
|
+
"Debug info: Memory size=#{@agent&.memory&.messages&.size || 0}, Time=#{Time.now}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Production monitoring tool
|
27
|
+
class MonitoringTool < Soka::AgentTool
|
28
|
+
desc 'Monitor system metrics'
|
29
|
+
|
30
|
+
def call
|
31
|
+
"System metrics: CPU=#{rand(10..90)}%, Memory=#{rand(30..70)}%, Uptime=#{rand(1..100)}h"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Admin-only tool
|
36
|
+
class AdminTool < Soka::AgentTool
|
37
|
+
desc 'Perform admin operations'
|
38
|
+
|
39
|
+
params do
|
40
|
+
requires :operation, String, desc: 'Admin operation to perform'
|
41
|
+
end
|
42
|
+
|
43
|
+
def call(operation:)
|
44
|
+
"Admin operation '#{operation}' completed successfully"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Feature flag tool
|
49
|
+
class FeatureFlagTool < Soka::AgentTool
|
50
|
+
desc 'Check feature flags'
|
51
|
+
|
52
|
+
params do
|
53
|
+
requires :flag, String, desc: 'Feature flag name'
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(flag:)
|
57
|
+
# Simulate feature flag service
|
58
|
+
enabled = %w[new_ui dark_mode beta_features].include?(flag)
|
59
|
+
"Feature '#{flag}' is #{enabled ? 'enabled' : 'disabled'}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Environment-aware agent
|
64
|
+
class ConditionalAgent < Soka::Agent
|
65
|
+
# Always available tools
|
66
|
+
tool FeatureFlagTool
|
67
|
+
|
68
|
+
# Conditional tools based on environment
|
69
|
+
tool DebugTool, if: -> { ENV['ENVIRONMENT'] == 'development' }
|
70
|
+
tool MonitoringTool, if: -> { %w[staging production].include?(ENV['ENVIRONMENT']) }
|
71
|
+
tool AdminTool, if: -> { ENV['USER_ROLE'] == 'admin' }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Dynamic condition agent
|
75
|
+
class DynamicConditionAgent < Soka::Agent
|
76
|
+
# Load tool based on time of day
|
77
|
+
tool DebugTool, if: -> { Time.now.hour.between?(9, 17) } # Business hours only
|
78
|
+
|
79
|
+
# Load based on environment variable
|
80
|
+
tool MonitoringTool, if: -> { ENV['ENABLE_MONITORING'] == 'true' }
|
81
|
+
|
82
|
+
# Multiple conditions
|
83
|
+
tool AdminTool, if: -> { ENV['USER_ROLE'] == 'admin' && ENV['SECURE_MODE'] == 'true' }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Main program
|
87
|
+
puts '=== Soka Conditional Tool Example ==='
|
88
|
+
puts "Demonstrating conditional tool loading\n\n"
|
89
|
+
|
90
|
+
# Example 1: Environment-based tools
|
91
|
+
puts 'Example 1: Environment-based tool loading'
|
92
|
+
puts '-' * 50
|
93
|
+
|
94
|
+
environments = %w[development staging production]
|
95
|
+
|
96
|
+
environments.each do |env|
|
97
|
+
ENV['ENVIRONMENT'] = env
|
98
|
+
agent = ConditionalAgent.new
|
99
|
+
|
100
|
+
puts "\nEnvironment: #{env}"
|
101
|
+
puts "Loaded tools: #{agent.tools.map { |t| t.class.name }.join(', ')}"
|
102
|
+
|
103
|
+
# Try to use monitoring
|
104
|
+
result = agent.run('Check system monitoring metrics')
|
105
|
+
puts "Result: #{result.final_answer}"
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "\n" + '=' * 50 + "\n"
|
109
|
+
|
110
|
+
# Example 2: Role-based tools
|
111
|
+
puts 'Example 2: Role-based tool access'
|
112
|
+
puts '-' * 50
|
113
|
+
|
114
|
+
roles = %w[user admin guest]
|
115
|
+
|
116
|
+
roles.each do |role|
|
117
|
+
ENV['USER_ROLE'] = role
|
118
|
+
ENV['ENVIRONMENT'] = 'production'
|
119
|
+
agent = ConditionalAgent.new
|
120
|
+
|
121
|
+
puts "\nUser role: #{role}"
|
122
|
+
puts "Available tools: #{agent.tools.map { |t| t.class.name }.join(', ')}"
|
123
|
+
|
124
|
+
if role == 'admin'
|
125
|
+
result = agent.run('Perform admin operation: restart_service')
|
126
|
+
puts "Admin result: #{result.final_answer}"
|
127
|
+
else
|
128
|
+
puts "Admin tools not available for #{role} role"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
puts "\n" + '=' * 50 + "\n"
|
133
|
+
|
134
|
+
# Example 3: Dynamic conditions
|
135
|
+
puts 'Example 3: Dynamic condition checking'
|
136
|
+
puts '-' * 50
|
137
|
+
|
138
|
+
# Set up dynamic conditions
|
139
|
+
ENV['ENABLE_MONITORING'] = 'true'
|
140
|
+
ENV['USER_ROLE'] = 'admin'
|
141
|
+
ENV['SECURE_MODE'] = 'true'
|
142
|
+
|
143
|
+
dynamic_agent = DynamicConditionAgent.new
|
144
|
+
puts "Current time: #{Time.now}"
|
145
|
+
puts "Business hours tool available: #{Time.now.hour.between?(9, 17)}"
|
146
|
+
puts "Monitoring enabled: #{ENV['ENABLE_MONITORING']}"
|
147
|
+
puts "Admin with secure mode: #{ENV['USER_ROLE'] == 'admin' && ENV['SECURE_MODE'] == 'true'}"
|
148
|
+
puts "\nLoaded tools: #{dynamic_agent.tools.map { |t| t.class.name }.join(', ')}"
|
149
|
+
|
150
|
+
result = dynamic_agent.run('List all available debugging options')
|
151
|
+
puts "\nResult: #{result.final_answer}"
|
152
|
+
|
153
|
+
puts "\n" + '=' * 50 + "\n"
|
154
|
+
|
155
|
+
# Example 4: Feature flags
|
156
|
+
puts 'Example 4: Feature flag integration'
|
157
|
+
puts '-' * 50
|
158
|
+
|
159
|
+
agent = ConditionalAgent.new
|
160
|
+
features = %w[new_ui dark_mode legacy_support beta_features]
|
161
|
+
|
162
|
+
puts "Checking feature flags:"
|
163
|
+
features.each do |feature|
|
164
|
+
result = agent.run("Check if feature flag '#{feature}' is enabled")
|
165
|
+
puts "- #{feature}: #{result.final_answer.include?('enabled') ? 'ā' : 'ā'}"
|
166
|
+
end
|
167
|
+
|
168
|
+
puts "\n=== Conditional Tool Benefits ==="
|
169
|
+
puts "1. Environment-specific functionality"
|
170
|
+
puts "2. Role-based access control"
|
171
|
+
puts "3. Feature flag integration"
|
172
|
+
puts "4. Resource optimization"
|
173
|
+
puts "5. Security through tool isolation"
|
174
|
+
|
175
|
+
puts "\n=== Use Cases ==="
|
176
|
+
puts "1. Debug tools only in development"
|
177
|
+
puts "2. Admin tools for privileged users"
|
178
|
+
puts "3. Monitoring in production only"
|
179
|
+
puts "4. Beta features behind flags"
|
180
|
+
puts "5. Time-based tool availability"
|
@@ -0,0 +1,112 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'soka'
|
6
|
+
require 'dotenv/load'
|
7
|
+
|
8
|
+
# Configure Soka with all providers
|
9
|
+
Soka.setup do |config|
|
10
|
+
# Default configuration can be overridden per agent
|
11
|
+
config.ai do |ai|
|
12
|
+
ai.provider = :gemini
|
13
|
+
ai.model = 'gemini-2.5-flash-lite'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Simple analysis tool
|
18
|
+
class AnalysisTool < Soka::AgentTool
|
19
|
+
desc 'Analyze text or data'
|
20
|
+
|
21
|
+
params do
|
22
|
+
requires :content, String, desc: 'Content to analyze'
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(content:)
|
26
|
+
word_count = content.split.size
|
27
|
+
char_count = content.length
|
28
|
+
"Analysis: #{word_count} words, #{char_count} characters"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Gemini Agent
|
33
|
+
class GeminiAgent < Soka::Agent
|
34
|
+
provider :gemini
|
35
|
+
model 'gemini-2.5-flash-lite'
|
36
|
+
api_key ENV.fetch('GEMINI_API_KEY', nil)
|
37
|
+
|
38
|
+
tool AnalysisTool
|
39
|
+
end
|
40
|
+
|
41
|
+
# OpenAI Agent
|
42
|
+
class OpenAIAgent < Soka::Agent
|
43
|
+
provider :openai
|
44
|
+
model 'gpt-4o-mini'
|
45
|
+
api_key ENV.fetch('OPENAI_API_KEY', nil)
|
46
|
+
|
47
|
+
tool AnalysisTool
|
48
|
+
end
|
49
|
+
|
50
|
+
# Anthropic Agent
|
51
|
+
class AnthropicAgent < Soka::Agent
|
52
|
+
provider :anthropic
|
53
|
+
model 'claude-3-5-haiku-latest'
|
54
|
+
api_key ENV.fetch('ANTHROPIC_API_KEY', nil)
|
55
|
+
|
56
|
+
tool AnalysisTool
|
57
|
+
end
|
58
|
+
|
59
|
+
# Main program
|
60
|
+
puts '=== Soka Multi-Provider Example ==='
|
61
|
+
puts "Demonstrating multiple LLM providers\n\n"
|
62
|
+
|
63
|
+
# Example 1: Different providers for same task
|
64
|
+
puts 'Example 1: Same task with different providers'
|
65
|
+
puts '-' * 50
|
66
|
+
|
67
|
+
task = 'Analyze this text: "Soka is a Ruby framework for building AI agents"'
|
68
|
+
|
69
|
+
# Try with Gemini
|
70
|
+
if ENV['GEMINI_API_KEY']
|
71
|
+
puts "\nš· Using Gemini:"
|
72
|
+
gemini_agent = GeminiAgent.new
|
73
|
+
result = gemini_agent.run(task)
|
74
|
+
puts "Response: #{result.final_answer}"
|
75
|
+
puts "Provider: Gemini (#{gemini_agent.llm.model})"
|
76
|
+
else
|
77
|
+
puts "\nš· Gemini: Skipped (no API key)"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Try with OpenAI
|
81
|
+
if ENV['OPENAI_API_KEY']
|
82
|
+
puts "\nš§ Using OpenAI:"
|
83
|
+
openai_agent = OpenAIAgent.new
|
84
|
+
result = openai_agent.run(task)
|
85
|
+
puts "Response: #{result.final_answer}"
|
86
|
+
puts "Provider: OpenAI (#{openai_agent.llm.model})"
|
87
|
+
else
|
88
|
+
puts "\nš§ OpenAI: Skipped (no API key)"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Try with Anthropic
|
92
|
+
if ENV['ANTHROPIC_API_KEY']
|
93
|
+
puts "\nš¶ Using Anthropic:"
|
94
|
+
anthropic_agent = AnthropicAgent.new
|
95
|
+
result = anthropic_agent.run(task)
|
96
|
+
puts "Response: #{result.final_answer}"
|
97
|
+
puts "Provider: Anthropic (#{anthropic_agent.llm.model})"
|
98
|
+
else
|
99
|
+
puts "\nš¶ Anthropic: Skipped (no API key)"
|
100
|
+
end
|
101
|
+
|
102
|
+
puts "\n=== Multi-Provider Benefits ==="
|
103
|
+
puts '1. Flexibility to choose best model for each task'
|
104
|
+
puts '2. Cost optimization (use cheaper models when appropriate)'
|
105
|
+
puts '3. Fallback options for reliability'
|
106
|
+
puts '4. Access to provider-specific features'
|
107
|
+
puts '5. Compare outputs across different models'
|
108
|
+
|
109
|
+
puts "\n=== Provider Comparison ==="
|
110
|
+
puts 'Gemini: Fast, cost-effective, good for general tasks'
|
111
|
+
puts 'OpenAI: Powerful GPT models, great for complex reasoning'
|
112
|
+
puts 'Anthropic: Claude models, excellent for analysis and writing'
|
data/lib/soka/agent.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
# Base class for AI agents that use ReAct pattern
|
5
|
+
class Agent
|
6
|
+
include Agents::RetryHandler
|
7
|
+
include Agents::ToolBuilder
|
8
|
+
include Agents::HookManager
|
9
|
+
include Agents::DSLMethods
|
10
|
+
include Agents::LLMBuilder
|
11
|
+
|
12
|
+
attr_reader :llm, :tools, :memory, :thoughts_memory, :engine
|
13
|
+
|
14
|
+
# Initialize a new Agent instance
|
15
|
+
# @param memory [Memory, Array, nil] The memory instance to use (defaults to new Memory)
|
16
|
+
# Can be a Memory instance or an Array of message hashes
|
17
|
+
# @param engine [Class] The engine class to use (defaults to Engine::React)
|
18
|
+
# @param options [Hash] Configuration options
|
19
|
+
# @option options [Integer] :max_iterations Maximum iterations for reasoning
|
20
|
+
# @option options [Integer] :timeout Timeout in seconds for operations
|
21
|
+
# @option options [Symbol] :provider LLM provider override
|
22
|
+
# @option options [String] :model LLM model override
|
23
|
+
# @option options [String] :api_key LLM API key override
|
24
|
+
def initialize(memory: nil, engine: Engines::React, **options)
|
25
|
+
@memory = initialize_memory(memory)
|
26
|
+
@thoughts_memory = ThoughtsMemory.new
|
27
|
+
@engine = engine
|
28
|
+
|
29
|
+
# Initialize components
|
30
|
+
@llm = build_llm(options)
|
31
|
+
@tools = build_tools
|
32
|
+
|
33
|
+
# Apply configuration with clear defaults
|
34
|
+
apply_configuration(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Apply configuration options with defaults
|
38
|
+
# @param options [Hash] Configuration options
|
39
|
+
def apply_configuration(options)
|
40
|
+
@max_iterations = options.fetch(:max_iterations) { self.class._max_iterations || 10 }
|
41
|
+
@timeout = options.fetch(:timeout) { self.class._timeout || 30 }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Run the agent with the given input
|
45
|
+
# @param input [String] The input query or task
|
46
|
+
# @yield [event] Optional block to handle events during execution
|
47
|
+
# @return [Result] The result of the agent's reasoning
|
48
|
+
def run(input, &)
|
49
|
+
validate_input(input)
|
50
|
+
execute_reasoning(input, &)
|
51
|
+
rescue ArgumentError
|
52
|
+
raise # Re-raise ArgumentError without handling
|
53
|
+
rescue StandardError => e
|
54
|
+
handle_error(e, input)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Initialize memory from various input formats
|
60
|
+
# @param memory [Memory, Array, nil] The memory input
|
61
|
+
# @return [Memory] The initialized memory instance
|
62
|
+
def initialize_memory(memory)
|
63
|
+
case memory
|
64
|
+
when Memory
|
65
|
+
memory
|
66
|
+
when Array
|
67
|
+
Memory.new(memory)
|
68
|
+
when nil
|
69
|
+
Memory.new
|
70
|
+
else
|
71
|
+
raise ArgumentError, "Invalid memory type: #{memory.class}. Expected Memory, Array, or nil"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Validate the input is not empty
|
76
|
+
# @param input [String] The input to validate
|
77
|
+
# @raise [ArgumentError] If input is empty
|
78
|
+
def validate_input(input)
|
79
|
+
raise ArgumentError, 'Input cannot be empty' if input.to_s.strip.empty?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Execute the reasoning process with hooks
|
83
|
+
# @param input [String] The input query
|
84
|
+
# @yield [event] Optional block to handle events
|
85
|
+
# @return [Result] The reasoning result
|
86
|
+
def execute_reasoning(input, &)
|
87
|
+
run_hooks(:before_action, input)
|
88
|
+
|
89
|
+
engine_result = perform_reasoning(input, &)
|
90
|
+
result = convert_engine_result(engine_result)
|
91
|
+
|
92
|
+
finalize_result(input, result)
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
# Perform the actual reasoning using the engine
|
97
|
+
# @param input [String] The input query
|
98
|
+
# @yield [event] Optional block to handle events
|
99
|
+
# @return [EngineResult] The raw engine result
|
100
|
+
def perform_reasoning(input, &)
|
101
|
+
engine_instance = @engine.new(self, @llm, @tools, @max_iterations)
|
102
|
+
with_retry { engine_instance.reason(input, &) }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Finalize the result by updating memories and running hooks
|
106
|
+
# @param input [String] The original input
|
107
|
+
# @param result [Result] The result to finalize
|
108
|
+
def finalize_result(input, result)
|
109
|
+
update_memories(input, result)
|
110
|
+
run_hooks(:after_action, result)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Handle errors during execution
|
114
|
+
# @param error [StandardError] The error that occurred
|
115
|
+
# @param input [String] The original input
|
116
|
+
# @return [Result] An error result
|
117
|
+
# @raise [StandardError] Re-raises if on_error hook returns :stop
|
118
|
+
def handle_error(error, input)
|
119
|
+
error_action = run_hooks(:on_error, error, input)
|
120
|
+
raise error if error_action == :stop
|
121
|
+
|
122
|
+
build_error_result(input, error)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Tool building methods are in ToolBuilder module
|
126
|
+
# Retry handling methods are in RetryHandler module
|
127
|
+
# LLM building methods are in LLMBuilder module
|
128
|
+
# Hook management methods are in HookManager module
|
129
|
+
end
|
130
|
+
end
|