soka 0.0.2 → 0.0.3

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.
@@ -8,6 +8,15 @@ module Soka
8
8
  private
9
9
 
10
10
  def system_prompt
11
+ # Use custom instructions if provided, otherwise use default ReAct prompt
12
+ if custom_instructions
13
+ combine_with_react_format(custom_instructions)
14
+ else
15
+ default_react_prompt
16
+ end
17
+ end
18
+
19
+ def default_react_prompt
11
20
  tools_description = format_tools_description(tools)
12
21
 
13
22
  <<~PROMPT
@@ -20,16 +29,50 @@ module Soka
20
29
  PROMPT
21
30
  end
22
31
 
32
+ def combine_with_react_format(instructions)
33
+ tools_description = format_tools_description(tools)
34
+
35
+ <<~PROMPT
36
+ #{instructions}
37
+
38
+ You have access to the following tools:
39
+ #{tools_description}
40
+
41
+ #{format_instructions}
42
+ PROMPT
43
+ end
44
+
23
45
  def format_instructions
46
+ thinking_instruction = build_thinking_instruction(think_in)
47
+
24
48
  <<~INSTRUCTIONS
25
49
  You must follow this exact format for each step:
26
50
 
51
+ #{thinking_instruction}
52
+
27
53
  <Thought>Your reasoning about what to do next</Thought>
28
54
  <Action>
29
55
  Tool: tool_name
30
56
  Parameters: {"param1": "value1", "param2": "value2"}
31
57
  </Action>
32
58
 
59
+ #{action_format_rules}
60
+ INSTRUCTIONS
61
+ end
62
+
63
+ # Build thinking instruction based on language
64
+ # @param language [String, nil] The language to use for thinking
65
+ # @return [String] The thinking instruction
66
+ def build_thinking_instruction(language)
67
+ return '' unless language
68
+
69
+ "Use #{language} for your reasoning in <Thought> tags."
70
+ end
71
+
72
+ # Action format rules
73
+ # @return [String] The action format rules
74
+ def action_format_rules
75
+ <<~RULES
33
76
  STOP HERE after each Action. Do NOT include <Observation> in your response.
34
77
  The system will execute the tool and provide the observation.
35
78
 
@@ -45,7 +88,7 @@ module Soka
45
88
  5. NEVER include <Observation> tags - wait for the system to provide them
46
89
  6. Provide a clear and complete <Final_Answer> when done
47
90
  7. If you cannot complete the task, explain why in the <Final_Answer>
48
- INSTRUCTIONS
91
+ RULES
49
92
  end
50
93
 
51
94
  def format_tools_description(tools)
@@ -72,58 +115,6 @@ module Soka
72
115
 
73
116
  properties.join(', ')
74
117
  end
75
-
76
- def parse_response(text)
77
- thoughts = extract_tagged_content(text, 'Thought')
78
- actions = extract_actions(text)
79
- final_answer = extract_tagged_content(text, 'Final_Answer').first
80
-
81
- {
82
- thoughts: thoughts,
83
- actions: actions,
84
- final_answer: final_answer
85
- }
86
- end
87
-
88
- def extract_tagged_content(text, tag)
89
- pattern = %r{<#{tag}>(.*?)</#{tag}>}m
90
- text.scan(pattern).map { |match| match[0].strip }
91
- end
92
-
93
- def extract_actions(text)
94
- action_blocks = text.scan(%r{<Action>(.*?)</Action>}m)
95
- action_blocks.filter_map { |block| parse_action_block(block[0]) }
96
- end
97
-
98
- def parse_action_block(content)
99
- content = content.strip
100
- tool_match = content.match(/Tool:\s*(.+)/)
101
- params_match = content.match(/Parameters:\s*(.+)/m)
102
-
103
- return unless tool_match && params_match
104
-
105
- tool_name = tool_match[1].strip
106
- params_json = params_match[1].strip
107
- params = parse_json_params(params_json)
108
-
109
- { tool: tool_name, params: params }
110
- end
111
-
112
- # Parse JSON parameters from action block
113
- # @param params_json [String] The JSON string to parse
114
- # @return [Hash] The parsed parameters as a hash with symbol keys
115
- def parse_json_params(params_json)
116
- # Clean up the JSON string - remove any trailing commas or whitespace
117
- cleaned_json = params_json.strip.gsub(/,\s*}/, '}').gsub(/,\s*\]/, ']')
118
- JSON.parse(cleaned_json, symbolize_names: true)
119
- rescue JSON::ParserError
120
- # Return empty hash to continue when JSON parsing fails
121
- {}
122
- end
123
-
124
- def format_observation(observation)
125
- "<Observation>#{observation}</Observation>"
126
- end
127
118
  end
