llm_chain 0.5.5 ā 0.6.0
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 +48 -2
- data/README.md +104 -14
- data/examples/composite_agent_example.rb +121 -0
- data/examples/planner_agent_example.rb +103 -0
- data/examples/quick_demo.rb +0 -6
- data/examples/react_agent_example.rb +64 -0
- data/examples/tools_example.rb +1 -163
- data/lib/llm_chain/agents/agent_factory.rb +120 -0
- data/lib/llm_chain/agents/composite_agent.rb +341 -0
- data/lib/llm_chain/agents/planner_agent.rb +108 -0
- data/lib/llm_chain/agents/react_agent.rb +307 -0
- data/lib/llm_chain/agents.rb +19 -0
- data/lib/llm_chain/clients/gemma3.rb +5 -3
- data/lib/llm_chain/clients/openai.rb +1 -1
- data/lib/llm_chain/clients/qwen.rb +27 -15
- data/lib/llm_chain/configuration_validator.rb +11 -7
- data/lib/llm_chain/embeddings/clients/local/ollama_client.rb +2 -0
- data/lib/llm_chain/embeddings/clients/local/weaviate_vector_store.rb +2 -2
- data/lib/llm_chain/interfaces/agent.rb +54 -0
- data/lib/llm_chain/tools/code_interpreter.rb +10 -3
- data/lib/llm_chain/tools/date_time.rb +63 -7
- data/lib/llm_chain/tools/web_search.rb +83 -22
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +1 -0
- metadata +114 -24
data/examples/tools_example.rb
CHANGED
@@ -5,11 +5,9 @@ require_relative '../lib/llm_chain'
|
|
5
5
|
puts "š ļø LLMChain Tools Demo"
|
6
6
|
puts "=" * 40
|
7
7
|
|
8
|
-
# 1. Individual Tool Usage
|
9
8
|
puts "\n1. š§® Calculator Tool"
|
10
9
|
calculator = LLMChain::Tools::Calculator.new
|
11
10
|
|
12
|
-
# Test mathematical expressions
|
13
11
|
examples = [
|
14
12
|
"15 * 7 + 3",
|
15
13
|
"sqrt(144)",
|
@@ -21,7 +19,6 @@ examples.each do |expr|
|
|
21
19
|
puts "š #{result[:formatted]}"
|
22
20
|
end
|
23
21
|
|
24
|
-
# 2. Web Search Tool
|
25
22
|
puts "\n2. š Web Search Tool"
|
26
23
|
search = LLMChain::Tools::WebSearch.new
|
27
24
|
|
@@ -45,15 +42,12 @@ search_queries.each do |query|
|
|
45
42
|
end
|
46
43
|
end
|
47
44
|
|
48
|
-
# 3. Code Interpreter Tool
|
49
45
|
puts "\n3. š» Code Interpreter Tool"
|
50
46
|
interpreter = LLMChain::Tools::CodeInterpreter.new
|
51
47
|
|
52
|
-
# Test Ruby code execution
|
53
48
|
ruby_examples = [
|
54
49
|
<<~RUBY,
|
55
50
|
```ruby
|
56
|
-
# Simple Fibonacci calculation
|
57
51
|
a, b = 0, 1
|
58
52
|
result = [a, b]
|
59
53
|
8.times do
|
@@ -69,7 +63,6 @@ ruby_examples = [
|
|
69
63
|
|
70
64
|
<<~RUBY
|
71
65
|
```ruby
|
72
|
-
# Data analysis example
|
73
66
|
numbers = [23, 45, 67, 89, 12, 34, 56, 78]
|
74
67
|
|
75
68
|
puts "Dataset: \#{numbers}"
|
@@ -97,159 +90,4 @@ ruby_examples.each_with_index do |code, i|
|
|
97
90
|
rescue => e
|
98
91
|
puts "ā Execution error: #{e.message}"
|
99
92
|
end
|
100
|
-
end
|
101
|
-
|
102
|
-
# 4. Tool Manager Usage
|
103
|
-
puts "\n4. šÆ Tool Manager"
|
104
|
-
tool_manager = LLMChain::Tools::ToolManagerFactory.create_default_toolset
|
105
|
-
|
106
|
-
puts "Registered tools: #{tool_manager.list_tools.map(&:name).join(', ')}"
|
107
|
-
|
108
|
-
# Test tool matching
|
109
|
-
test_prompts = [
|
110
|
-
"What is 25 squared?",
|
111
|
-
"Find information about Ruby gems",
|
112
|
-
"Run this code: puts 'Hello World'"
|
113
|
-
]
|
114
|
-
|
115
|
-
test_prompts.each do |prompt|
|
116
|
-
puts "\nšÆ Testing: \"#{prompt}\""
|
117
|
-
matched_tools = tool_manager.list_tools.select { |tool| tool.match?(prompt) }
|
118
|
-
|
119
|
-
if matched_tools.any?
|
120
|
-
puts " Matched tools: #{matched_tools.map(&:name).join(', ')}"
|
121
|
-
|
122
|
-
# Execute with first matched tool
|
123
|
-
tool = matched_tools.first
|
124
|
-
result = tool.call(prompt)
|
125
|
-
puts " Result: #{result[:formatted] || result.inspect}"
|
126
|
-
else
|
127
|
-
puts " No tools matched"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# 5. LLM Chain with Tools
|
132
|
-
puts "\n5. š¤ LLM Chain with Tools"
|
133
|
-
|
134
|
-
begin
|
135
|
-
# Create chain with tools (but without retriever for local testing)
|
136
|
-
chain = LLMChain::Chain.new(
|
137
|
-
model: "qwen3:1.7b",
|
138
|
-
tools: tool_manager,
|
139
|
-
retriever: false # Disable RAG for this example
|
140
|
-
)
|
141
|
-
|
142
|
-
# Test queries that should trigger tools
|
143
|
-
test_queries = [
|
144
|
-
"Calculate the area of a circle with radius 5 (use pi = 3.14159)",
|
145
|
-
"What's the latest news about Ruby programming?",
|
146
|
-
"Execute this Ruby code: puts (1..100).select(&:even?).sum"
|
147
|
-
]
|
148
|
-
|
149
|
-
test_queries.each_with_index do |query, i|
|
150
|
-
puts "\nš¤ Query #{i+1}: #{query}"
|
151
|
-
puts "š Processing..."
|
152
|
-
|
153
|
-
response = chain.ask(query)
|
154
|
-
puts "š Response: #{response}"
|
155
|
-
puts "-" * 40
|
156
|
-
end
|
157
|
-
|
158
|
-
rescue => e
|
159
|
-
puts "ā Error with LLM Chain: #{e.message}"
|
160
|
-
puts "š” Make sure Ollama is running with qwen3:1.7b model"
|
161
|
-
end
|
162
|
-
|
163
|
-
# 6. Custom Tool Example
|
164
|
-
puts "\n6. š§ Custom Tool Example"
|
165
|
-
|
166
|
-
# Create a simple DateTime tool
|
167
|
-
class DateTimeTool < LLMChain::Tools::BaseTool
|
168
|
-
def initialize
|
169
|
-
super(
|
170
|
-
name: "datetime",
|
171
|
-
description: "Gets current date and time information",
|
172
|
-
parameters: {
|
173
|
-
format: {
|
174
|
-
type: "string",
|
175
|
-
description: "Date format (optional)"
|
176
|
-
}
|
177
|
-
}
|
178
|
-
)
|
179
|
-
end
|
180
|
-
|
181
|
-
def match?(prompt)
|
182
|
-
contains_keywords?(prompt, ['time', 'date', 'now', 'current', 'today'])
|
183
|
-
end
|
184
|
-
|
185
|
-
def call(prompt, context: {})
|
186
|
-
now = Time.now
|
187
|
-
|
188
|
-
# Try to detect desired format from prompt
|
189
|
-
format = if prompt.match?(/iso|standard/i)
|
190
|
-
now.iso8601
|
191
|
-
elsif prompt.match?(/human|readable/i)
|
192
|
-
now.strftime("%B %d, %Y at %I:%M %p")
|
193
|
-
else
|
194
|
-
now.to_s
|
195
|
-
end
|
196
|
-
|
197
|
-
{
|
198
|
-
timestamp: now.to_i,
|
199
|
-
formatted_time: format,
|
200
|
-
timezone: now.zone,
|
201
|
-
formatted: "Current time: #{format} (#{now.zone})"
|
202
|
-
}
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
# Test custom tool
|
207
|
-
datetime_tool = DateTimeTool.new
|
208
|
-
time_queries = [
|
209
|
-
"What time is it now?",
|
210
|
-
"Give me current date in human readable format",
|
211
|
-
"Show me the current time in ISO format"
|
212
|
-
]
|
213
|
-
|
214
|
-
time_queries.each do |query|
|
215
|
-
puts "\nš Query: #{query}"
|
216
|
-
if datetime_tool.match?(query)
|
217
|
-
result = datetime_tool.call(query)
|
218
|
-
puts " #{result[:formatted]}"
|
219
|
-
else
|
220
|
-
puts " Query didn't match datetime tool"
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
# 7. Configuration-based Tool Setup
|
225
|
-
puts "\n7. āļø Configuration-based Tools"
|
226
|
-
|
227
|
-
tools_config = [
|
228
|
-
{
|
229
|
-
class: 'calculator'
|
230
|
-
},
|
231
|
-
{
|
232
|
-
class: 'web_search',
|
233
|
-
options: {
|
234
|
-
search_engine: :duckduckgo
|
235
|
-
}
|
236
|
-
},
|
237
|
-
{
|
238
|
-
class: 'code_interpreter',
|
239
|
-
options: {
|
240
|
-
timeout: 30,
|
241
|
-
allowed_languages: ['ruby', 'python']
|
242
|
-
}
|
243
|
-
}
|
244
|
-
]
|
245
|
-
|
246
|
-
config_tool_manager = LLMChain::Tools::ToolManagerFactory.from_config(tools_config)
|
247
|
-
puts "Tools from config: #{config_tool_manager.list_tools.map(&:name).join(', ')}"
|
248
|
-
|
249
|
-
# Test configuration-based setup
|
250
|
-
config_result = config_tool_manager.execute_tool('calculator', 'What is 99 * 99?')
|
251
|
-
puts "Config test result: #{config_result[:formatted]}" if config_result
|
252
|
-
|
253
|
-
puts "\n" + "=" * 40
|
254
|
-
puts "⨠Tools demo completed!"
|
255
|
-
puts "\nTry running the chain with: ruby -I lib examples/quick_demo.rb"
|
93
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interfaces/agent'
|
4
|
+
|
5
|
+
module LLMChain
|
6
|
+
module Agents
|
7
|
+
# Factory for creating different types of agents
|
8
|
+
#
|
9
|
+
# Provides a centralized way to instantiate agents with proper configuration.
|
10
|
+
# Supports dependency injection and easy extension for new agent types.
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# agent = AgentFactory.create(type: :react, model: "qwen3:1.7b")
|
14
|
+
# result = agent.run("Analyze this data")
|
15
|
+
#
|
16
|
+
# @example With custom tools and memory
|
17
|
+
# agent = AgentFactory.create(
|
18
|
+
# type: :react,
|
19
|
+
# model: "gpt-4",
|
20
|
+
# tools: custom_tool_manager,
|
21
|
+
# memory: redis_memory
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
# @example Registering custom agent
|
25
|
+
# class MyCustomAgent < LLMChain::Interfaces::Agent
|
26
|
+
# # implementation
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# AgentFactory.register(:my_custom, MyCustomAgent)
|
30
|
+
# agent = AgentFactory.create(type: :my_custom)
|
31
|
+
class AgentFactory
|
32
|
+
@registry = {}
|
33
|
+
@descriptions = {}
|
34
|
+
|
35
|
+
# Register a custom agent class
|
36
|
+
# @param type [Symbol] agent type identifier
|
37
|
+
# @param agent_class [Class] agent class that implements LLMChain::Interfaces::Agent
|
38
|
+
# @param description [String] human-readable description of the agent
|
39
|
+
# @return [void]
|
40
|
+
def self.register(type, agent_class, description: nil)
|
41
|
+
unless agent_class.ancestors.include?(LLMChain::Interfaces::Agent)
|
42
|
+
raise ArgumentError, "Agent class must implement LLMChain::Interfaces::Agent"
|
43
|
+
end
|
44
|
+
|
45
|
+
@registry[type.to_sym] = agent_class
|
46
|
+
@descriptions[type.to_sym] = description || "Custom agent: #{agent_class.name}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Create an agent instance by type
|
50
|
+
# @param type [Symbol] the type of agent to create
|
51
|
+
# @param model [String] LLM model identifier
|
52
|
+
# @param tools [LLMChain::Interfaces::ToolManager] tool manager
|
53
|
+
# @param memory [LLMChain::Interfaces::Memory] memory backend
|
54
|
+
# @param max_iterations [Integer] maximum reasoning iterations (for ReAct)
|
55
|
+
# @param client_options [Hash] additional client options
|
56
|
+
# @return [LLMChain::Interfaces::Agent] agent instance
|
57
|
+
# @raise [ArgumentError] when agent type is unknown
|
58
|
+
def self.create(
|
59
|
+
type:,
|
60
|
+
model: nil,
|
61
|
+
tools: nil,
|
62
|
+
memory: nil,
|
63
|
+
max_iterations: 5,
|
64
|
+
**client_options
|
65
|
+
)
|
66
|
+
type_sym = type.to_sym
|
67
|
+
|
68
|
+
# Get registered agent class
|
69
|
+
agent_class = @registry[type_sym]
|
70
|
+
unless agent_class
|
71
|
+
raise ArgumentError, "Unknown agent type: #{type}. Supported types: #{supported_types.join(', ')}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Create agent instance with standard parameters
|
75
|
+
agent_class.new(
|
76
|
+
model: model || LLMChain.configuration.default_model,
|
77
|
+
tools: tools || LLMChain::Tools::ToolManagerFactory.create_default_toolset,
|
78
|
+
memory: memory || LLMChain::Memory::Array.new,
|
79
|
+
max_iterations: max_iterations,
|
80
|
+
**client_options
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get list of supported agent types
|
85
|
+
# @return [Array<Symbol>] supported agent types
|
86
|
+
def self.supported_types
|
87
|
+
@registry.keys
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if agent type is supported
|
91
|
+
# @param type [Symbol] agent type to check
|
92
|
+
# @return [Boolean] whether the type is supported
|
93
|
+
def self.supported?(type)
|
94
|
+
supported_types.include?(type.to_sym)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get description of available agent types
|
98
|
+
# @return [Hash<Symbol, String>] agent type descriptions
|
99
|
+
def self.agent_descriptions
|
100
|
+
@descriptions
|
101
|
+
end
|
102
|
+
|
103
|
+
# Unregister a custom agent
|
104
|
+
# @param type [Symbol] agent type to unregister
|
105
|
+
# @return [void]
|
106
|
+
def self.unregister(type)
|
107
|
+
type_sym = type.to_sym
|
108
|
+
@registry.delete(type_sym)
|
109
|
+
@descriptions.delete(type_sym)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get registered agent class
|
113
|
+
# @param type [Symbol] agent type
|
114
|
+
# @return [Class, nil] agent class or nil if not registered
|
115
|
+
def self.get_registered_agent(type)
|
116
|
+
@registry[type.to_sym]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,341 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interfaces/agent'
|
4
|
+
require_relative 'agent_factory'
|
5
|
+
require_relative 'planner_agent'
|
6
|
+
require_relative 'react_agent'
|
7
|
+
|
8
|
+
module LLMChain
|
9
|
+
module Agents
|
10
|
+
# Composite agent that combines planning and execution capabilities.
|
11
|
+
#
|
12
|
+
# This agent uses a PlannerAgent to decompose complex tasks into atomic steps,
|
13
|
+
# then uses a ReActAgent to execute each step. This provides better handling
|
14
|
+
# of multi-step tasks compared to using ReActAgent alone.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# agent = LLMChain::Agents::CompositeAgent.new(
|
18
|
+
# model: "qwen3:1.7b",
|
19
|
+
# tools: tool_manager
|
20
|
+
# )
|
21
|
+
# result = agent.run("Find the president of the US and the capital of France")
|
22
|
+
class CompositeAgent < LLMChain::Interfaces::Agent
|
23
|
+
attr_reader :model, :tools, :memory, :planner, :executor
|
24
|
+
|
25
|
+
# Initialize the composite agent with planning and execution capabilities.
|
26
|
+
# @param model [String] LLM model identifier
|
27
|
+
# @param tools [LLMChain::Interfaces::ToolManager] tool manager
|
28
|
+
# @param memory [LLMChain::Interfaces::Memory] memory backend
|
29
|
+
# @param max_iterations [Integer] maximum reasoning iterations for executor
|
30
|
+
# @param client_options [Hash] additional client options
|
31
|
+
def initialize(model:, tools:, memory: nil, max_iterations: 3, **client_options)
|
32
|
+
@model = model
|
33
|
+
@tools = tools
|
34
|
+
@memory = memory || LLMChain::Memory::Array.new
|
35
|
+
@max_iterations = max_iterations
|
36
|
+
|
37
|
+
# Create planner and executor agents through factory
|
38
|
+
@planner = AgentFactory.create(
|
39
|
+
type: :planner,
|
40
|
+
model: @model,
|
41
|
+
**client_options
|
42
|
+
)
|
43
|
+
@executor = AgentFactory.create(
|
44
|
+
type: :react,
|
45
|
+
model: @model,
|
46
|
+
tools: @tools,
|
47
|
+
memory: @memory,
|
48
|
+
max_iterations: @max_iterations,
|
49
|
+
**client_options
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Execute a task using planning and execution methodology.
|
54
|
+
# @param task [String] the task to accomplish
|
55
|
+
# @param stream [Boolean] whether to stream reasoning steps
|
56
|
+
# @yield [Hash] reasoning step information
|
57
|
+
# @return [Hash] final result with reasoning trace
|
58
|
+
def run(task, stream: false, &block)
|
59
|
+
# Step 1: Determine if task needs planning
|
60
|
+
use_planner = should_use_planner?(task)
|
61
|
+
|
62
|
+
if use_planner
|
63
|
+
# Use planning approach for complex tasks
|
64
|
+
run_with_planning(task, stream: stream, &block)
|
65
|
+
else
|
66
|
+
# Use direct execution for simple tasks
|
67
|
+
execute_directly(task, stream: stream, &block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if this agent can handle the given task.
|
72
|
+
# @param task [String] task description
|
73
|
+
# @return [Boolean] whether this agent can handle the task
|
74
|
+
def can_handle?(task)
|
75
|
+
# Composite agent can handle any task that the planner or executor can handle
|
76
|
+
@planner.can_handle?(task) || @executor.can_handle?(task)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get description of agent capabilities.
|
80
|
+
# @return [String] agent description
|
81
|
+
def description
|
82
|
+
"Composite agent with intelligent planning and execution capabilities for complex multi-step tasks"
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Determine if task should use planning approach
|
88
|
+
# @param task [String] the task to analyze
|
89
|
+
# @return [Boolean] whether to use planning
|
90
|
+
def should_use_planner?(task)
|
91
|
+
# Simple tasks that don't need planning
|
92
|
+
simple_patterns = [
|
93
|
+
/\bcalculate\s+\d+\s*[\+\-\*\/]\s*\d+\b/i,
|
94
|
+
/\bwhat\s+is\s+\d+\s*[\+\-\*\/]\s*\d+\b/i,
|
95
|
+
/\b\d+\s*[\+\-\*\/]\s*\d+\b/,
|
96
|
+
/\bwhat\s+time\s+is\s+it\b/i,
|
97
|
+
/\bcurrent\s+time\b/i,
|
98
|
+
/\bwhat\s+year\s+is\s+it\b/i,
|
99
|
+
/\bcurrent\s+date\b/i,
|
100
|
+
/\bwhat\s+time\b/i,
|
101
|
+
/\bcurrent\s+date\b/i
|
102
|
+
]
|
103
|
+
|
104
|
+
# Complex patterns that need planning
|
105
|
+
complex_patterns = [
|
106
|
+
/\band\b/i,
|
107
|
+
/\bthen\b/i,
|
108
|
+
/\bnext\b/i,
|
109
|
+
/\bfind\b.*\band\b/i,
|
110
|
+
/\bsearch\b.*\band\b/i,
|
111
|
+
/\bget\b.*\band\b/i,
|
112
|
+
/\bcalculate\b.*\band\b/i,
|
113
|
+
/\bwhat\s+is\b.*\band\b/i
|
114
|
+
]
|
115
|
+
|
116
|
+
# Check if task matches simple patterns
|
117
|
+
return false if simple_patterns.any? { |pattern| task.match?(pattern) }
|
118
|
+
|
119
|
+
# Check if task matches complex patterns
|
120
|
+
return true if complex_patterns.any? { |pattern| task.match?(pattern) }
|
121
|
+
|
122
|
+
# Default to planning for tasks longer than 50 characters
|
123
|
+
task.length > 50
|
124
|
+
end
|
125
|
+
|
126
|
+
# Run task with planning approach
|
127
|
+
# @param task [String] the task to accomplish
|
128
|
+
# @param stream [Boolean] whether to stream reasoning steps
|
129
|
+
# @yield [Hash] reasoning step information
|
130
|
+
# @return [Hash] final result with reasoning trace
|
131
|
+
def run_with_planning(task, stream: false, &block)
|
132
|
+
# Step 1: Plan - Decompose the task into atomic steps
|
133
|
+
planning_result = @planner.run(task, stream: stream, &block)
|
134
|
+
steps = planning_result[:steps] || [task]
|
135
|
+
|
136
|
+
# Step 2: Execute - Run each step with the executor
|
137
|
+
execution_results = []
|
138
|
+
validated_answers = []
|
139
|
+
|
140
|
+
steps.each_with_index do |step, index|
|
141
|
+
step_result = @executor.run(step, stream: stream, &block)
|
142
|
+
execution_results << step_result
|
143
|
+
|
144
|
+
# Validate and process the result
|
145
|
+
validated_answer = validate_and_process_result(step_result, step, index + 1)
|
146
|
+
validated_answers << validated_answer if validated_answer
|
147
|
+
|
148
|
+
# Yield step completion if streaming
|
149
|
+
if block_given? && stream
|
150
|
+
yield({
|
151
|
+
step: index + 1,
|
152
|
+
total_steps: steps.length,
|
153
|
+
current_step: step,
|
154
|
+
step_result: step_result,
|
155
|
+
validated_answer: validated_answer,
|
156
|
+
type: "step_completion"
|
157
|
+
})
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Step 3: Compile final result with smart aggregation
|
162
|
+
final_answer = smart_aggregate_results(validated_answers, task)
|
163
|
+
|
164
|
+
{
|
165
|
+
task: task,
|
166
|
+
final_answer: final_answer,
|
167
|
+
reasoning_trace: [
|
168
|
+
{
|
169
|
+
step: 1,
|
170
|
+
action: "plan",
|
171
|
+
action_input: task,
|
172
|
+
observation: "Decomposed into #{steps.length} steps: #{steps.join(', ')}"
|
173
|
+
},
|
174
|
+
*execution_results.flat_map { |result| result[:reasoning_trace] }
|
175
|
+
],
|
176
|
+
iterations: execution_results.sum { |result| result[:iterations] || 0 },
|
177
|
+
success: validate_overall_success(execution_results, validated_answers, task),
|
178
|
+
planning_result: planning_result,
|
179
|
+
execution_results: execution_results,
|
180
|
+
validated_answers: validated_answers
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
# Execute task directly without planning
|
185
|
+
# @param task [String] the task to accomplish
|
186
|
+
# @param stream [Boolean] whether to stream reasoning steps
|
187
|
+
# @yield [Hash] reasoning step information
|
188
|
+
# @return [Hash] final result with reasoning trace
|
189
|
+
def execute_directly(task, stream: false, &block)
|
190
|
+
result = @executor.run(task, stream: stream, &block)
|
191
|
+
|
192
|
+
{
|
193
|
+
task: task,
|
194
|
+
final_answer: result[:final_answer],
|
195
|
+
reasoning_trace: result[:reasoning_trace],
|
196
|
+
iterations: result[:iterations],
|
197
|
+
success: result[:success],
|
198
|
+
planning_result: { steps: [task] },
|
199
|
+
execution_results: [result],
|
200
|
+
validated_answers: [],
|
201
|
+
approach: "direct"
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
# Validate and process execution result
|
206
|
+
# @param result [Hash] execution result
|
207
|
+
# @param step [String] step description
|
208
|
+
# @param step_number [Integer] step number
|
209
|
+
# @return [Hash, nil] validated and processed result
|
210
|
+
def validate_and_process_result(result, step, step_number)
|
211
|
+
return nil unless result[:success] && result[:final_answer]
|
212
|
+
|
213
|
+
answer = result[:final_answer]
|
214
|
+
|
215
|
+
# Check for error indicators
|
216
|
+
error_indicators = [
|
217
|
+
"unable to complete",
|
218
|
+
"insufficient data",
|
219
|
+
"error",
|
220
|
+
"failed",
|
221
|
+
"please provide",
|
222
|
+
"no results found"
|
223
|
+
]
|
224
|
+
|
225
|
+
return nil if error_indicators.any? { |indicator| answer.downcase.include?(indicator) }
|
226
|
+
|
227
|
+
# Extract meaningful information based on step type
|
228
|
+
processed_answer = extract_meaningful_info(answer, step)
|
229
|
+
|
230
|
+
{
|
231
|
+
step_number: step_number,
|
232
|
+
step: step,
|
233
|
+
original_answer: answer,
|
234
|
+
processed_answer: processed_answer,
|
235
|
+
quality_score: calculate_quality_score(answer, step)
|
236
|
+
}
|
237
|
+
end
|
238
|
+
|
239
|
+
# Extract meaningful information from answer
|
240
|
+
# @param answer [String] original answer
|
241
|
+
# @param step [String] step description
|
242
|
+
# @return [String] processed answer
|
243
|
+
def extract_meaningful_info(answer, step)
|
244
|
+
# For mathematical calculations, extract the result
|
245
|
+
if step.match?(/\b(calculate|add|multiply|divide|subtract)\b/i)
|
246
|
+
# Look for numbers in the answer
|
247
|
+
numbers = answer.scan(/\d+/).map(&:to_i)
|
248
|
+
return numbers.last.to_s if numbers.any?
|
249
|
+
end
|
250
|
+
|
251
|
+
# For time/date queries, extract the formatted time
|
252
|
+
if step.match?(/\b(time|date|year)\b/i) && answer.include?("formatted")
|
253
|
+
# Extract the formatted time from JSON
|
254
|
+
if match = answer.match(/"formatted":\s*"([^"]+)"/)
|
255
|
+
return match[1]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# For web search results, extract the most relevant snippet
|
260
|
+
if answer.include?("Search results") && answer.include?("snippet")
|
261
|
+
# Extract first meaningful snippet
|
262
|
+
if match = answer.match(/"snippet":\s*"([^"]+)"/)
|
263
|
+
return match[1].gsub(/\.\.\./, '').strip
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Default: return first 100 characters
|
268
|
+
answer.length > 100 ? answer[0..100] + "..." : answer
|
269
|
+
end
|
270
|
+
|
271
|
+
# Calculate quality score for answer
|
272
|
+
# @param answer [String] answer to score
|
273
|
+
# @param step [String] step description
|
274
|
+
# @return [Integer] quality score (0-10)
|
275
|
+
def calculate_quality_score(answer, step)
|
276
|
+
score = 5 # Base score
|
277
|
+
|
278
|
+
# Penalize error indicators
|
279
|
+
error_indicators = ["unable to complete", "insufficient data", "error", "failed"]
|
280
|
+
score -= 3 if error_indicators.any? { |indicator| answer.downcase.include?(indicator) }
|
281
|
+
|
282
|
+
# Reward meaningful content
|
283
|
+
score += 2 if answer.length > 20
|
284
|
+
score += 1 if answer.match?(/\d+/)
|
285
|
+
score += 1 if answer.match?(/[A-Za-z]+/)
|
286
|
+
|
287
|
+
# Reward specific content types
|
288
|
+
score += 2 if step.match?(/\b(calculate|add|multiply)\b/i) && answer.match?(/\d+/)
|
289
|
+
score += 2 if step.match?(/\b(time|date)\b/i) && answer.include?("formatted")
|
290
|
+
score += 2 if step.match?(/\b(search|find)\b/i) && answer.include?("results")
|
291
|
+
|
292
|
+
[score, 10].min # Cap at 10
|
293
|
+
end
|
294
|
+
|
295
|
+
# Smart aggregation of results
|
296
|
+
# @param validated_answers [Array] validated answers
|
297
|
+
# @param original_task [String] original task
|
298
|
+
# @return [String] aggregated result
|
299
|
+
def smart_aggregate_results(validated_answers, original_task)
|
300
|
+
return "Task could not be completed successfully." if validated_answers.empty?
|
301
|
+
|
302
|
+
# For single answer, return it directly
|
303
|
+
if validated_answers.length == 1
|
304
|
+
return validated_answers.first[:processed_answer]
|
305
|
+
end
|
306
|
+
|
307
|
+
# For multiple answers, create a structured response
|
308
|
+
parts = []
|
309
|
+
validated_answers.each_with_index do |answer, index|
|
310
|
+
parts << "Part #{index + 1}: #{answer[:processed_answer]}"
|
311
|
+
end
|
312
|
+
|
313
|
+
# Add summary if it's a complex task
|
314
|
+
if original_task.match?(/\band\b/i)
|
315
|
+
parts << "\nSummary: All requested information has been gathered."
|
316
|
+
end
|
317
|
+
|
318
|
+
parts.join("\n\n")
|
319
|
+
end
|
320
|
+
|
321
|
+
# Validate overall success
|
322
|
+
# @param execution_results [Array] execution results
|
323
|
+
# @param validated_answers [Array] validated answers
|
324
|
+
# @param task [String] original task
|
325
|
+
# @return [Boolean] overall success
|
326
|
+
def validate_overall_success(execution_results, validated_answers, task)
|
327
|
+
# Check if we have any valid answers
|
328
|
+
return false if validated_answers.empty?
|
329
|
+
|
330
|
+
# For simple tasks, one good answer is enough
|
331
|
+
return true unless task.match?(/\band\b/i)
|
332
|
+
|
333
|
+
# For complex tasks, we need at least 50% of expected parts
|
334
|
+
expected_parts = task.scan(/\band\b/i).length + 1
|
335
|
+
actual_parts = validated_answers.length
|
336
|
+
|
337
|
+
actual_parts >= (expected_parts * 0.5).ceil
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|