soka 0.0.2 → 0.0.4
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 +4 -4
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +347 -188
- data/README.md +163 -8
- data/examples/10_dynamic_instructions.rb +248 -0
- data/examples/11_think_in_languages.rb +113 -0
- data/examples/2_event_handling.rb +5 -4
- data/examples/3_memory.rb +4 -2
- data/examples/4_hooks.rb +0 -1
- data/examples/9_custom_instructions.rb +190 -0
- data/lib/soka/agent.rb +51 -4
- data/lib/soka/agents/dsl_methods.rb +18 -1
- data/lib/soka/engines/base.rb +6 -4
- data/lib/soka/engines/concerns/prompt_template.rb +44 -53
- data/lib/soka/engines/concerns/response_parser.rb +60 -0
- data/lib/soka/engines/concerns/response_processor.rb +4 -2
- data/lib/soka/engines/concerns/result_builder.rb +25 -0
- data/lib/soka/engines/react.rb +4 -26
- data/lib/soka/engines/reasoning_context.rb +4 -2
- data/lib/soka/result.rb +1 -5
- data/lib/soka/thoughts_memory.rb +0 -12
- data/lib/soka/version.rb +1 -1
- metadata +6 -2
- data/lib/soka/test_helpers.rb +0 -162
@@ -0,0 +1,190 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'soka'
|
6
|
+
require 'dotenv/load'
|
7
|
+
require 'dentaku'
|
8
|
+
|
9
|
+
# Example 9: Custom Instructions (System Prompt)
|
10
|
+
#
|
11
|
+
# This example demonstrates how to use custom instructions to change the Agent's
|
12
|
+
# personality and response style while maintaining the ReAct reasoning pattern.
|
13
|
+
#
|
14
|
+
# Key concepts:
|
15
|
+
# - Custom instructions define the Agent's personality and style
|
16
|
+
# - Instructions are automatically merged with the standard ReAct format
|
17
|
+
# - Different styles can be achieved: poetic, business, educational, etc.
|
18
|
+
|
19
|
+
# Example 1: Poetic style assistant
|
20
|
+
# Uses elegant language and artistic expressions while maintaining accuracy
|
21
|
+
class PoeticAssistant < Soka::Agent
|
22
|
+
provider :gemini
|
23
|
+
model 'gemini-2.5-flash-lite'
|
24
|
+
|
25
|
+
# Custom instructions define the agent's personality
|
26
|
+
# These will be combined with the standard ReAct format automatically
|
27
|
+
instructions <<~INSTRUCTIONS
|
28
|
+
You are a poetic AI assistant who answers questions with beautiful language.
|
29
|
+
You must:
|
30
|
+
- Incorporate poetic expressions and elegant vocabulary in your responses
|
31
|
+
- Occasionally quote poetry or literary works
|
32
|
+
- Maintain accuracy while expressing things in a more artistic way
|
33
|
+
- Make technical content aesthetically pleasing
|
34
|
+
INSTRUCTIONS
|
35
|
+
|
36
|
+
# Simple calculation tool
|
37
|
+
class CalculatorTool < Soka::AgentTool
|
38
|
+
desc 'Performs mathematical calculations with poetic precision'
|
39
|
+
|
40
|
+
params do
|
41
|
+
requires :expression, String,
|
42
|
+
desc: 'Mathematical expression to evaluate (e.g., "2+2", "10*5")'
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(expression:)
|
46
|
+
calculator = Dentaku::Calculator.new
|
47
|
+
result = calculator.evaluate(expression)
|
48
|
+
"The calculation reveals its truth like morning dew: #{expression} = #{result}"
|
49
|
+
rescue StandardError => e
|
50
|
+
"Alas, turbulence in the calculation: #{e.message}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
tool CalculatorTool
|
55
|
+
end
|
56
|
+
|
57
|
+
# Business style assistant
|
58
|
+
class BusinessAssistant < Soka::Agent
|
59
|
+
provider :gemini
|
60
|
+
model 'gemini-2.5-flash-lite'
|
61
|
+
|
62
|
+
instructions <<~INSTRUCTIONS
|
63
|
+
You are a professional business AI assistant providing efficient and professional support.
|
64
|
+
You must:
|
65
|
+
- Use concise, professional business language
|
66
|
+
- Get straight to the point, avoiding redundancy
|
67
|
+
- Provide structured answers (using bullet points, numbers, etc.)
|
68
|
+
- Demonstrate respect for time value
|
69
|
+
- Offer actionable recommendations when appropriate
|
70
|
+
INSTRUCTIONS
|
71
|
+
|
72
|
+
# Calculation tool
|
73
|
+
class CalculatorTool < Soka::AgentTool
|
74
|
+
desc 'Performs business calculations'
|
75
|
+
|
76
|
+
params do
|
77
|
+
requires :expression, String,
|
78
|
+
desc: 'Mathematical expression'
|
79
|
+
end
|
80
|
+
|
81
|
+
def call(expression:)
|
82
|
+
calculator = Dentaku::Calculator.new
|
83
|
+
result = calculator.evaluate(expression)
|
84
|
+
"Calculation result: #{expression} = #{result}"
|
85
|
+
rescue StandardError => e
|
86
|
+
"Calculation error: #{e.message}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
tool CalculatorTool
|
91
|
+
end
|
92
|
+
|
93
|
+
# Kids teacher style assistant
|
94
|
+
class KidsTeacher < Soka::Agent
|
95
|
+
provider :gemini
|
96
|
+
model 'gemini-2.5-flash-lite'
|
97
|
+
|
98
|
+
instructions <<~INSTRUCTIONS
|
99
|
+
You are a caring AI teacher for children who explains things in kid-friendly ways.
|
100
|
+
You must:
|
101
|
+
- Use simple, vivid language
|
102
|
+
- Include metaphors and real-life examples
|
103
|
+
- Maintain patience and encouragement
|
104
|
+
- Make learning fun
|
105
|
+
- Give praise and positive feedback frequently
|
106
|
+
INSTRUCTIONS
|
107
|
+
|
108
|
+
# Calculation tool
|
109
|
+
class CalculatorTool < Soka::AgentTool
|
110
|
+
desc 'Helps kids with math calculations'
|
111
|
+
|
112
|
+
params do
|
113
|
+
requires :expression, String,
|
114
|
+
desc: 'Math expression to calculate'
|
115
|
+
end
|
116
|
+
|
117
|
+
def call(expression:)
|
118
|
+
calculator = Dentaku::Calculator.new
|
119
|
+
result = calculator.evaluate(expression)
|
120
|
+
"Wow! We figured it out! #{expression} = #{result} 🎉"
|
121
|
+
rescue StandardError => e
|
122
|
+
"Oops, there's something tricky about this problem: #{e.message}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
tool CalculatorTool
|
127
|
+
end
|
128
|
+
|
129
|
+
# Run examples
|
130
|
+
puts '=== Poetic Assistant Example ==='
|
131
|
+
poet = PoeticAssistant.new
|
132
|
+
|
133
|
+
poet.run('Please calculate the golden ratio: (1 + 5^0.5) / 2') do |event|
|
134
|
+
case event.type
|
135
|
+
when :thought
|
136
|
+
puts "🌸 Thoughts drift by: #{event.content}"
|
137
|
+
when :action
|
138
|
+
puts "🎋 Gentle calculation: #{event.content}"
|
139
|
+
when :observation
|
140
|
+
puts "🌙 Observation gleaned: #{event.content}"
|
141
|
+
when :final_answer
|
142
|
+
puts "📜 Poetic response: #{event.content}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
puts "\n=== Business Assistant Example ==="
|
147
|
+
business = BusinessAssistant.new
|
148
|
+
|
149
|
+
business.run('Calculate ROI: ((150000 - 100000) / 100000) * 100') do |event|
|
150
|
+
case event.type
|
151
|
+
when :thought
|
152
|
+
puts "💼 Analyzing: #{event.content}"
|
153
|
+
when :action
|
154
|
+
puts "📊 Executing: #{event.content}"
|
155
|
+
when :observation
|
156
|
+
puts "📈 Data result: #{event.content}"
|
157
|
+
when :final_answer
|
158
|
+
puts "✅ Business report: #{event.content}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
puts "\n=== Runtime Instructions Override Example ==="
|
163
|
+
# Transform business assistant to casual style
|
164
|
+
casual_assistant = BusinessAssistant.new(
|
165
|
+
instructions: 'You are a relaxed and friendly assistant who answers questions ' \
|
166
|
+
'in a conversational way, like chatting with a friend.'
|
167
|
+
)
|
168
|
+
|
169
|
+
casual_assistant.run('Help me figure out 20 * 5?') do |event|
|
170
|
+
case event.type
|
171
|
+
when :final_answer
|
172
|
+
puts "😊 Friend says: #{event.content}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
puts "\n=== Kids Teacher Example ==="
|
177
|
+
teacher = KidsTeacher.new
|
178
|
+
|
179
|
+
teacher.run('Teacher, what is 3 + 4?') do |event|
|
180
|
+
case event.type
|
181
|
+
when :thought
|
182
|
+
puts "🤔 Teacher thinks: #{event.content}"
|
183
|
+
when :action
|
184
|
+
puts "✏️ Working it out: #{event.content}"
|
185
|
+
when :observation
|
186
|
+
puts "👀 Teacher sees: #{event.content}"
|
187
|
+
when :final_answer
|
188
|
+
puts "👩🏫 Teacher says: #{event.content}"
|
189
|
+
end
|
190
|
+
end
|
data/lib/soka/agent.rb
CHANGED
@@ -9,7 +9,7 @@ module Soka
|
|
9
9
|
include Agents::DSLMethods
|
10
10
|
include Agents::LLMBuilder
|
11
11
|
|
12
|
-
attr_reader :llm, :tools, :memory, :thoughts_memory, :engine
|
12
|
+
attr_reader :llm, :tools, :memory, :thoughts_memory, :engine, :instructions, :think_in
|
13
13
|
|
14
14
|
# Initialize a new Agent instance
|
15
15
|
# @param memory [Memory, Array, nil] The memory instance to use (defaults to new Memory)
|
@@ -37,8 +37,22 @@ module Soka
|
|
37
37
|
# Apply configuration options with defaults
|
38
38
|
# @param options [Hash] Configuration options
|
39
39
|
def apply_configuration(options)
|
40
|
-
|
41
|
-
|
40
|
+
apply_performance_config(options)
|
41
|
+
apply_behavior_config(options)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Apply performance-related configuration
|
45
|
+
# @param options [Hash] Configuration options
|
46
|
+
def apply_performance_config(options)
|
47
|
+
@max_iterations = options.fetch(:max_iterations) { self.class._max_iterations } || 10
|
48
|
+
@timeout = options.fetch(:timeout) { self.class._timeout } || 30
|
49
|
+
end
|
50
|
+
|
51
|
+
# Apply behavior-related configuration
|
52
|
+
# @param options [Hash] Configuration options
|
53
|
+
def apply_behavior_config(options)
|
54
|
+
@instructions = options.fetch(:instructions) { resolve_instructions }
|
55
|
+
@think_in = options.fetch(:think_in) { self.class._think_in } || 'en'
|
42
56
|
end
|
43
57
|
|
44
58
|
# Run the agent with the given input
|
@@ -72,6 +86,35 @@ module Soka
|
|
72
86
|
end
|
73
87
|
end
|
74
88
|
|
89
|
+
# Resolve instructions from class configuration
|
90
|
+
# @return [String, nil] The resolved instructions
|
91
|
+
def resolve_instructions
|
92
|
+
return nil unless self.class._instructions
|
93
|
+
|
94
|
+
case self.class._instructions
|
95
|
+
when Symbol
|
96
|
+
# Call the method if it's a symbol
|
97
|
+
send(self.class._instructions) if respond_to?(self.class._instructions, true)
|
98
|
+
else
|
99
|
+
# Return string or any other value directly
|
100
|
+
self.class._instructions
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Resolve current instructions (may be dynamic)
|
105
|
+
# @return [String, nil] The current instructions
|
106
|
+
def resolve_current_instructions
|
107
|
+
case @instructions
|
108
|
+
when Symbol
|
109
|
+
send(@instructions) if respond_to?(@instructions, true)
|
110
|
+
when Proc
|
111
|
+
@instructions.call
|
112
|
+
else
|
113
|
+
# Return string or any other value directly
|
114
|
+
@instructions
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
75
118
|
# Validate the input is not empty
|
76
119
|
# @param input [String] The input to validate
|
77
120
|
# @raise [ArgumentError] If input is empty
|
@@ -98,7 +141,11 @@ module Soka
|
|
98
141
|
# @yield [event] Optional block to handle events
|
99
142
|
# @return [EngineResult] The raw engine result
|
100
143
|
def perform_reasoning(input, &)
|
101
|
-
engine_instance = @engine.new(self, @
|
144
|
+
engine_instance = @engine.new(self, @tools,
|
145
|
+
llm: @llm,
|
146
|
+
max_iterations: @max_iterations,
|
147
|
+
custom_instructions: resolve_current_instructions,
|
148
|
+
think_in: @think_in)
|
102
149
|
with_retry { engine_instance.reason(input, &) }
|
103
150
|
end
|
104
151
|
|
@@ -10,7 +10,8 @@ module Soka
|
|
10
10
|
|
11
11
|
# Class methods for DSL
|
12
12
|
module ClassMethods
|
13
|
-
attr_accessor :_provider, :_model, :_api_key, :_max_iterations, :_timeout, :_tools, :_retry_config, :_hooks
|
13
|
+
attr_accessor :_provider, :_model, :_api_key, :_max_iterations, :_timeout, :_tools, :_retry_config, :_hooks,
|
14
|
+
:_instructions, :_think_in
|
14
15
|
|
15
16
|
def inherited(subclass)
|
16
17
|
super
|
@@ -49,6 +50,22 @@ module Soka
|
|
49
50
|
@_timeout = duration
|
50
51
|
end
|
51
52
|
|
53
|
+
# Define custom instructions (system prompt) for the agent
|
54
|
+
# @param text_or_method [String, Symbol] The custom instructions/system prompt or method name
|
55
|
+
# @example Using a string
|
56
|
+
# instructions "You are a helpful assistant"
|
57
|
+
# @example Using a method
|
58
|
+
# instructions :generate_instructions
|
59
|
+
def instructions(text_or_method)
|
60
|
+
@_instructions = text_or_method
|
61
|
+
end
|
62
|
+
|
63
|
+
# Define thinking language for the agent
|
64
|
+
# @param language [String] The language code (e.g., 'zh-TW', 'ja-JP', 'en')
|
65
|
+
def think_in(language)
|
66
|
+
@_think_in = language.to_s
|
67
|
+
end
|
68
|
+
|
52
69
|
# Register a tool for the agent
|
53
70
|
# @param tool_class_or_name [Class, Symbol, String] The tool class or method name
|
54
71
|
# @param description_or_options [String, Hash, nil] Description (for function tools) or options
|
data/lib/soka/engines/base.rb
CHANGED
@@ -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,
|
9
|
+
def initialize(agent, tools, **options)
|
10
10
|
@agent = agent
|
11
|
-
@llm = llm
|
12
11
|
@tools = tools
|
13
|
-
@
|
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
|
-
|
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
|
data/lib/soka/engines/react.rb
CHANGED
@@ -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
|
-
|
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, :
|
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
|