128
119
  end
129
120
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ module Concerns
6
+ # Module for parsing LLM responses in ReAct format
7
+ module ResponseParser
8
+ private
9
+
10
+ def parse_response(text)
11
+ extract_response_parts(text)
12
+ end
13
+
14
+ def extract_response_parts(text)
15
+ {
16
+ thoughts: extract_tagged_content(text, 'Thought'),
17
+ actions: extract_actions(text),
18
+ final_answer: extract_tagged_content(text, 'Final_Answer').first
19
+ }
20
+ end
21
+
22
+ def extract_tagged_content(text, tag)
23
+ pattern = %r{<#{tag}>(.*?)</#{tag}>}m
24
+ text.scan(pattern).map { |match| match[0].strip }
25
+ end
26
+
27
+ def extract_actions(text)
28
+ action_blocks = text.scan(%r{<Action>(.*?)</Action>}m)
29
+ action_blocks.filter_map { |block| parse_action_block(block[0]) }
30
+ end
31
+
32
+ def parse_action_block(content)
33
+ content = content.strip
34
+ tool_match = content.match(/Tool:\s*(.+)/)
35
+ params_match = content.match(/Parameters:\s*(.+)/m)
36
+
37
+ return unless tool_match && params_match
38
+
39
+ tool_name = tool_match[1].strip
40
+ params_json = params_match[1].strip
41
+ params = parse_json_params(params_json)
42
+
43
+ { tool: tool_name, params: params }
44
+ end
45
+
46
+ # Parse JSON parameters from action block
47
+ # @param params_json [String] The JSON string to parse
48
+ # @return [Hash] The parsed parameters as a hash with symbol keys
49
+ def parse_json_params(params_json)
50
+ # Clean up the JSON string - remove any trailing commas or whitespace
51
+ cleaned_json = params_json.strip.gsub(/,\s*}/, '}').gsub(/,\s*\]/, ']')
52
+ JSON.parse(cleaned_json, symbolize_names: true)
53
+ rescue JSON::ParserError
54
+ # Return empty hash to continue when JSON parsing fails
55
+ {}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -5,8 +5,6 @@ module Soka
5
5
  module Concerns
6
6
  # Module for processing responses in ReAct engine
7
7
  module ResponseProcessor
8
- include Concerns::PromptTemplate
9
-
10
8
  private
11
9
 
12
10
  # Process thoughts from parsed response
@@ -94,6 +92,10 @@ module Soka
94
92
  content: 'Please follow the exact format with <Thought>, <Action>, and <Final_Answer> tags.'
95
93
  )
96
94
  end
95
+
96
+ def format_observation(observation)
97
+ "<Observation>#{observation}</Observation>"
98
+ end
97
99
  end
98
100
  end
99
101
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ module Engines
5
+ module Concerns
6
+ # Module for building ReAct reasoning results
7
+ module ResultBuilder
8
+ private
9
+
10
+ def build_result(input:, thoughts:, final_answer:, status:, error: nil)
11
+ result = {
12
+ input: input,
13
+ thoughts: thoughts,
14
+ final_answer: final_answer,
15
+ status: status
16
+ }
17
+
18
+ result[:error] = error if error
19
+
20
+ Soka::Engines::React::ReasonResult.new(**result)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -8,6 +8,8 @@ module Soka
8
8
  class React < Base
9
9
  include Concerns::ResponseProcessor
10
10
  include Concerns::PromptTemplate
11
+ include Concerns::ResponseParser
12
+ include Concerns::ResultBuilder
11
13
 
