llm_chain 0.4.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 -103
- data/examples/quick_demo.rb +93 -0
- data/examples/tools_example.rb +255 -0
- data/lib/llm_chain/chain.rb +19 -5
- data/lib/llm_chain/clients/qwen.rb +13 -1
- 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 +5 -0
- metadata +9 -2
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/llm_chain'
|
4
|
+
|
5
|
+
puts "🦾 LLMChain v#{LlmChain::VERSION} - Quick Demo"
|
6
|
+
puts "=" * 50
|
7
|
+
|
8
|
+
# 1. Simple chain without tools
|
9
|
+
puts "\n1. 💬 Simple conversation"
|
10
|
+
begin
|
11
|
+
simple_chain = LLMChain::Chain.new(
|
12
|
+
model: "qwen3:1.7b",
|
13
|
+
retriever: false
|
14
|
+
)
|
15
|
+
|
16
|
+
response = simple_chain.ask("Hello! Tell me briefly about yourself.")
|
17
|
+
puts "🤖 #{response}"
|
18
|
+
rescue => e
|
19
|
+
puts "❌ Error: #{e.message}"
|
20
|
+
puts "💡 Make sure Ollama is running and qwen3:1.7b model is downloaded"
|
21
|
+
end
|
22
|
+
|
23
|
+
# 2. Calculator
|
24
|
+
puts "\n2. 🧮 Built-in calculator"
|
25
|
+
calculator = LLMChain::Tools::Calculator.new
|
26
|
+
result = calculator.call("Calculate 25 * 8 + 15")
|
27
|
+
puts "📊 #{result[:formatted]}"
|
28
|
+
|
29
|
+
# 3. Code interpreter
|
30
|
+
puts "\n3. 💻 Code interpreter"
|
31
|
+
begin
|
32
|
+
interpreter = LLMChain::Tools::CodeInterpreter.new
|
33
|
+
ruby_code = <<~RUBY_CODE
|
34
|
+
```ruby
|
35
|
+
# Simple program
|
36
|
+
data = [1, 2, 3, 4, 5]
|
37
|
+
total = data.sum
|
38
|
+
puts "Sum of numbers: \#{total}"
|
39
|
+
puts "Average: \#{total / data.size.to_f}"
|
40
|
+
```
|
41
|
+
RUBY_CODE
|
42
|
+
|
43
|
+
code_result = interpreter.call(ruby_code)
|
44
|
+
|
45
|
+
if code_result.is_a?(Hash) && code_result[:result]
|
46
|
+
puts "✅ Execution result:"
|
47
|
+
puts code_result[:result]
|
48
|
+
elsif code_result.is_a?(Hash) && code_result[:error]
|
49
|
+
puts "❌ #{code_result[:error]}"
|
50
|
+
else
|
51
|
+
puts "📝 #{code_result}"
|
52
|
+
end
|
53
|
+
rescue => e
|
54
|
+
puts "❌ Interpreter error: #{e.message}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# 4. Web search (may not work without internet)
|
58
|
+
puts "\n4. 🔍 Web search"
|
59
|
+
search = LLMChain::Tools::WebSearch.new
|
60
|
+
search_result = search.call("Ruby programming language")
|
61
|
+
|
62
|
+
if search_result.is_a?(Hash) && search_result[:results] && !search_result[:results].empty?
|
63
|
+
puts "🌐 Found #{search_result[:count]} results:"
|
64
|
+
search_result[:results].first(2).each_with_index do |result, i|
|
65
|
+
puts " #{i+1}. #{result[:title]}"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
puts "❌ Search failed or no results found"
|
69
|
+
end
|
70
|
+
|
71
|
+
# 5. Chain with tools
|
72
|
+
puts "\n5. 🛠️ Chain with automatic tools"
|
73
|
+
begin
|
74
|
+
tool_manager = LLMChain::Tools::ToolManager.create_default_toolset
|
75
|
+
smart_chain = LLMChain::Chain.new(
|
76
|
+
model: "qwen3:1.7b",
|
77
|
+
tools: tool_manager,
|
78
|
+
retriever: false
|
79
|
+
)
|
80
|
+
|
81
|
+
puts "\n🧮 Math test:"
|
82
|
+
math_response = smart_chain.ask("How much is 12 * 15?")
|
83
|
+
puts "🤖 #{math_response}"
|
84
|
+
|
85
|
+
rescue => e
|
86
|
+
puts "❌ Tools error: #{e.message}"
|
87
|
+
end
|
88
|
+
|
89
|
+
puts "\n" + "=" * 50
|
90
|
+
puts "✨ Demo completed!"
|
91
|
+
puts "\n📖 More examples:"
|
92
|
+
puts " - ruby -I lib examples/tools_example.rb"
|
93
|
+
puts " - See README.md for complete documentation"
|
@@ -0,0 +1,255 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/llm_chain'
|
4
|
+
|
5
|
+
puts "🛠️ LLMChain Tools Demo"
|
6
|
+
puts "=" * 40
|
7
|
+
|
8
|
+
# 1. Individual Tool Usage
|
9
|
+
puts "\n1. 🧮 Calculator Tool"
|
10
|
+
calculator = LLMChain::Tools::Calculator.new
|
11
|
+
|
12
|
+
# Test mathematical expressions
|
13
|
+
examples = [
|
14
|
+
"15 * 7 + 3",
|
15
|
+
"sqrt(144)",
|
16
|
+
"2 * 2 * 2"
|
17
|
+
]
|
18
|
+
|
19
|
+
examples.each do |expr|
|
20
|
+
result = calculator.call("Calculate: #{expr}")
|
21
|
+
puts "📊 #{result[:formatted]}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# 2. Web Search Tool
|
25
|
+
puts "\n2. 🔍 Web Search Tool"
|
26
|
+
search = LLMChain::Tools::WebSearch.new
|
27
|
+
|
28
|
+
search_queries = [
|
29
|
+
"Ruby programming language",
|
30
|
+
"Machine learning basics"
|
31
|
+
]
|
32
|
+
|
33
|
+
search_queries.each do |query|
|
34
|
+
puts "\n🔍 Searching: #{query}"
|
35
|
+
result = search.call(query)
|
36
|
+
|
37
|
+
if result[:results] && result[:results].any?
|
38
|
+
puts "Found #{result[:count]} results:"
|
39
|
+
result[:results].first(2).each_with_index do |item, i|
|
40
|
+
puts " #{i+1}. #{item[:title]}"
|
41
|
+
puts " #{item[:url]}" if item[:url]
|
42
|
+
end
|
43
|
+
else
|
44
|
+
puts "No results found"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# 3. Code Interpreter Tool
|
49
|
+
puts "\n3. 💻 Code Interpreter Tool"
|
50
|
+
interpreter = LLMChain::Tools::CodeInterpreter.new
|
51
|
+
|
52
|
+
# Test Ruby code execution
|
53
|
+
ruby_examples = [
|
54
|
+
<<~RUBY,
|
55
|
+
```ruby
|
56
|
+
# Simple Fibonacci calculation
|
57
|
+
a, b = 0, 1
|
58
|
+
result = [a, b]
|
59
|
+
8.times do
|
60
|
+
c = a + b
|
61
|
+
result << c
|
62
|
+
a, b = b, c
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "Fibonacci sequence (first 10 numbers):"
|
66
|
+
puts result.join(" ")
|
67
|
+
```
|
68
|
+
RUBY
|
69
|
+
|
70
|
+
<<~RUBY
|
71
|
+
```ruby
|
72
|
+
# Data analysis example
|
73
|
+
numbers = [23, 45, 67, 89, 12, 34, 56, 78]
|
74
|
+
|
75
|
+
puts "Dataset: \#{numbers}"
|
76
|
+
puts "Sum: \#{numbers.sum}"
|
77
|
+
puts "Average: \#{numbers.sum.to_f / numbers.size}"
|
78
|
+
puts "Max: \#{numbers.max}"
|
79
|
+
puts "Min: \#{numbers.min}"
|
80
|
+
```
|
81
|
+
RUBY
|
82
|
+
]
|
83
|
+
|
84
|
+
ruby_examples.each_with_index do |code, i|
|
85
|
+
puts "\n💻 Ruby Example #{i+1}:"
|
86
|
+
begin
|
87
|
+
result = interpreter.call(code)
|
88
|
+
|
89
|
+
if result.is_a?(Hash) && result[:result]
|
90
|
+
puts "✅ Output:"
|
91
|
+
puts result[:result]
|
92
|
+
elsif result.is_a?(Hash) && result[:error]
|
93
|
+
puts "❌ Error: #{result[:error]}"
|
94
|
+
else
|
95
|
+
puts "📝 #{result}"
|
96
|
+
end
|
97
|
+
rescue => e
|
98
|
+
puts "❌ Execution error: #{e.message}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# 4. Tool Manager Usage
|
103
|
+
puts "\n4. 🎯 Tool Manager"
|
104
|
+
tool_manager = LLMChain::Tools::ToolManager.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::ToolManager.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"
|
data/lib/llm_chain/chain.rb
CHANGED
@@ -56,11 +56,21 @@ module LLMChain
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def process_tools(prompt)
|
59
|
-
@tools.
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
return {} if @tools.nil? || (@tools.respond_to?(:empty?) && @tools.empty?)
|
60
|
+
|
61
|
+
# Если @tools - это ToolManager
|
62
|
+
if @tools.respond_to?(:auto_execute)
|
63
|
+
@tools.auto_execute(prompt)
|
64
|
+
elsif @tools.is_a?(Array)
|
65
|
+
# Старая логика для массива инструментов
|
66
|
+
@tools.each_with_object({}) do |tool, acc|
|
67
|
+
if tool.match?(prompt)
|
68
|
+
response = tool.call(prompt)
|
69
|
+
acc[tool.name] = response unless response.nil?
|
70
|
+
end
|
63
71
|
end
|
72
|
+
else
|
73
|
+
{}
|
64
74
|
end
|
65
75
|
end
|
66
76
|
|
@@ -94,7 +104,11 @@ module LLMChain
|
|
94
104
|
def build_tool_responses(tool_responses)
|
95
105
|
parts = ["Tool results:"]
|
96
106
|
tool_responses.each do |name, response|
|
97
|
-
|
107
|
+
if response.is_a?(Hash) && response[:formatted]
|
108
|
+
parts << "#{name}: #{response[:formatted]}"
|
109
|
+
else
|
110
|
+
parts << "#{name}: #{response}"
|
111
|
+
end
|
98
112
|
end
|
99
113
|
parts.join("\n")
|
100
114
|
end
|
@@ -10,6 +10,12 @@ module LLMChain
|
|
10
10
|
default: "qwen:7b",
|
11
11
|
versions: ["qwen:7b", "qwen:14b", "qwen:72b", "qwen:0.5b"]
|
12
12
|
},
|
13
|
+
qwen2: {
|
14
|
+
default: "qwen2:1.5b",
|
15
|
+
versions: [
|
16
|
+
"qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b"
|
17
|
+
]
|
18
|
+
},
|
13
19
|
qwen3: {
|
14
20
|
default: "qwen3:latest",
|
15
21
|
versions: [
|
@@ -100,7 +106,13 @@ module LLMChain
|
|
100
106
|
private
|
101
107
|
|
102
108
|
def model_version
|
103
|
-
@model.start_with?('qwen3:')
|
109
|
+
if @model.start_with?('qwen3:')
|
110
|
+
:qwen3
|
111
|
+
elsif @model.start_with?('qwen2:')
|
112
|
+
:qwen2
|
113
|
+
else
|
114
|
+
:qwen
|
115
|
+
end
|
104
116
|
end
|
105
117
|
|
106
118
|
def detect_default_model
|
@@ -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
|