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
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
# Base class for agent tools with parameter validation
|
5
|
+
class AgentTool
|
6
|
+
include AgentTools::ParamsValidator
|
7
|
+
|
8
|
+
# Handles parameter definitions for tools
|
9
|
+
class ParamsDefinition
|
10
|
+
attr_reader :required_params, :optional_params, :validations
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@required_params = {}
|
14
|
+
@optional_params = {}
|
15
|
+
@validations = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def requires(name, type, desc: nil, **options)
|
19
|
+
param_config = {
|
20
|
+
type: type,
|
21
|
+
desc: desc
|
22
|
+
}
|
23
|
+
param_config[:default] = options[:default] if options.key?(:default)
|
24
|
+
@required_params[name] = param_config
|
25
|
+
end
|
26
|
+
|
27
|
+
def optional(name, type, desc: nil, **options)
|
28
|
+
param_config = {
|
29
|
+
type: type,
|
30
|
+
desc: desc
|
31
|
+
}
|
32
|
+
param_config[:default] = options[:default] if options.key?(:default)
|
33
|
+
@optional_params[name] = param_config
|
34
|
+
end
|
35
|
+
|
36
|
+
def validates(name, **rules)
|
37
|
+
@validations[name] = rules
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
attr_reader :description, :params_definition
|
43
|
+
|
44
|
+
def desc(description)
|
45
|
+
@description = description
|
46
|
+
end
|
47
|
+
|
48
|
+
def params(&)
|
49
|
+
@params_definition = ParamsDefinition.new
|
50
|
+
@params_definition.instance_eval(&) if block_given?
|
51
|
+
end
|
52
|
+
|
53
|
+
def tool_name
|
54
|
+
return 'anonymous' if name.nil?
|
55
|
+
|
56
|
+
name.split('::').last.gsub(/Tool$/, '').downcase
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h
|
60
|
+
{
|
61
|
+
name: tool_name,
|
62
|
+
description: description || 'No description provided',
|
63
|
+
parameters: parameters_schema
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def parameters_schema
|
68
|
+
schema = base_schema
|
69
|
+
add_parameters_to_schema(schema) if params_definition
|
70
|
+
schema
|
71
|
+
end
|
72
|
+
|
73
|
+
def base_schema
|
74
|
+
{
|
75
|
+
type: 'object',
|
76
|
+
properties: {},
|
77
|
+
required: []
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_parameters_to_schema(schema)
|
82
|
+
add_required_params(schema)
|
83
|
+
add_optional_params(schema)
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_required_params(schema)
|
87
|
+
params_definition.required_params.each do |name, config|
|
88
|
+
schema[:properties][name.to_s] = build_property_schema(name, config)
|
89
|
+
schema[:required] << name.to_s
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_optional_params(schema)
|
94
|
+
params_definition.optional_params.each do |name, config|
|
95
|
+
schema[:properties][name.to_s] = build_property_schema(name, config)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def build_property_schema(_name, config)
|
102
|
+
schema = {
|
103
|
+
type: type_to_json_type(config[:type]),
|
104
|
+
description: config[:desc]
|
105
|
+
}
|
106
|
+
|
107
|
+
schema[:default] = config[:default] if config.key?(:default)
|
108
|
+
schema
|
109
|
+
end
|
110
|
+
|
111
|
+
def type_to_json_type(ruby_type)
|
112
|
+
type_mapping[ruby_type.to_s] || 'string'
|
113
|
+
end
|
114
|
+
|
115
|
+
def type_mapping
|
116
|
+
@type_mapping ||= build_type_mapping
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_type_mapping
|
120
|
+
{
|
121
|
+
'String' => 'string',
|
122
|
+
'Integer' => 'integer', 'Fixnum' => 'integer', 'Bignum' => 'integer',
|
123
|
+
'Float' => 'number', 'Numeric' => 'number',
|
124
|
+
'TrueClass' => 'boolean', 'FalseClass' => 'boolean', 'Boolean' => 'boolean',
|
125
|
+
'Array' => 'array',
|
126
|
+
'Hash' => 'object'
|
127
|
+
}.freeze
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def call(**params)
|
132
|
+
raise NotImplementedError, "#{self.class} must implement #call method"
|
133
|
+
end
|
134
|
+
|
135
|
+
def execute(**params)
|
136
|
+
validate_params!(params)
|
137
|
+
call(**params)
|
138
|
+
rescue ToolError
|
139
|
+
raise # Re-raise ToolError without wrapping
|
140
|
+
rescue StandardError => e
|
141
|
+
raise ToolError, "Error executing #{self.class.tool_name}: #{e.message}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Validation methods are in ParamsValidator module
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module AgentTools
|
5
|
+
# Module for parameter validation
|
6
|
+
module ParamsValidator
|
7
|
+
private
|
8
|
+
|
9
|
+
def validate_params!(params)
|
10
|
+
return unless self.class.params_definition
|
11
|
+
|
12
|
+
definition = self.class.params_definition
|
13
|
+
validate_required_params!(params, definition)
|
14
|
+
validate_optional_params!(params, definition)
|
15
|
+
run_validations!(params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_required_params!(params, definition)
|
19
|
+
definition.required_params.each do |name, config|
|
20
|
+
process_required_param(params, name, config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_required_param(params, name, config)
|
25
|
+
unless params.key?(name)
|
26
|
+
raise ToolError, "Missing required parameter: #{name}" unless config.key?(:default)
|
27
|
+
|
28
|
+
params[name] = config[:default]
|
29
|
+
end
|
30
|
+
validate_type!(name, params[name], config[:type])
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_optional_params!(params, definition)
|
34
|
+
definition.optional_params.each do |name, config|
|
35
|
+
process_optional_param(params, name, config)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def process_optional_param(params, name, config)
|
40
|
+
if params.key?(name)
|
41
|
+
validate_type!(name, params[name], config[:type])
|
42
|
+
elsif config.key?(:default)
|
43
|
+
params[name] = config[:default]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_type!(name, value, expected_type)
|
48
|
+
return if value.nil?
|
49
|
+
return if value.is_a?(expected_type)
|
50
|
+
|
51
|
+
raise ToolError, "Parameter #{name} must be a #{expected_type}, got #{value.class}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_validations!(params)
|
55
|
+
return unless self.class.params_definition
|
56
|
+
|
57
|
+
self.class.params_definition.validations.each do |param_name, rules|
|
58
|
+
# Only validate if parameter was provided
|
59
|
+
next unless params.key?(param_name)
|
60
|
+
|
61
|
+
validate_param_rules(param_name, params[param_name], rules)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_param_rules(param_name, value, rules)
|
66
|
+
# Skip validation if parameter wasn't provided and has no presence validation
|
67
|
+
return if value.nil? && !rules[:presence]
|
68
|
+
|
69
|
+
rules.each do |rule, rule_value|
|
70
|
+
apply_validation_rule(param_name, value, rule, rule_value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def apply_validation_rule(param_name, value, rule, rule_value)
|
75
|
+
case rule
|
76
|
+
when :presence
|
77
|
+
validate_presence!(param_name, value, rule_value)
|
78
|
+
when :length
|
79
|
+
validate_length!(param_name, value, rule_value)
|
80
|
+
when :inclusion
|
81
|
+
validate_inclusion!(param_name, value, rule_value)
|
82
|
+
when :format
|
83
|
+
validate_format!(param_name, value, rule_value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_presence!(param_name, value, rule_value)
|
88
|
+
return unless rule_value && (value.nil? || value.to_s.empty?)
|
89
|
+
|
90
|
+
raise ToolError, "Parameter #{param_name} can't be blank"
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_length!(name, value, constraints)
|
94
|
+
return unless value.respond_to?(:length)
|
95
|
+
|
96
|
+
check_minimum_length!(name, value, constraints[:minimum])
|
97
|
+
check_maximum_length!(name, value, constraints[:maximum])
|
98
|
+
end
|
99
|
+
|
100
|
+
def check_minimum_length!(name, value, minimum)
|
101
|
+
return unless minimum && value.length < minimum
|
102
|
+
|
103
|
+
raise ToolError,
|
104
|
+
"Parameter #{name} is too short (minimum is #{minimum} characters)"
|
105
|
+
end
|
106
|
+
|
107
|
+
def check_maximum_length!(name, value, maximum)
|
108
|
+
return unless maximum && value.length > maximum
|
109
|
+
|
110
|
+
raise ToolError,
|
111
|
+
"Parameter #{name} is too long (maximum is #{maximum} characters)"
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_inclusion!(name, value, constraints)
|
115
|
+
allowed_values = constraints[:in]
|
116
|
+
allow_nil = constraints[:allow_nil]
|
117
|
+
|
118
|
+
return if value.nil? && allow_nil
|
119
|
+
return if allowed_values.include?(value)
|
120
|
+
|
121
|
+
values_string = if allowed_values.is_a?(Range)
|
122
|
+
"#{allowed_values.min}..#{allowed_values.max}"
|
123
|
+
else
|
124
|
+
allowed_values.join(', ')
|
125
|
+
end
|
126
|
+
raise ToolError, "Parameter #{name} must be one of: #{values_string}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def validate_format!(name, value, constraints)
|
130
|
+
return unless value.is_a?(String)
|
131
|
+
|
132
|
+
pattern = constraints[:with]
|
133
|
+
return if value.match?(pattern)
|
134
|
+
|
135
|
+
raise ToolError, "Parameter #{name} has invalid format"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module Agents
|
5
|
+
# Module for DSL methods
|
6
|
+
module DSLMethods
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Class methods for DSL
|
12
|
+
module ClassMethods
|
13
|
+
attr_accessor :_provider, :_model, :_api_key, :_max_iterations, :_timeout, :_tools, :_retry_config, :_hooks
|
14
|
+
|
15
|
+
def inherited(subclass)
|
16
|
+
super
|
17
|
+
subclass._tools = []
|
18
|
+
subclass._hooks = { before_action: [], after_action: [], on_error: [] }
|
19
|
+
subclass._retry_config = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define provider for the agent
|
23
|
+
# @param provider [Symbol] The LLM provider (:gemini, :openai, :anthropic)
|
24
|
+
def provider(provider)
|
25
|
+
@_provider = provider
|
26
|
+
end
|
27
|
+
|
28
|
+
# Define model for the agent
|
29
|
+
# @param model [String] The model name (e.g., 'gemini-1.5-pro')
|
30
|
+
def model(model)
|
31
|
+
@_model = model
|
32
|
+
end
|
33
|
+
|
34
|
+
# Define API key for the agent
|
35
|
+
# @param key [String] The API key
|
36
|
+
def api_key(key)
|
37
|
+
@_api_key = key
|
38
|
+
end
|
39
|
+
|
40
|
+
# Define maximum iterations for the agent
|
41
|
+
# @param num [Integer] The maximum number of iterations
|
42
|
+
def max_iterations(num)
|
43
|
+
@_max_iterations = num
|
44
|
+
end
|
45
|
+
|
46
|
+
# Define timeout for the agent
|
47
|
+
# @param duration [Integer] The timeout duration in seconds
|
48
|
+
def timeout(duration)
|
49
|
+
@_timeout = duration
|
50
|
+
end
|
51
|
+
|
52
|
+
# Register a tool for the agent
|
53
|
+
# @param tool_class_or_name [Class, Symbol, String] The tool class or method name
|
54
|
+
# @param description_or_options [String, Hash, nil] Description (for function tools) or options
|
55
|
+
# @param options [Hash] Additional options (if description provided)
|
56
|
+
def tool(tool_class_or_name, description_or_options = nil, options = {})
|
57
|
+
if tool_class_or_name.is_a?(Symbol) || tool_class_or_name.is_a?(String)
|
58
|
+
# Function tool - expects description and options
|
59
|
+
add_function_tool(tool_class_or_name, description_or_options, options)
|
60
|
+
else
|
61
|
+
# Class tool - second parameter is options
|
62
|
+
opts = description_or_options.is_a?(Hash) ? description_or_options : options
|
63
|
+
add_class_tool(tool_class_or_name, opts)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Register multiple tools at once
|
68
|
+
# @param tool_classes [Array<Class>] The tool classes to register
|
69
|
+
def tools(*tool_classes)
|
70
|
+
tool_classes.each { |tool_class| tool(tool_class) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Configure retry behavior
|
74
|
+
# @yield Configuration block
|
75
|
+
def retry_config(&)
|
76
|
+
config = Agents::RetryConfig.new
|
77
|
+
config.instance_eval(&)
|
78
|
+
@_retry_config = config.to_h
|
79
|
+
end
|
80
|
+
|
81
|
+
# Register before_action hook
|
82
|
+
# @param method_name [Symbol] The method to call before action
|
83
|
+
def before_action(method_name)
|
84
|
+
@_hooks[:before_action] << method_name
|
85
|
+
end
|
86
|
+
|
87
|
+
# Register after_action hook
|
88
|
+
# @param method_name [Symbol] The method to call after action
|
89
|
+
def after_action(method_name)
|
90
|
+
@_hooks[:after_action] << method_name
|
91
|
+
end
|
92
|
+
|
93
|
+
# Register on_error hook
|
94
|
+
# @param method_name [Symbol] The method to call on error
|
95
|
+
def on_error(method_name)
|
96
|
+
@_hooks[:on_error] << method_name
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def add_function_tool(name, description, options)
|
102
|
+
@_tools << {
|
103
|
+
type: :function,
|
104
|
+
name: name,
|
105
|
+
description: description,
|
106
|
+
options: options
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_class_tool(tool_class, options)
|
111
|
+
condition = options[:if]
|
112
|
+
return unless condition.nil? || (condition.respond_to?(:call) ? condition.call : condition)
|
113
|
+
|
114
|
+
@_tools << { type: :class, class: tool_class, options: options }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Configuration for retry behavior
|
120
|
+
class RetryConfig
|
121
|
+
attr_accessor :max_retries, :backoff_strategy, :retry_on
|
122
|
+
|
123
|
+
def initialize
|
124
|
+
@max_retries = 3
|
125
|
+
@backoff_strategy = :exponential
|
126
|
+
@retry_on = []
|
127
|
+
end
|
128
|
+
|
129
|
+
# Convert to hash
|
130
|
+
# @return [Hash] The configuration as a hash
|
131
|
+
def to_h
|
132
|
+
{
|
133
|
+
max_retries: max_retries,
|
134
|
+
backoff_strategy: backoff_strategy,
|
135
|
+
retry_on: retry_on
|
136
|
+
}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module Agents
|
5
|
+
# Module for managing hooks and result processing
|
6
|
+
module HookManager
|
7
|
+
private
|
8
|
+
|
9
|
+
# Run registered hooks for the given type
|
10
|
+
# @param hook_type [Symbol] The type of hook (:before_action, :after_action, :on_error)
|
11
|
+
# @param args [Array] Arguments to pass to the hook methods
|
12
|
+
# @return [Object, nil] The result from on_error hooks, or nil
|
13
|
+
def run_hooks(hook_type, *)
|
14
|
+
return unless self.class._hooks && self.class._hooks[hook_type]
|
15
|
+
|
16
|
+
self.class._hooks[hook_type].each do |method_name|
|
17
|
+
if respond_to?(method_name, true)
|
18
|
+
result = send(method_name, *)
|
19
|
+
return result if hook_type == :on_error && result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert engine result to final Result object
|
27
|
+
# @param engine_result [Struct] The raw engine result
|
28
|
+
# @return [Result] The converted result
|
29
|
+
def convert_engine_result(engine_result)
|
30
|
+
Result.new(
|
31
|
+
input: engine_result.input,
|
32
|
+
thoughts: engine_result.thoughts,
|
33
|
+
final_answer: engine_result.final_answer,
|
34
|
+
status: engine_result.status,
|
35
|
+
error: engine_result.error,
|
36
|
+
confidence_score: engine_result.confidence_score
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Update conversation and thoughts memories
|
41
|
+
# @param input [String] The original input
|
42
|
+
# @param result [Result] The result to record
|
43
|
+
def update_memories(input, result)
|
44
|
+
# Update conversation memory
|
45
|
+
@memory.add(role: 'user', content: input)
|
46
|
+
@memory.add(role: 'assistant', content: result.final_answer) if result.final_answer
|
47
|
+
|
48
|
+
# Update thoughts memory
|
49
|
+
@thoughts_memory.add(input, result)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Build an error result object
|
53
|
+
# @param input [String] The original input
|
54
|
+
# @param error [StandardError] The error that occurred
|
55
|
+
# @return [Result] An error result
|
56
|
+
def build_error_result(input, error)
|
57
|
+
Result.new(
|
58
|
+
input: input,
|
59
|
+
thoughts: [],
|
60
|
+
final_answer: nil,
|
61
|
+
status: :failed,
|
62
|
+
error: error.message,
|
63
|
+
confidence_score: 0.0
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module Agents
|
5
|
+
# Module for building LLM instances
|
6
|
+
module LLMBuilder
|
7
|
+
private
|
8
|
+
|
9
|
+
# Build LLM instance with configuration
|
10
|
+
# @param options [Hash] Configuration options
|
11
|
+
# @return [LLM] The LLM instance
|
12
|
+
def build_llm(options)
|
13
|
+
provider = get_config_value(:provider, options)
|
14
|
+
model = get_config_value(:model, options)
|
15
|
+
api_key = get_config_value(:api_key, options)
|
16
|
+
|
17
|
+
LLM.new(provider, model: model, api_key: api_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get configuration value with fallback chain
|
21
|
+
# @param key [Symbol] The configuration key
|
22
|
+
# @param options [Hash] Configuration options
|
23
|
+
# @return [Object] The configuration value
|
24
|
+
def get_config_value(key, options)
|
25
|
+
# Check options first, then class settings, finally global config
|
26
|
+
options[key] ||
|
27
|
+
self.class.send("_#{key}") ||
|
28
|
+
Soka.configuration.ai.send(key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module Agents
|
5
|
+
# Module for handling retry logic
|
6
|
+
module RetryHandler
|
7
|
+
private
|
8
|
+
|
9
|
+
# Execute a block with retry logic
|
10
|
+
# @yield The block to execute with retries
|
11
|
+
# @return [Object] The result of the block
|
12
|
+
def with_retry(&)
|
13
|
+
config = extract_retry_config
|
14
|
+
execute_with_retries(config, &)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Extract retry configuration with defaults
|
18
|
+
# @return [Hash] The retry configuration
|
19
|
+
def extract_retry_config
|
20
|
+
config = self.class._retry_config
|
21
|
+
{
|
22
|
+
max_retries: config[:max_retries] || 3,
|
23
|
+
backoff_strategy: config[:backoff_strategy] || :exponential,
|
24
|
+
retry_on: config[:retry_on] || []
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Execute with retry logic
|
29
|
+
# @param config [Hash] Retry configuration
|
30
|
+
# @yield The block to execute
|
31
|
+
# @return [Object] The result of the block
|
32
|
+
def execute_with_retries(config, &block)
|
33
|
+
retries = 0
|
34
|
+
begin
|
35
|
+
block.call
|
36
|
+
rescue StandardError => e
|
37
|
+
raise e unless should_retry?(e, config[:retry_on]) && retries < config[:max_retries]
|
38
|
+
|
39
|
+
retries += 1
|
40
|
+
sleep(calculate_backoff(retries, config[:backoff_strategy]))
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if error should trigger retry
|
46
|
+
# @param error [StandardError] The error to check
|
47
|
+
# @param retry_on [Array<Class>] Error classes to retry on
|
48
|
+
# @return [Boolean] True if should retry
|
49
|
+
def should_retry?(error, retry_on)
|
50
|
+
# Never retry ToolError - let it propagate immediately to on_error hook
|
51
|
+
return false if error.is_a?(Soka::ToolError)
|
52
|
+
|
53
|
+
return true if retry_on.empty?
|
54
|
+
|
55
|
+
retry_on.any? { |error_class| error.is_a?(error_class) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Calculate backoff time
|
59
|
+
# @param retries [Integer] Number of retries
|
60
|
+
# @param strategy [Symbol] Backoff strategy
|
61
|
+
# @return [Integer] Sleep time in seconds
|
62
|
+
def calculate_backoff(retries, strategy)
|
63
|
+
case strategy
|
64
|
+
when :exponential
|
65
|
+
2**(retries - 1)
|
66
|
+
when :linear
|
67
|
+
retries
|
68
|
+
else
|
69
|
+
1 # constant or unknown strategy
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Soka
|
4
|
+
module Agents
|
5
|
+
# Module for building tools
|
6
|
+
module ToolBuilder
|
7
|
+
private
|
8
|
+
|
9
|
+
# Build tools from configuration
|
10
|
+
# @return [Array<AgentTool>] Array of tool instances
|
11
|
+
def build_tools
|
12
|
+
tools = build_configured_tools
|
13
|
+
tools.empty? ? build_default_tools : tools
|
14
|
+
end
|
15
|
+
|
16
|
+
# Build tools from class configuration
|
17
|
+
# @return [Array<AgentTool>] Array of configured tools
|
18
|
+
def build_configured_tools
|
19
|
+
self.class._tools.map do |tool_config|
|
20
|
+
create_tool_from_config(tool_config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create tool instance from configuration
|
25
|
+
# @param tool_config [Hash] Tool configuration
|
26
|
+
# @return [AgentTool] The tool instance
|
27
|
+
def create_tool_from_config(tool_config)
|
28
|
+
case tool_config[:type]
|
29
|
+
when :class
|
30
|
+
tool_config[:class].new
|
31
|
+
when :function
|
32
|
+
build_function_tool(tool_config)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Build default tools from global configuration
|
37
|
+
# @return [Array<AgentTool>] Array of default tools
|
38
|
+
def build_default_tools
|
39
|
+
return [] unless Soka.configuration.tools.any?
|
40
|
+
|
41
|
+
Soka.configuration.tools.map(&:new)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Build a function-based tool
|
45
|
+
# @param config [Hash] Tool configuration
|
46
|
+
# @return [AgentTool] The function tool instance
|
47
|
+
def build_function_tool(config)
|
48
|
+
tool_name = config[:name]
|
49
|
+
description = config[:description]
|
50
|
+
|
51
|
+
# Create a dynamic tool class
|
52
|
+
dynamic_tool = create_dynamic_tool_class(tool_name, description)
|
53
|
+
dynamic_tool.new.tap { |tool| tool.instance_variable_set(:@agent, self) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create a dynamic tool class
|
57
|
+
# @param tool_name [Symbol, String] The tool name
|
58
|
+
# @param description [String] The tool description
|
59
|
+
# @return [Class] The dynamic tool class
|
60
|
+
def create_dynamic_tool_class(tool_name, description)
|
61
|
+
Class.new(AgentTool) do
|
62
|
+
desc description
|
63
|
+
|
64
|
+
define_method :call do |**params|
|
65
|
+
# Call the method on the agent instance
|
66
|
+
raise ToolError, "Method #{tool_name} not found on agent" unless @agent.respond_to?(tool_name)
|
67
|
+
|
68
|
+
@agent.send(tool_name, **params)
|
69
|
+
end
|
70
|
+
|
71
|
+
define_singleton_method :tool_name do
|
72
|
+
tool_name.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|