12
14
  ReasonResult = Struct.new(:input, :thoughts, :final_answer, :status, :error, :confidence_score,
13
15
  keyword_init: true) do
@@ -21,7 +23,8 @@ module Soka
21
23
  # @yield [event] Optional block to handle events during execution
22
24
  # @return [ReasonResult] The result of the reasoning process
23
25
  def reason(task, &block)
24
- context = ReasoningContext.new(task: task, event_handler: block, max_iterations: max_iterations)
26
+ context = ReasoningContext.new(task: task, event_handler: block, max_iterations: max_iterations,
27
+ think_in: think_in)
25
28
  context.messages = build_messages(task)
26
29
 
27
30
  result = iterate_reasoning(context)
@@ -106,31 +109,6 @@ module Soka
106
109
  status: :success
107
110
  )
108
111
  end
109
-
110
- def build_result(input:, thoughts:, final_answer:, status:, error: nil)
111
- result = {
112
- input: input,
113
- thoughts: thoughts,
114
- final_answer: final_answer,
115
- status: status
116
- }
117
-
118
- result[:error] = error if error
119
-
120
- # Calculate confidence score based on iterations and status
121
- result[:confidence_score] = calculate_confidence_score(thoughts, status)
122
-
123
- ReasonResult.new(**result)
124
- end
125
-
126
- def calculate_confidence_score(thoughts, status)
127
- return 0.0 if status != :success
128
-
129
- base_score = 0.85
130
- iteration_penalty = thoughts.length * 0.05
131
-
132
- [base_score - iteration_penalty, 0.5].max
133
- end
134
112
  end
135
113
  end
136
114
  end
@@ -8,17 +8,19 @@ module Soka
8
8
  # Event structure for emitting events
9
9
  Event = Struct.new(:type, :content)
10
10
 
11
- attr_accessor :messages, :thoughts, :task, :iteration, :parsed_response
11
+ attr_accessor :messages, :thoughts, :task, :iteration, :parsed_response, :think_in
12
12
  attr_reader :event_handler, :max_iterations
13
13
 
14
14
  # Initialize a new reasoning context
15
15
  # @param task [String] The task to be processed
16
16
  # @param event_handler [Proc, nil] Optional block to handle events
17
17
  # @param max_iterations [Integer] Maximum number of reasoning iterations
18
- def initialize(task:, event_handler: nil, max_iterations: 10)
18
+ # @param think_in [String, nil] The language to use for thinking
19
+ def initialize(task:, event_handler: nil, max_iterations: 10, think_in: nil)
19
20
  @task = task
20
21
  @event_handler = event_handler
21
22
  @max_iterations = max_iterations
23
+ @think_in = think_in
22
24
  @messages = []
23
25
  @thoughts = []
24
26
  @iteration = 0
data/lib/soka/result.rb CHANGED
@@ -3,14 +3,13 @@
3
3
  module Soka
4
4
  # Represents the result of an agent's reasoning process
5
5
  class Result
6
- attr_reader :input, :thoughts, :final_answer, :confidence_score, :status, :error, :execution_time
6
+ attr_reader :input, :thoughts, :final_answer, :status, :error, :execution_time
7
7
 
8
8
  # Initialize a new Result instance
9
9
  # @param attributes [Hash] Result attributes
10
10
  # @option attributes [String] :input The original input
11
11
  # @option attributes [Array] :thoughts Array of thought objects
12
12
  # @option attributes [String] :final_answer The final answer
13
- # @option attributes [Float] :confidence_score Confidence score (0.0-1.0)
14
13
  # @option attributes [Symbol] :status The result status
15
14
  # @option attributes [String] :error Error message if failed
16
15
  # @option attributes [Float] :execution_time Time taken in seconds
@@ -18,7 +17,6 @@ module Soka
18
17
  @input = attributes[:input]
19
18
  @thoughts = attributes[:thoughts] || []
20
19
  @final_answer = attributes[:final_answer]
21
- @confidence_score = attributes[:confidence_score] || 0.0
22
20
  @status = attributes[:status] || :pending
