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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +347 -188
- data/README.md +135 -8
- data/examples/10_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 +22 -4
- data/lib/soka/agents/dsl_methods.rb +14 -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 +5 -2
- data/lib/soka/test_helpers.rb +0 -162
@@ -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
|
data/lib/soka/thoughts_memory.rb
CHANGED
@@ -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
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.
|
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
|
data/lib/soka/test_helpers.rb
DELETED
@@ -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
|