llm_chain 0.3.0 → 0.5.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/README.md +466 -101
- data/examples/quick_demo.rb +93 -0
- data/examples/tools_example.rb +255 -0
- data/lib/llm_chain/chain.rb +58 -42
- data/lib/llm_chain/client_registry.rb +2 -0
- data/lib/llm_chain/clients/gemma3.rb +144 -0
- data/lib/llm_chain/clients/qwen.rb +14 -3
- data/lib/llm_chain/tools/base_tool.rb +81 -0
- data/lib/llm_chain/tools/calculator.rb +143 -0
- data/lib/llm_chain/tools/code_interpreter.rb +233 -0
- data/lib/llm_chain/tools/tool_manager.rb +204 -0
- data/lib/llm_chain/tools/web_search.rb +255 -0
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +6 -0
- metadata +11 -3
@@ -0,0 +1,81 @@
|
|
1
|
+
module LLMChain
|
2
|
+
module Tools
|
3
|
+
class BaseTool
|
4
|
+
attr_reader :name, :description, :parameters
|
5
|
+
|
6
|
+
def initialize(name:, description:, parameters: {})
|
7
|
+
@name = name
|
8
|
+
@description = description
|
9
|
+
@parameters = parameters
|
10
|
+
end
|
11
|
+
|
12
|
+
# Проверяет, подходит ли инструмент для данного промпта
|
13
|
+
# @param prompt [String] Входной промпт от пользователя
|
14
|
+
# @return [Boolean] true если инструмент должен быть вызван
|
15
|
+
def match?(prompt)
|
16
|
+
raise NotImplementedError, "Subclasses must implement #match?"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Выполняет инструмент
|
20
|
+
# @param prompt [String] Входной промпт от пользователя
|
21
|
+
# @param context [Hash] Дополнительный контекст
|
22
|
+
# @return [String, Hash] Результат выполнения инструмента
|
23
|
+
def call(prompt, context: {})
|
24
|
+
raise NotImplementedError, "Subclasses must implement #call"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Возвращает JSON-схему для LLM
|
28
|
+
def to_schema
|
29
|
+
{
|
30
|
+
name: @name,
|
31
|
+
description: @description,
|
32
|
+
parameters: {
|
33
|
+
type: "object",
|
34
|
+
properties: @parameters,
|
35
|
+
required: required_parameters
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Извлекает параметры из промпта (для автоматического парсинга)
|
41
|
+
# @param prompt [String] Входной промпт
|
42
|
+
# @return [Hash] Извлеченные параметры
|
43
|
+
def extract_parameters(prompt)
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Форматирует результат для включения в промпт
|
48
|
+
# @param result [Object] Результат выполнения инструмента
|
49
|
+
# @return [String] Форматированный результат
|
50
|
+
def format_result(result)
|
51
|
+
case result
|
52
|
+
when String then result
|
53
|
+
when Hash, Array then JSON.pretty_generate(result)
|
54
|
+
else result.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# Список обязательных параметров
|
61
|
+
def required_parameters
|
62
|
+
[]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Помощник для проверки ключевых слов в промпте
|
66
|
+
def contains_keywords?(prompt, keywords)
|
67
|
+
keywords.any? { |keyword| prompt.downcase.include?(keyword.downcase) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Помощник для извлечения числовых значений
|
71
|
+
def extract_numbers(text)
|
72
|
+
text.scan(/-?\d+\.?\d*/).map(&:to_f)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Помощник для извлечения URL
|
76
|
+
def extract_urls(text)
|
77
|
+
text.scan(%r{https?://[^\s]+})
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module LLMChain
|
4
|
+
module Tools
|
5
|
+
class Calculator < BaseTool
|
6
|
+
KEYWORDS = %w[
|
7
|
+
calculate compute math equation formula
|
8
|
+
add subtract multiply divide
|
9
|
+
sum difference product quotient
|
10
|
+
plus minus times divided
|
11
|
+
+ - * / = equals
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
super(
|
16
|
+
name: "calculator",
|
17
|
+
description: "Performs mathematical calculations and evaluates expressions",
|
18
|
+
parameters: {
|
19
|
+
expression: {
|
20
|
+
type: "string",
|
21
|
+
description: "Mathematical expression to evaluate (e.g., '2 + 2', '15 * 3.14', 'sqrt(16)')"
|
22
|
+
}
|
23
|
+
}
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def match?(prompt)
|
28
|
+
contains_keywords?(prompt, KEYWORDS) ||
|
29
|
+
contains_math_pattern?(prompt)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(prompt, context: {})
|
33
|
+
expression = extract_expression(prompt)
|
34
|
+
return "No mathematical expression found" if expression.empty?
|
35
|
+
|
36
|
+
begin
|
37
|
+
result = evaluate_expression(expression)
|
38
|
+
{
|
39
|
+
expression: expression,
|
40
|
+
result: result,
|
41
|
+
formatted: "#{expression} = #{result}"
|
42
|
+
}
|
43
|
+
rescue => e
|
44
|
+
{
|
45
|
+
expression: expression,
|
46
|
+
error: e.message,
|
47
|
+
formatted: "Error calculating '#{expression}': #{e.message}"
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_parameters(prompt)
|
53
|
+
{ expression: extract_expression(prompt) }
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def contains_math_pattern?(prompt)
|
59
|
+
# Проверяем наличие математических операторов и чисел
|
60
|
+
prompt.match?(/\d+\s*[+\-*\/]\s*\d+/) ||
|
61
|
+
prompt.match?(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\(/i)
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_expression(prompt)
|
65
|
+
# Пробуем найти выражение в кавычках
|
66
|
+
quoted = prompt.match(/"([^"]+)"/) || prompt.match(/'([^']+)'/)
|
67
|
+
return quoted[1] if quoted
|
68
|
+
|
69
|
+
# Ищем выражение после ключевых слов
|
70
|
+
KEYWORDS.each do |keyword|
|
71
|
+
if prompt.downcase.include?(keyword)
|
72
|
+
escaped_keyword = Regexp.escape(keyword)
|
73
|
+
after_keyword = prompt.split(/#{escaped_keyword}/i, 2)[1]
|
74
|
+
if after_keyword
|
75
|
+
# Извлекаем математическое выражение
|
76
|
+
expr = after_keyword.strip.split(/[.!?]/).first
|
77
|
+
return clean_expression(expr) if expr
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Пробуем найти простое выражение в тексте
|
83
|
+
math_expr = prompt.match(/(\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-*\/]\s*\d+(?:\.\d+)?)*)/)
|
84
|
+
return math_expr[1] if math_expr
|
85
|
+
|
86
|
+
# Ищем функции
|
87
|
+
func_expr = prompt.match(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^)]+\)/i)
|
88
|
+
return func_expr[0] if func_expr
|
89
|
+
|
90
|
+
""
|
91
|
+
end
|
92
|
+
|
93
|
+
def clean_expression(expr)
|
94
|
+
# Удаляем лишние слова и оставляем только математическое выражение
|
95
|
+
expr.gsub(/\b(is|what|equals?|result|answer)\b/i, '')
|
96
|
+
.gsub(/[^\d+\-*\/().\s]/, '')
|
97
|
+
.strip
|
98
|
+
end
|
99
|
+
|
100
|
+
def evaluate_expression(expression)
|
101
|
+
# Заменяем математические функции на Ruby-методы
|
102
|
+
expr = expression.downcase
|
103
|
+
.gsub(/sqrt\s*\(([^)]+)\)/) { "Math.sqrt(#{$1})" }
|
104
|
+
.gsub(/sin\s*\(([^)]+)\)/) { "Math.sin(#{$1})" }
|
105
|
+
.gsub(/cos\s*\(([^)]+)\)/) { "Math.cos(#{$1})" }
|
106
|
+
.gsub(/tan\s*\(([^)]+)\)/) { "Math.tan(#{$1})" }
|
107
|
+
.gsub(/log\s*\(([^)]+)\)/) { "Math.log10(#{$1})" }
|
108
|
+
.gsub(/ln\s*\(([^)]+)\)/) { "Math.log(#{$1})" }
|
109
|
+
.gsub(/exp\s*\(([^)]+)\)/) { "Math.exp(#{$1})" }
|
110
|
+
.gsub(/abs\s*\(([^)]+)\)/) { "(#{$1}).abs" }
|
111
|
+
.gsub(/round\s*\(([^)]+)\)/) { "(#{$1}).round" }
|
112
|
+
.gsub(/ceil\s*\(([^)]+)\)/) { "(#{$1}).ceil" }
|
113
|
+
.gsub(/floor\s*\(([^)]+)\)/) { "(#{$1}).floor" }
|
114
|
+
|
115
|
+
# Безопасная оценка выражения
|
116
|
+
result = safe_eval(expr)
|
117
|
+
|
118
|
+
# Округляем результат до разумного количества знаков
|
119
|
+
if result.is_a?(Float)
|
120
|
+
result.round(10)
|
121
|
+
else
|
122
|
+
result
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def safe_eval(expression)
|
127
|
+
# Проверяем, что выражение содержит только безопасные символы
|
128
|
+
unless expression.match?(/\A[\d+\-*\/().\s]+\z/) ||
|
129
|
+
expression.include?('Math.') ||
|
130
|
+
expression.match?(/\.(abs|round|ceil|floor)\b/)
|
131
|
+
raise "Unsafe expression: #{expression}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Оцениваем выражение
|
135
|
+
eval(expression)
|
136
|
+
end
|
137
|
+
|
138
|
+
def required_parameters
|
139
|
+
['expression']
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module LLMChain
|
5
|
+
module Tools
|
6
|
+
class CodeInterpreter < BaseTool
|
7
|
+
KEYWORDS = %w[
|
8
|
+
code run execute script program
|
9
|
+
ruby python javascript
|
10
|
+
calculate compute
|
11
|
+
def class function
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
SUPPORTED_LANGUAGES = %w[ruby python javascript].freeze
|
15
|
+
|
16
|
+
DANGEROUS_PATTERNS = [
|
17
|
+
/system\s*\(/i,
|
18
|
+
/exec\s*\(/i,
|
19
|
+
/`[^`]*`/,
|
20
|
+
/File\.(delete|rm|unlink)/i,
|
21
|
+
/Dir\.(delete|rmdir)/i,
|
22
|
+
/require\s+['"]net\/http['"]/i,
|
23
|
+
/require\s+['"]open-uri['"]/i,
|
24
|
+
/eval\s*\(/i,
|
25
|
+
/instance_eval/i,
|
26
|
+
/class_eval/i
|
27
|
+
].freeze
|
28
|
+
|
29
|
+
def initialize(timeout: 30, allowed_languages: SUPPORTED_LANGUAGES)
|
30
|
+
@timeout = timeout
|
31
|
+
@allowed_languages = allowed_languages
|
32
|
+
|
33
|
+
super(
|
34
|
+
name: "code_interpreter",
|
35
|
+
description: "Executes code safely in an isolated environment",
|
36
|
+
parameters: {
|
37
|
+
code: {
|
38
|
+
type: "string",
|
39
|
+
description: "Code to execute"
|
40
|
+
},
|
41
|
+
language: {
|
42
|
+
type: "string",
|
43
|
+
description: "Programming language (ruby, python, javascript)",
|
44
|
+
enum: @allowed_languages
|
45
|
+
}
|
46
|
+
}
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def match?(prompt)
|
51
|
+
contains_keywords?(prompt, KEYWORDS) ||
|
52
|
+
contains_code_blocks?(prompt) ||
|
53
|
+
contains_function_definitions?(prompt)
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(prompt, context: {})
|
57
|
+
code = extract_code(prompt)
|
58
|
+
language = detect_language(code, prompt)
|
59
|
+
|
60
|
+
return "No code found to execute" if code.empty?
|
61
|
+
return "Unsupported language: #{language}" unless @allowed_languages.include?(language)
|
62
|
+
|
63
|
+
begin
|
64
|
+
if safe_to_execute?(code)
|
65
|
+
result = execute_code(code, language)
|
66
|
+
{
|
67
|
+
code: code,
|
68
|
+
language: language,
|
69
|
+
result: result,
|
70
|
+
formatted: format_execution_result(code, language, result)
|
71
|
+
}
|
72
|
+
else
|
73
|
+
{
|
74
|
+
code: code,
|
75
|
+
language: language,
|
76
|
+
error: "Code contains potentially dangerous operations",
|
77
|
+
formatted: "Cannot execute: Code contains potentially dangerous operations"
|
78
|
+
}
|
79
|
+
end
|
80
|
+
rescue => e
|
81
|
+
{
|
82
|
+
code: code,
|
83
|
+
language: language,
|
84
|
+
error: e.message,
|
85
|
+
formatted: "Execution error: #{e.message}"
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_parameters(prompt)
|
91
|
+
code = extract_code(prompt)
|
92
|
+
{
|
93
|
+
code: code,
|
94
|
+
language: detect_language(code, prompt)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def contains_code_blocks?(prompt)
|
101
|
+
prompt.include?('```') ||
|
102
|
+
prompt.match?(/^\s*def\s+\w+/m) ||
|
103
|
+
prompt.match?(/^\s*class\s+\w+/m)
|
104
|
+
end
|
105
|
+
|
106
|
+
def contains_function_definitions?(prompt)
|
107
|
+
prompt.match?(/\b(def|function|class)\s+\w+/i)
|
108
|
+
end
|
109
|
+
|
110
|
+
def extract_code(prompt)
|
111
|
+
# Ищем код в блоках ```
|
112
|
+
code_block = prompt.match(/```(?:ruby|python|javascript|js)?\s*\n(.*?)\n```/m)
|
113
|
+
return code_block[1].strip if code_block
|
114
|
+
|
115
|
+
# Ищем код после ключевых слов
|
116
|
+
KEYWORDS.each do |keyword|
|
117
|
+
if prompt.downcase.include?(keyword)
|
118
|
+
lines = prompt.split("\n")
|
119
|
+
keyword_line = lines.find_index { |line| line.downcase.include?(keyword) }
|
120
|
+
if keyword_line
|
121
|
+
# Берем строки после ключевого слова
|
122
|
+
code_lines = lines[(keyword_line + 1)..-1]
|
123
|
+
code = code_lines&.join("\n")&.strip
|
124
|
+
return code if code && !code.empty?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Ищем строки, которые выглядят как код
|
130
|
+
code_lines = prompt.split("\n").select do |line|
|
131
|
+
line.strip.match?(/^(def|class|function|var|let|const|print|puts|console\.log)/i) ||
|
132
|
+
line.strip.match?(/^\w+\s*[=+\-*\/]\s*/) ||
|
133
|
+
line.strip.match?(/^\s*(if|for|while|return)[\s(]/i)
|
134
|
+
end
|
135
|
+
|
136
|
+
code_lines.join("\n")
|
137
|
+
end
|
138
|
+
|
139
|
+
def detect_language(code, prompt)
|
140
|
+
# Явное указание языка
|
141
|
+
return 'ruby' if prompt.match?(/```ruby/i) || prompt.include?('Ruby')
|
142
|
+
return 'python' if prompt.match?(/```python/i) || prompt.include?('Python')
|
143
|
+
return 'javascript' if prompt.match?(/```(javascript|js)/i) || prompt.include?('JavaScript')
|
144
|
+
|
145
|
+
# Определение по синтаксису
|
146
|
+
return 'ruby' if code.include?('puts') || code.include?('def ') || code.match?(/\bend\b/)
|
147
|
+
return 'python' if code.include?('print(') || code.match?(/def \w+\(.*\):/) || code.include?('import ')
|
148
|
+
return 'javascript' if code.include?('console.log') || code.include?('function ') || code.include?('var ') || code.include?('let ')
|
149
|
+
|
150
|
+
'ruby' # default
|
151
|
+
end
|
152
|
+
|
153
|
+
def safe_to_execute?(code)
|
154
|
+
DANGEROUS_PATTERNS.none? { |pattern| code.match?(pattern) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def execute_code(code, language)
|
158
|
+
case language
|
159
|
+
when 'ruby'
|
160
|
+
execute_ruby(code)
|
161
|
+
when 'python'
|
162
|
+
execute_python(code)
|
163
|
+
when 'javascript'
|
164
|
+
execute_javascript(code)
|
165
|
+
else
|
166
|
+
raise "Unsupported language: #{language}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def execute_ruby(code)
|
171
|
+
Timeout.timeout(@timeout) do
|
172
|
+
# Создаем временный файл
|
173
|
+
Tempfile.create(['code', '.rb']) do |file|
|
174
|
+
file.write(code)
|
175
|
+
file.flush
|
176
|
+
|
177
|
+
# Выполняем код в отдельном процессе
|
178
|
+
result = `ruby #{file.path} 2>&1`
|
179
|
+
|
180
|
+
if $?.success?
|
181
|
+
result.strip
|
182
|
+
else
|
183
|
+
raise "Ruby execution failed: #{result}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def execute_python(code)
|
190
|
+
Timeout.timeout(@timeout) do
|
191
|
+
Tempfile.create(['code', '.py']) do |file|
|
192
|
+
file.write(code)
|
193
|
+
file.flush
|
194
|
+
|
195
|
+
result = `python3 #{file.path} 2>&1`
|
196
|
+
|
197
|
+
if $?.success?
|
198
|
+
result.strip
|
199
|
+
else
|
200
|
+
raise "Python execution failed: #{result}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def execute_javascript(code)
|
207
|
+
Timeout.timeout(@timeout) do
|
208
|
+
Tempfile.create(['code', '.js']) do |file|
|
209
|
+
file.write(code)
|
210
|
+
file.flush
|
211
|
+
|
212
|
+
# Пробуем node.js
|
213
|
+
result = `node #{file.path} 2>&1`
|
214
|
+
|
215
|
+
if $?.success?
|
216
|
+
result.strip
|
217
|
+
else
|
218
|
+
raise "JavaScript execution failed: #{result}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def format_execution_result(code, language, result)
|
225
|
+
"Code execution (#{language}):\n\n```#{language}\n#{code}\n```\n\nOutput:\n```\n#{result}\n```"
|
226
|
+
end
|
227
|
+
|
228
|
+
def required_parameters
|
229
|
+
['code']
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module LLMChain
|
2
|
+
module Tools
|
3
|
+
class ToolManager
|
4
|
+
attr_reader :tools
|
5
|
+
|
6
|
+
def initialize(tools: [])
|
7
|
+
@tools = {}
|
8
|
+
tools.each { |tool| register_tool(tool) }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Регистрирует новый инструмент
|
12
|
+
def register_tool(tool)
|
13
|
+
unless tool.is_a?(BaseTool)
|
14
|
+
raise ArgumentError, "Tool must inherit from BaseTool"
|
15
|
+
end
|
16
|
+
@tools[tool.name] = tool
|
17
|
+
end
|
18
|
+
|
19
|
+
# Удаляет инструмент
|
20
|
+
def unregister_tool(name)
|
21
|
+
@tools.delete(name.to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Получает инструмент по имени
|
25
|
+
def get_tool(name)
|
26
|
+
@tools[name.to_s]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Возвращает список всех инструментов
|
30
|
+
def list_tools
|
31
|
+
@tools.values
|
32
|
+
end
|
33
|
+
|
34
|
+
# Получает схемы всех инструментов для LLM
|
35
|
+
def get_tools_schema
|
36
|
+
@tools.values.map(&:to_schema)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Находит подходящие инструменты для промпта
|
40
|
+
def find_matching_tools(prompt)
|
41
|
+
@tools.values.select { |tool| tool.match?(prompt) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Выполняет все подходящие инструменты
|
45
|
+
def execute_tools(prompt, context: {})
|
46
|
+
matching_tools = find_matching_tools(prompt)
|
47
|
+
|
48
|
+
results = {}
|
49
|
+
matching_tools.each do |tool|
|
50
|
+
begin
|
51
|
+
result = tool.call(prompt, context: context)
|
52
|
+
results[tool.name] = {
|
53
|
+
success: true,
|
54
|
+
result: result,
|
55
|
+
formatted: tool.format_result(result)
|
56
|
+
}
|
57
|
+
rescue => e
|
58
|
+
results[tool.name] = {
|
59
|
+
success: false,
|
60
|
+
error: e.message,
|
61
|
+
formatted: "Error in #{tool.name}: #{e.message}"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
results
|
67
|
+
end
|
68
|
+
|
69
|
+
# Выполняет конкретный инструмент по имени
|
70
|
+
def execute_tool(name, prompt, context: {})
|
71
|
+
tool = get_tool(name)
|
72
|
+
raise ArgumentError, "Tool '#{name}' not found" unless tool
|
73
|
+
|
74
|
+
begin
|
75
|
+
result = tool.call(prompt, context: context)
|
76
|
+
{
|
77
|
+
success: true,
|
78
|
+
result: result,
|
79
|
+
formatted: tool.format_result(result)
|
80
|
+
}
|
81
|
+
rescue => e
|
82
|
+
{
|
83
|
+
success: false,
|
84
|
+
error: e.message,
|
85
|
+
formatted: "Error in #{name}: #{e.message}"
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Создает стандартный набор инструментов
|
91
|
+
def self.create_default_toolset
|
92
|
+
tools = [
|
93
|
+
Calculator.new,
|
94
|
+
WebSearch.new,
|
95
|
+
CodeInterpreter.new
|
96
|
+
]
|
97
|
+
|
98
|
+
new(tools: tools)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Создает набор инструментов из конфигурации
|
102
|
+
def self.from_config(config)
|
103
|
+
tools = []
|
104
|
+
|
105
|
+
config.each do |tool_config|
|
106
|
+
tool_class = tool_config[:class] || tool_config['class']
|
107
|
+
tool_options = tool_config[:options] || tool_config['options'] || {}
|
108
|
+
|
109
|
+
case tool_class.to_s.downcase
|
110
|
+
when 'calculator'
|
111
|
+
tools << Calculator.new
|
112
|
+
when 'web_search', 'websearch'
|
113
|
+
tools << WebSearch.new(**tool_options)
|
114
|
+
when 'code_interpreter', 'codeinterpreter'
|
115
|
+
tools << CodeInterpreter.new(**tool_options)
|
116
|
+
else
|
117
|
+
raise ArgumentError, "Unknown tool class: #{tool_class}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
new(tools: tools)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Форматирует результаты выполнения для включения в промпт
|
125
|
+
def format_tool_results(results)
|
126
|
+
return "" if results.empty?
|
127
|
+
|
128
|
+
formatted_results = results.map do |tool_name, result|
|
129
|
+
"#{tool_name}: #{result[:formatted]}"
|
130
|
+
end
|
131
|
+
|
132
|
+
"Tool Results:\n#{formatted_results.join("\n\n")}"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Получает краткое описание доступных инструментов
|
136
|
+
def tools_description
|
137
|
+
descriptions = @tools.values.map do |tool|
|
138
|
+
"- #{tool.name}: #{tool.description}"
|
139
|
+
end
|
140
|
+
|
141
|
+
"Available tools:\n#{descriptions.join("\n")}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Проверяет, содержит ли промпт запрос на использование инструментов
|
145
|
+
def needs_tools?(prompt)
|
146
|
+
# Проверяем явные запросы на использование инструментов
|
147
|
+
return true if prompt.match?(/\b(use tool|call tool|execute|calculate|search|run code)\b/i)
|
148
|
+
|
149
|
+
# Проверяем, есть ли подходящие инструменты
|
150
|
+
find_matching_tools(prompt).any?
|
151
|
+
end
|
152
|
+
|
153
|
+
# Автоматически решает, какие инструменты использовать
|
154
|
+
def auto_execute(prompt, context: {})
|
155
|
+
return {} unless needs_tools?(prompt)
|
156
|
+
|
157
|
+
# Ограничиваем количество одновременно выполняемых инструментов
|
158
|
+
matching_tools = find_matching_tools(prompt)
|
159
|
+
selected_tools = select_best_tools(matching_tools, prompt)
|
160
|
+
|
161
|
+
results = {}
|
162
|
+
selected_tools.each do |tool|
|
163
|
+
begin
|
164
|
+
result = tool.call(prompt, context: context)
|
165
|
+
results[tool.name] = {
|
166
|
+
success: true,
|
167
|
+
result: result,
|
168
|
+
formatted: tool.format_result(result)
|
169
|
+
}
|
170
|
+
rescue => e
|
171
|
+
results[tool.name] = {
|
172
|
+
success: false,
|
173
|
+
error: e.message,
|
174
|
+
formatted: "Error in #{tool.name}: #{e.message}"
|
175
|
+
}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
results
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# Выбирает лучшие инструменты для выполнения (ограничение по количеству)
|
185
|
+
def select_best_tools(tools, prompt, limit: 3)
|
186
|
+
# Простая логика приоритизации
|
187
|
+
prioritized = tools.sort_by do |tool|
|
188
|
+
case tool.name
|
189
|
+
when 'calculator'
|
190
|
+
prompt.include?('calculate') || prompt.match?(/\d+\s*[+\-*\/]\s*\d+/) ? 0 : 2
|
191
|
+
when 'web_search'
|
192
|
+
prompt.include?('search') || prompt.match?(/\b(what|who|where|when)\b/i) ? 0 : 2
|
193
|
+
when 'code_interpreter'
|
194
|
+
prompt.include?('```') || prompt.include?('code') ? 0 : 2
|
195
|
+
else
|
196
|
+
1
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
prioritized.first(limit)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|