23
21
  @error = attributes[:error]
24
22
  @execution_time = attributes[:execution_time]
@@ -82,7 +80,6 @@ module Soka
82
80
  def execution_details
83
81
  {
84
82
  iterations: iterations,
85
- confidence: confidence_score ? format('%.1f%%', confidence_score * 100) : 'N/A',
86
83
  time: execution_time ? "#{execution_time.round(2)}s" : 'N/A',
87
84
  status: status
88
85
  }
@@ -99,7 +96,6 @@ module Soka
99
96
  input: input,
100
97
  thoughts: thoughts,
101
98
  final_answer: final_answer,
102
- confidence_score: confidence_score,
103
99
  status: status
104
100
  }
105
101
  end
@@ -15,7 +15,6 @@ module Soka
15
15
  thoughts: result.thoughts || [],
16
16
  final_answer: result.final_answer,
17
17
  status: result.status,
18
- confidence_score: result.confidence_score,
19
18
  timestamp: Time.now
20
19
  }
21
20
 
@@ -52,14 +51,6 @@ module Soka
52
51
  @sessions.select { |s| s[:status] == :failed }
53
52
  end
54
53
 
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
54
  def average_iterations
64
55
  return 0 if @sessions.empty?
65
56
 
@@ -80,7 +71,6 @@ module Soka
80
71
  total: size,
81
72
  successful: successful_sessions.size,
82
73
  failed: failed_sessions.size,
83
- avg_confidence: format('%.2f', average_confidence_score),
84
74
  avg_iterations: format('%.1f', average_iterations)
85
75
  }
86
76
  end
@@ -88,7 +78,6 @@ module Soka
88
78
  def format_stats_string(stats)
89
79
  "<Soka::ThoughtsMemory> (#{stats[:total]} sessions, " \
90
80
  "#{stats[:successful]} successful, #{stats[:failed]} failed, " \
91
- "avg confidence: #{stats[:avg_confidence]}, " \
92
81
  "avg iterations: #{stats[:avg_iterations]})"
93
82
  end
94
83
 
@@ -103,7 +92,6 @@ module Soka
103
92
  total_sessions: size,
104
93
  successful_sessions: successful_sessions.size,
105
94
  failed_sessions: failed_sessions.size,
106
- average_confidence_score: average_confidence_score,
107
95
  average_iterations: average_iterations
108
96
  }
109
97
  }
data/lib/soka/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Soka
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - jiunjiun
@@ -53,6 +53,7 @@ files:
53
53
  - LICENSE
54
54
  - README.md
55
55
  - Rakefile
56
+ - examples/10_think_in_languages.rb
56
57
  - examples/1_basic.rb
57
58
  - examples/2_event_handling.rb
58
59
  - examples/3_memory.rb
@@ -61,6 +62,7 @@ files:
61
62
  - examples/6_retry.rb
62
63
  - examples/7_tool_conditional.rb
63
64
  - examples/8_multi_provider.rb
65
+ - examples/9_custom_instructions.rb
64
66
  - lib/soka.rb
65
67
  - lib/soka/agent.rb
66
68
  - lib/soka/agent_tool.rb
@@ -73,7 +75,9 @@ files:
73
75
  - lib/soka/configuration.rb
74
76
  - lib/soka/engines/base.rb
75
77
  - lib/soka/engines/concerns/prompt_template.rb
78
+ - lib/soka/engines/concerns/response_parser.rb
76
79
  - lib/soka/engines/concerns/response_processor.rb
80
+ - lib/soka/engines/concerns/result_builder.rb
77
81
  - lib/soka/engines/react.rb
78
82
  - lib/soka/engines/reasoning_context.rb
79
83
  - lib/soka/llm.rb
@@ -83,7 +87,6 @@ files:
83
87
  - lib/soka/llms/openai.rb
84
88
  - lib/soka/memory.rb
85
89
  - lib/soka/result.rb
86
- - lib/soka/test_helpers.rb
87
90
  - lib/soka/thoughts_memory.rb
88
91
  - lib/soka/version.rb
89
92
  - sig/soka.rbs
@@ -1,162 +0,0 @@
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