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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +365 -0
  4. data/CHANGELOG.md +31 -0
  5. data/CLAUDE.md +213 -0
  6. data/LICENSE +21 -0
  7. data/README.md +650 -0
  8. data/Rakefile +10 -0
  9. data/examples/1_basic.rb +94 -0
  10. data/examples/2_event_handling.rb +120 -0
  11. data/examples/3_memory.rb +182 -0
  12. data/examples/4_hooks.rb +140 -0
  13. data/examples/5_error_handling.rb +85 -0
  14. data/examples/6_retry.rb +164 -0
  15. data/examples/7_tool_conditional.rb +180 -0
  16. data/examples/8_multi_provider.rb +112 -0
  17. data/lib/soka/agent.rb +130 -0
  18. data/lib/soka/agent_tool.rb +146 -0
  19. data/lib/soka/agent_tools/params_validator.rb +139 -0
  20. data/lib/soka/agents/dsl_methods.rb +140 -0
  21. data/lib/soka/agents/hook_manager.rb +68 -0
  22. data/lib/soka/agents/llm_builder.rb +32 -0
  23. data/lib/soka/agents/retry_handler.rb +74 -0
  24. data/lib/soka/agents/tool_builder.rb +78 -0
  25. data/lib/soka/configuration.rb +60 -0
  26. data/lib/soka/engines/base.rb +67 -0
  27. data/lib/soka/engines/concerns/prompt_template.rb +130 -0
  28. data/lib/soka/engines/concerns/response_processor.rb +103 -0
  29. data/lib/soka/engines/react.rb +136 -0
  30. data/lib/soka/engines/reasoning_context.rb +92 -0
  31. data/lib/soka/llm.rb +85 -0
  32. data/lib/soka/llms/anthropic.rb +124 -0
  33. data/lib/soka/llms/base.rb +114 -0
  34. data/lib/soka/llms/concerns/response_parser.rb +47 -0
  35. data/lib/soka/llms/concerns/streaming_handler.rb +78 -0
  36. data/lib/soka/llms/gemini.rb +106 -0
  37. data/lib/soka/llms/openai.rb +97 -0
  38. data/lib/soka/memory.rb +83 -0
  39. data/lib/soka/result.rb +136 -0
  40. data/lib/soka/test_helpers.rb +162 -0
  41. data/lib/soka/thoughts_memory.rb +112 -0
  42. data/lib/soka/version.rb +5 -0
  43. data/lib/soka.rb +49 -0
  44. data/sig/soka.rbs +4 -0
  45. 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