soka 0.0.1 → 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.
@@ -4,13 +4,15 @@ module Soka
4
4
  module Engines
5
5
  # Base class for reasoning engines
6
6
  class Base
7
- attr_reader :agent, :llm, :tools, :max_iterations
7
+ attr_reader :agent, :llm, :tools, :max_iterations, :custom_instructions, :think_in
8
8
 
9
- def initialize(agent, llm, tools, max_iterations)
9
+ def initialize(agent, tools, **options)
10
10
  @agent = agent
11
- @llm = llm
12
11
  @tools = tools
13
- @max_iterations = max_iterations
12
+ @llm = options[:llm]
13
+ @max_iterations = options[:max_iterations] || 10
14
+ @custom_instructions = options[:custom_instructions]
15
+ @think_in = options[:think_in]
14
16
  end
15
17
 
16
18
  def reason(task)
@@ -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
@@ -75,10 +73,7 @@ module Soka
75
73
  tool = tools.find { |t| t.class.tool_name == tool_name }
76
74
  raise ToolError, "Tool '#{tool_name}' not found" unless tool
77
75
 
78
- tool.call(**symbolize_keys(tool_input))
79
- rescue StandardError => e
80
- # Re-raise as ToolError to be caught by process_action
81
- raise ToolError, "Error executing tool: #{e.message}"
76
+ tool.execute(**symbolize_keys(tool_input))
82
77
  end
83
78
 
84
79
  def symbolize_keys(hash)
@@ -97,6 +92,10 @@ module Soka
97
92
  content: 'Please follow the exact format with <Thought>, <Action>, and <Final_Answer> tags.'
98
93
  )
99
94
  end
95
+
96
+ def format_observation(observation)
97
+ "<Observation>#{observation}</Observation>"
98
+ end
100
99
  end
101
100
  end
102
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.1'
4
+ VERSION = '0.0.3'
5
5
  end
data/lib/soka.rb CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  require 'zeitwerk'
4
4
  require 'faraday'
5
- require 'dry-validation'
6
- require 'dry-struct'
7
- require 'dry-types'
8
5
 
9
6
  # Main module for the Soka ReAct Agent Framework
10
7
  # Provides AI agent capabilities with multiple LLM providers support
metadata CHANGED
@@ -1,56 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - jiunjiun
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
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
12
  - !ruby/object:Gem::Dependency
55
13
  name: faraday
56
14
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +38,7 @@ dependencies:
80
38
  - !ruby/object:Gem::Version
81
39
  version: '2.6'
82
40
  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,
41
+ and Acting) pattern. It supports multiple AI providers including Gemini AI Studio,
84
42
  OpenAI, and Anthropic.
85
43
  email:
86
44
  - imjiunjiun@gmail.com
@@ -95,6 +53,7 @@ files:
95
53
  - LICENSE
96
54
  - README.md
97
55
  - Rakefile
56
+ - examples/10_think_in_languages.rb
98
57
  - examples/1_basic.rb
99
58
  - examples/2_event_handling.rb
100
59
  - examples/3_memory.rb
@@ -103,6 +62,7 @@ files:
103
62
  - examples/6_retry.rb
104
63
  - examples/7_tool_conditional.rb
105
64
  - examples/8_multi_provider.rb
65
+ - examples/9_custom_instructions.rb
106
66
  - lib/soka.rb
107
67
  - lib/soka/agent.rb
108
68
  - lib/soka/agent_tool.rb
@@ -115,7 +75,9 @@ files:
115
75
  - lib/soka/configuration.rb
116
76
  - lib/soka/engines/base.rb
117
77
  - lib/soka/engines/concerns/prompt_template.rb
78
+ - lib/soka/engines/concerns/response_parser.rb
118
79
  - lib/soka/engines/concerns/response_processor.rb
80
+ - lib/soka/engines/concerns/result_builder.rb
119
81
  - lib/soka/engines/react.rb
120
82
  - lib/soka/engines/reasoning_context.rb
121
83
  - lib/soka/llm.rb
@@ -125,7 +87,6 @@ files:
125
87
  - lib/soka/llms/openai.rb
126
88
  - lib/soka/memory.rb
127
89
  - lib/soka/result.rb
128
- - lib/soka/test_helpers.rb
129
90
  - lib/soka/thoughts_memory.rb
130
91
  - lib/soka/version.rb
131
92
  - sig/soka.rbs