llm_chain 0.4.0 → 0.5.1

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.
@@ -0,0 +1,242 @@
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
+ # Ищем код после ключевых слов в той же строке (например, "Execute code: puts ...")
116
+ execute_match = prompt.match(/execute\s+code:\s*(.+)/i)
117
+ return execute_match[1].strip if execute_match
118
+
119
+ run_match = prompt.match(/run\s+code:\s*(.+)/i)
120
+ return run_match[1].strip if run_match
121
+
122
+ # Ищем код после ключевых слов в разных строках
123
+ KEYWORDS.each do |keyword|
124
+ if prompt.downcase.include?(keyword)
125
+ lines = prompt.split("\n")
126
+ keyword_line = lines.find_index { |line| line.downcase.include?(keyword) }
127
+ if keyword_line
128
+ # Берем строки после ключевого слова
129
+ code_lines = lines[(keyword_line + 1)..-1]
130
+ code = code_lines&.join("\n")&.strip
131
+ return code if code && !code.empty?
132
+ end
133
+ end
134
+ end
135
+
136
+ # Ищем строки, которые выглядят как код
137
+ code_lines = prompt.split("\n").select do |line|
138
+ line.strip.match?(/^(def|class|function|var|let|const|print|puts|console\.log)/i) ||
139
+ line.strip.match?(/^\w+\s*[=+\-*\/]\s*/) ||
140
+ line.strip.match?(/^\s*(if|for|while|return)[\s(]/i) ||
141
+ line.strip.match?(/puts\s+/) ||
142
+ line.strip.match?(/print\s*\(/)
143
+ end
144
+
145
+ code_lines.join("\n")
146
+ end
147
+
148
+ def detect_language(code, prompt)
149
+ # Явное указание языка
150
+ return 'ruby' if prompt.match?(/```ruby/i) || prompt.include?('Ruby')
151
+ return 'python' if prompt.match?(/```python/i) || prompt.include?('Python')
152
+ return 'javascript' if prompt.match?(/```(javascript|js)/i) || prompt.include?('JavaScript')
153
+
154
+ # Определение по синтаксису
155
+ return 'ruby' if code.include?('puts') || code.include?('def ') || code.match?(/\bend\b/)
156
+ return 'python' if code.include?('print(') || code.match?(/def \w+\(.*\):/) || code.include?('import ')
157
+ return 'javascript' if code.include?('console.log') || code.include?('function ') || code.include?('var ') || code.include?('let ')
158
+
159
+ 'ruby' # default
160
+ end
161
+
162
+ def safe_to_execute?(code)
163
+ DANGEROUS_PATTERNS.none? { |pattern| code.match?(pattern) }
164
+ end
165
+
166
+ def execute_code(code, language)
167
+ case language
168
+ when 'ruby'
169
+ execute_ruby(code)
170
+ when 'python'
171
+ execute_python(code)
172
+ when 'javascript'
173
+ execute_javascript(code)
174
+ else
175
+ raise "Unsupported language: #{language}"
176
+ end
177
+ end
178
+
179
+ def execute_ruby(code)
180
+ Timeout.timeout(@timeout) do
181
+ # Создаем временный файл
182
+ Tempfile.create(['code', '.rb']) do |file|
183
+ file.write(code)
184
+ file.flush
185
+
186
+ # Выполняем код в отдельном процессе
187
+ result = `ruby #{file.path} 2>&1`
188
+
189
+ if $?.success?
190
+ result.strip
191
+ else
192
+ raise "Ruby execution failed: #{result}"
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def execute_python(code)
199
+ Timeout.timeout(@timeout) do
200
+ Tempfile.create(['code', '.py']) do |file|
201
+ file.write(code)
202
+ file.flush
203
+
204
+ result = `python3 #{file.path} 2>&1`
205
+
206
+ if $?.success?
207
+ result.strip
208
+ else
209
+ raise "Python execution failed: #{result}"
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def execute_javascript(code)
216
+ Timeout.timeout(@timeout) do
217
+ Tempfile.create(['code', '.js']) do |file|
218
+ file.write(code)
219
+ file.flush
220
+
221
+ # Пробуем node.js
222
+ result = `node #{file.path} 2>&1`
223
+
224
+ if $?.success?
225
+ result.strip
226
+ else
227
+ raise "JavaScript execution failed: #{result}"
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def format_execution_result(code, language, result)
234
+ "Code execution (#{language}):\n\n```#{language}\n#{code}\n```\n\nOutput:\n```\n#{result}\n```"
235
+ end
236
+
237
+ def required_parameters
238
+ ['code']
239
+ end
240
+ end
241
+ end
242
+ 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