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,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ # Represents the result of an agent's reasoning process
5
+ class Result
6
+ attr_reader :input, :thoughts, :final_answer, :confidence_score, :status, :error, :execution_time
7
+
8
+ # Initialize a new Result instance
9
+ # @param attributes [Hash] Result attributes
10
+ # @option attributes [String] :input The original input
11
+ # @option attributes [Array] :thoughts Array of thought objects
12
+ # @option attributes [String] :final_answer The final answer
13
+ # @option attributes [Float] :confidence_score Confidence score (0.0-1.0)
14
+ # @option attributes [Symbol] :status The result status
15
+ # @option attributes [String] :error Error message if failed
16
+ # @option attributes [Float] :execution_time Time taken in seconds
17
+ def initialize(attributes = {})
18
+ @input = attributes[:input]
19
+ @thoughts = attributes[:thoughts] || []
20
+ @final_answer = attributes[:final_answer]
21
+ @confidence_score = attributes[:confidence_score] || 0.0
22
+ @status = attributes[:status] || :pending
23
+ @error = attributes[:error]
24
+ @execution_time = attributes[:execution_time]
25
+ @created_at = Time.now
26
+ end
27
+
28
+ # Status check methods
29
+
30
+ # Check if the result is successful
31
+ # @return [Boolean]
32
+ def successful?
33
+ status == :success
34
+ end
35
+
36
+ # Check if the result failed
37
+ # @return [Boolean]
38
+ def failed?
39
+ status == :failed
40
+ end
41
+
42
+ # Check if the result timed out
43
+ # @return [Boolean]
44
+ def timeout?
45
+ status == :timeout
46
+ end
47
+
48
+ # Check if max iterations were reached
49
+ # @return [Boolean]
50
+ def max_iterations_reached?
51
+ status == :max_iterations_reached
52
+ end
53
+
54
+ # Data access methods
55
+
56
+ # Get the number of iterations (thoughts)
57
+ # @return [Integer]
58
+ def iterations
59
+ thoughts.length
60
+ end
61
+
62
+ # Convert to hash representation
63
+ # @return [Hash]
64
+ def to_h
65
+ build_hash.compact
66
+ end
67
+
68
+ # Convert to JSON string
69
+ # @return [String]
70
+ def to_json(*)
71
+ to_h.to_json(*)
72
+ end
73
+
74
+ # Get a summary of the result
75
+ # @return [String]
76
+ def summary
77
+ status_message_for(status)
78
+ end
79
+
80
+ # Get execution details
81
+ # @return [Hash]
82
+ def execution_details
83
+ {
84
+ iterations: iterations,
85
+ confidence: confidence_score ? format('%.1f%%', confidence_score * 100) : 'N/A',
86
+ time: execution_time ? "#{execution_time.round(2)}s" : 'N/A',
87
+ status: status
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ def build_hash
94
+ base_attributes.merge(execution_attributes)
95
+ end
96
+
97
+ def base_attributes
98
+ {
99
+ input: input,
100
+ thoughts: thoughts,
101
+ final_answer: final_answer,
102
+ confidence_score: confidence_score,
103
+ status: status
104
+ }
105
+ end
106
+
107
+ def execution_attributes
108
+ {
109
+ error: error,
110
+ execution_time: execution_time,
111
+ iterations: iterations,
112
+ created_at: @created_at
113
+ }
114
+ end
115
+
116
+ def status_message_for(current_status)
117
+ message_mapping[current_status] || "Status: #{current_status}"
118
+ end
119
+
120
+ def message_mapping
121
+ {
122
+ success: "Success: #{truncate(final_answer)}",
123
+ failed: "Failed: #{error}",
124
+ timeout: 'Timeout: Execution exceeded time limit',
125
+ max_iterations_reached: "Max iterations reached: #{iterations} iterations"
126
+ }
127
+ end
128
+
129
+ def truncate(text, length = 100)
130
+ return nil if text.nil?
131
+ return text if text.length <= length
132
+
133
+ "#{text[0..length]}..."
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ # RSpec test helpers for Soka framework
5
+ module TestHelpers
6
+ def self.included(base)
7
+ base.class_eval do
8
+ let(:mock_llm_response) { nil }
9
+ let(:mock_tool_responses) { {} }
10
+ end
11
+ end
12
+
13
+ def mock_ai_response(response_data)
14
+ @mock_llm_response = response_data
15
+
16
+ # Create a mock LLM that returns the specified response
17
+ mock_llm = instance_double(Soka::LLM::Base)
18
+
19
+ # Format the response content based on the response data
20
+ content = build_react_content(response_data)
21
+
22
+ result = create_mock_llm_result(content)
23
+
24
+ allow(mock_llm).to receive(:chat).and_return(result)
25
+ allow(Soka::LLM).to receive(:new).and_return(mock_llm)
26
+
27
+ mock_llm
28
+ end
29
+
30
+ def create_mock_llm_result(content)
31
+ Soka::LLM::Result.new(
32
+ model: 'mock-model',
33
+ content: content,
34
+ input_tokens: 100,
35
+ output_tokens: 200,
36
+ finish_reason: 'stop',
37
+ raw_response: { mock: true }
38
+ )
39
+ end
40
+
41
+ def mock_tool_response(tool_class, response)
42
+ @mock_tool_responses[tool_class] = response
43
+
44
+ # Create a mock instance of the tool
45
+ mock_tool = instance_double(tool_class)
46
+ allow(mock_tool).to receive(:class).and_return(tool_class)
47
+ allow(mock_tool).to receive(:execute).and_return(response)
48
+
49
+ # Allow the tool class to be instantiated with the mock
50
+ allow(tool_class).to receive(:new).and_return(mock_tool)
51
+
52
+ mock_tool
53
+ end
54
+
55
+ def allow_tool_to_fail(tool_class, error)
56
+ mock_tool = instance_double(tool_class)
57
+ allow(mock_tool).to receive(:class).and_return(tool_class)
58
+ allow(mock_tool).to receive(:execute).and_raise(error)
59
+
60
+ allow(tool_class).to receive(:new).and_return(mock_tool)
61
+
62
+ mock_tool
63
+ end
64
+
65
+ def create_test_agent(options = {})
66
+ Class.new(Soka::Agent) do
67
+ provider :gemini
68
+ model 'gemini-2.5-flash'
69
+ max_iterations 5
70
+ timeout 10
71
+ end.new(**options)
72
+ end
73
+
74
+ def create_test_tool(name: 'test_tool', description: 'Test tool', &block)
75
+ Class.new(Soka::AgentTool) do
76
+ desc description
77
+
78
+ define_singleton_method :tool_name do
79
+ name
80
+ end
81
+
82
+ define_method :call, &block || -> { 'Test response' }
83
+ end
84
+ end
85
+
86
+ def stub_env(env_vars)
87
+ env_vars.each do |key, value|
88
+ allow(ENV).to receive(:fetch).with(key).and_return(value)
89
+ allow(ENV).to receive(:[]).with(key).and_return(value)
90
+ end
91
+ end
92
+
93
+ def with_configuration
94
+ original_config = Soka.configuration
95
+ Soka.reset!
96
+ yield
97
+ ensure
98
+ Soka.configuration = original_config
99
+ end
100
+
101
+ private
102
+
103
+ def build_react_content(response_data)
104
+ content = []
105
+
106
+ # Build thoughts and actions
107
+ response_data[:thoughts].each_with_index do |thought_data, _index|
108
+ content << "<Thought>#{thought_data[:thought]}</Thought>"
109
+
110
+ next unless thought_data[:action]
111
+
112
+ action = thought_data[:action]
113
+ params_json = action[:params].to_json
114
+
115
+ content << build_action_content(action[:tool], params_json)
116
+
117
+ # The observation will be added by the engine
118
+ end
119
+
120
+ # Add final answer if present
121
+ content << "<Final_Answer>#{response_data[:final_answer]}</Final_Answer>" if response_data[:final_answer]
122
+
123
+ content.join("\n")
124
+ end
125
+
126
+ def build_action_content(tool, params_json)
127
+ <<~ACTION
128
+ <Action>
129
+ Tool: #{tool}
130
+ Parameters: #{params_json}
131
+ </Action>
132
+ ACTION
133
+ end
134
+
135
+ # Matcher helpers for RSpec
136
+ module Matchers
137
+ def be_successful
138
+ satisfy(&:successful?)
139
+ end
140
+
141
+ def be_failed
142
+ satisfy(&:failed?)
143
+ end
144
+
145
+ def final_answer?(expected = nil)
146
+ if expected
147
+ satisfy { |result| result.final_answer == expected }
148
+ else
149
+ satisfy { |result| !result.final_answer.nil? }
150
+ end
151
+ end
152
+
153
+ def thoughts_count?(count)
154
+ satisfy { |result| result.thoughts.length == count }
155
+ end
156
+
157
+ def confidence_score_above?(threshold)
158
+ satisfy { |result| result.confidence_score > threshold }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ # Stores and manages the thinking process history
5
+ class ThoughtsMemory
6
+ attr_reader :sessions
7
+
8
+ def initialize
9
+ @sessions = []
10
+ end
11
+
12
+ def add(input, result)
13
+ session = {
14
+ input: input,
15
+ thoughts: result.thoughts || [],
16
+ final_answer: result.final_answer,
17
+ status: result.status,
18
+ confidence_score: result.confidence_score,
19
+ timestamp: Time.now
20
+ }
21
+
22
+ session[:error] = result.error if result.error
23
+
24
+ @sessions << session
25
+ end
26
+
27
+ def last_session
28
+ @sessions.last
29
+ end
30
+
31
+ def all_sessions
32
+ @sessions
33
+ end
34
+
35
+ def clear
36
+ @sessions.clear
37
+ end
38
+
39
+ def size
40
+ @sessions.size
41
+ end
42
+
43
+ def empty?
44
+ @sessions.empty?
45
+ end
46
+
47
+ def successful_sessions
48
+ @sessions.select { |s| s[:status] == :success }
49
+ end
50
+
51
+ def failed_sessions
52
+ @sessions.select { |s| s[:status] == :failed }
53
+ end
54
+
55
+ def average_confidence_score
56
+ successful = successful_sessions
57
+ return 0.0 if successful.empty?
58
+
59
+ total = successful.sum { |s| s[:confidence_score] || 0.0 }
60
+ total / successful.size
61
+ end
62
+
63
+ def average_iterations
64
+ return 0 if @sessions.empty?
65
+
66
+ total = @sessions.sum { |s| s[:thoughts].size }
67
+ total.to_f / @sessions.size
68
+ end
69
+
70
+ def to_s
71
+ return '<Soka::ThoughtsMemory> (0 sessions)' if empty?
72
+
73
+ stats = build_stats
74
+
75
+ format_stats_string(stats)
76
+ end
77
+
78
+ def build_stats
79
+ {
80
+ total: size,
81
+ successful: successful_sessions.size,
82
+ failed: failed_sessions.size,
83
+ avg_confidence: format('%.2f', average_confidence_score),
84
+ avg_iterations: format('%.1f', average_iterations)
85
+ }
86
+ end
87
+
88
+ def format_stats_string(stats)
89
+ "<Soka::ThoughtsMemory> (#{stats[:total]} sessions, " \
90
+ "#{stats[:successful]} successful, #{stats[:failed]} failed, " \
91
+ "avg confidence: #{stats[:avg_confidence]}, " \
92
+ "avg iterations: #{stats[:avg_iterations]})"
93
+ end
94
+
95
+ def inspect
96
+ to_s
97
+ end
98
+
99
+ def to_h
100
+ {
101
+ sessions: @sessions,
102
+ stats: {
103
+ total_sessions: size,
104
+ successful_sessions: successful_sessions.size,
105
+ failed_sessions: failed_sessions.size,
106
+ average_confidence_score: average_confidence_score,
107
+ average_iterations: average_iterations
108
+ }
109
+ }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ VERSION = '0.0.1.beta2'
5
+ end
data/lib/soka.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'faraday'
5
+ require 'dry-validation'
6
+ require 'dry-struct'
7
+ require 'dry-types'
8
+
9
+ # Main module for the Soka ReAct Agent Framework
10
+ # Provides AI agent capabilities with multiple LLM providers support
11
+ module Soka
12
+ class Error < StandardError; end
13
+ class ConfigurationError < Error; end
14
+ class LLMError < Error; end
15
+ class ToolError < Error; end
16
+ class AgentError < Error; end
17
+ class MemoryError < Error; end
18
+
19
+ class << self
20
+ attr_accessor :configuration
21
+
22
+ def setup
23
+ self.configuration ||= Configuration.new
24
+ yield(configuration) if block_given?
25
+ end
26
+
27
+ def configure(&)
28
+ setup(&)
29
+ end
30
+
31
+ def reset!
32
+ self.configuration = Configuration.new
33
+ end
34
+ end
35
+
36
+ loader = Zeitwerk::Loader.for_gem
37
+ loader.inflector.inflect(
38
+ 'llm' => 'LLM',
39
+ 'llms' => 'LLMs',
40
+ 'ai' => 'AI',
41
+ 'openai' => 'OpenAI',
42
+ 'dsl_methods' => 'DSLMethods',
43
+ 'llm_builder' => 'LLMBuilder'
44
+ )
45
+ loader.setup
46
+ end
47
+
48
+ # Initialize default configuration
49
+ Soka.setup
data/sig/soka.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Soka
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: soka
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.beta2
5
+ platform: ruby
6
+ authors:
7
+ - jiunjiun
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-struct
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: dry-types
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.7'
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-validation
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.10'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.10'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: zeitwerk
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.6'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.6'
82
+ description: Soka is a Ruby framework for building AI agents using the ReAct (Reasoning
83
+ and Acting) pattern. It supports multiple AI providers including Gemini Studio,
84
+ OpenAI, and Anthropic.
85
+ email:
86
+ - imjiunjiun@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - CHANGELOG.md
94
+ - CLAUDE.md
95
+ - LICENSE
96
+ - README.md
97
+ - Rakefile
98
+ - examples/1_basic.rb
99
+ - examples/2_event_handling.rb
100
+ - examples/3_memory.rb
101
+ - examples/4_hooks.rb
102
+ - examples/5_error_handling.rb
103
+ - examples/6_retry.rb
104
+ - examples/7_tool_conditional.rb
105
+ - examples/8_multi_provider.rb
106
+ - lib/soka.rb
107
+ - lib/soka/agent.rb
108
+ - lib/soka/agent_tool.rb
109
+ - lib/soka/agent_tools/params_validator.rb
110
+ - lib/soka/agents/dsl_methods.rb
111
+ - lib/soka/agents/hook_manager.rb
112
+ - lib/soka/agents/llm_builder.rb
113
+ - lib/soka/agents/retry_handler.rb
114
+ - lib/soka/agents/tool_builder.rb
115
+ - lib/soka/configuration.rb
116
+ - lib/soka/engines/base.rb
117
+ - lib/soka/engines/concerns/prompt_template.rb
118
+ - lib/soka/engines/concerns/response_processor.rb
119
+ - lib/soka/engines/react.rb
120
+ - lib/soka/engines/reasoning_context.rb
121
+ - lib/soka/llm.rb
122
+ - lib/soka/llms/anthropic.rb
123
+ - lib/soka/llms/base.rb
124
+ - lib/soka/llms/concerns/response_parser.rb
125
+ - lib/soka/llms/concerns/streaming_handler.rb
126
+ - lib/soka/llms/gemini.rb
127
+ - lib/soka/llms/openai.rb
128
+ - lib/soka/memory.rb
129
+ - lib/soka/result.rb
130
+ - lib/soka/test_helpers.rb
131
+ - lib/soka/thoughts_memory.rb
132
+ - lib/soka/version.rb
133
+ - sig/soka.rbs
134
+ homepage: https://github.com/jiunjiun/soka
135
+ licenses: []
136
+ metadata:
137
+ homepage_uri: https://github.com/jiunjiun/soka
138
+ source_code_uri: https://github.com/jiunjiun/soka
139
+ changelog_uri: https://github.com/jiunjiun/soka/blob/main/CHANGELOG.md
140
+ rubygems_mfa_required: 'true'
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '3.4'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 3.6.9
156
+ specification_version: 4
157
+ summary: A Ruby ReAct Agent Framework with multi-LLM support
158
+ test_files: []