llm_chain 0.5.4 → 0.5.5
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 +17 -2
- data/README.md +7 -6
- data/examples/quick_demo.rb +1 -1
- data/examples/tools_example.rb +2 -2
- data/exe/llm-chain +1 -1
- data/lib/llm_chain/builders/memory_context.rb +24 -0
- data/lib/llm_chain/builders/prompt.rb +26 -0
- data/lib/llm_chain/builders/rag_documents.rb +25 -0
- data/lib/llm_chain/builders/retriever_context.rb +25 -0
- data/lib/llm_chain/builders/tool_responses.rb +27 -0
- data/lib/llm_chain/chain.rb +68 -81
- data/lib/llm_chain/interfaces/builders/memory_context_builder.rb +20 -0
- data/lib/llm_chain/interfaces/builders/prompt_builder.rb +23 -0
- data/lib/llm_chain/interfaces/builders/rag_documents_builder.rb +20 -0
- data/lib/llm_chain/interfaces/builders/retriever_context_builder.rb +22 -0
- data/lib/llm_chain/interfaces/builders/tool_responses_builder.rb +20 -0
- data/lib/llm_chain/interfaces/memory.rb +38 -0
- data/lib/llm_chain/interfaces/tool_manager.rb +87 -0
- data/lib/llm_chain/memory/array.rb +18 -1
- data/lib/llm_chain/memory/redis.rb +20 -3
- data/lib/llm_chain/system_diagnostics.rb +73 -0
- data/lib/llm_chain/tools/calculator.rb +117 -44
- data/lib/llm_chain/tools/tool_manager.rb +32 -87
- data/lib/llm_chain/tools/tool_manager_factory.rb +44 -0
- data/lib/llm_chain/tools/web_search.rb +167 -335
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +55 -55
- metadata +16 -2
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLMChain
|
4
|
+
module Interfaces
|
5
|
+
# Abstract interface for tool management in LLMChain.
|
6
|
+
# Implementations must provide methods for registering, finding, and executing tools.
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
class ToolManager
|
10
|
+
# Register a new tool instance.
|
11
|
+
# @param tool [LLMChain::Tools::Base]
|
12
|
+
# @return [void]
|
13
|
+
def register_tool(tool)
|
14
|
+
raise NotImplementedError, "Implement in subclass"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Unregister a tool by name.
|
18
|
+
# @param name [String]
|
19
|
+
# @return [void]
|
20
|
+
def unregister_tool(name)
|
21
|
+
raise NotImplementedError, "Implement in subclass"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetch a tool by its name.
|
25
|
+
# @param name [String]
|
26
|
+
# @return [LLMChain::Tools::Base, nil]
|
27
|
+
def get_tool(name)
|
28
|
+
raise NotImplementedError, "Implement in subclass"
|
29
|
+
end
|
30
|
+
|
31
|
+
# List all registered tools.
|
32
|
+
# @return [Array<LLMChain::Tools::Base>]
|
33
|
+
def list_tools
|
34
|
+
raise NotImplementedError, "Implement in subclass"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Find tools whose #match? returns true for the prompt.
|
38
|
+
# @param prompt [String]
|
39
|
+
# @return [Array<LLMChain::Tools::Base>]
|
40
|
+
def find_matching_tools(prompt)
|
41
|
+
raise NotImplementedError, "Implement in subclass"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Execute every matching tool and collect results.
|
45
|
+
# @param prompt [String]
|
46
|
+
# @param context [Hash]
|
47
|
+
# @return [Hash] mapping tool name → result hash
|
48
|
+
def execute_tools(prompt, context: {})
|
49
|
+
raise NotImplementedError, "Implement in subclass"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Format tool execution results for inclusion into an LLM prompt.
|
53
|
+
# @param results [Hash]
|
54
|
+
# @return [String]
|
55
|
+
def format_tool_results(results)
|
56
|
+
raise NotImplementedError, "Implement in subclass"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Human-readable list of available tools.
|
60
|
+
# @return [String]
|
61
|
+
def tools_description
|
62
|
+
raise NotImplementedError, "Implement in subclass"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Determine if prompt likely needs tool usage.
|
66
|
+
# @param prompt [String]
|
67
|
+
# @return [Boolean]
|
68
|
+
def needs_tools?(prompt)
|
69
|
+
raise NotImplementedError, "Implement in subclass"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Auto-select and execute best tools for prompt.
|
73
|
+
# @param prompt [String]
|
74
|
+
# @param context [Hash]
|
75
|
+
# @return [Hash]
|
76
|
+
def auto_execute(prompt, context: {})
|
77
|
+
raise NotImplementedError, "Implement in subclass"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Build JSON schemas for all registered tools.
|
81
|
+
# @return [Array<Hash>]
|
82
|
+
def get_tools_schema
|
83
|
+
raise NotImplementedError, "Implement in subclass"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -1,24 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interfaces/memory'
|
4
|
+
|
1
5
|
module LLMChain
|
2
6
|
module Memory
|
3
|
-
|
7
|
+
# In-memory array-based memory adapter for LLMChain.
|
8
|
+
# Stores conversation history in a simple Ruby array.
|
9
|
+
class Array < Interfaces::Memory
|
4
10
|
def initialize(max_size: 10)
|
5
11
|
@storage = []
|
6
12
|
@max_size = max_size
|
7
13
|
end
|
8
14
|
|
15
|
+
# Store a prompt/response pair in memory.
|
16
|
+
# @param prompt [String]
|
17
|
+
# @param response [String]
|
18
|
+
# @return [void]
|
9
19
|
def store(prompt, response)
|
10
20
|
@storage << { prompt: prompt, response: response }
|
11
21
|
@storage.shift if @storage.size > @max_size
|
12
22
|
end
|
13
23
|
|
24
|
+
# Recall conversation history (optionally filtered by prompt).
|
25
|
+
# @param prompt [String, nil]
|
26
|
+
# @return [Array<Hash>]
|
14
27
|
def recall(_ = nil)
|
15
28
|
@storage.dup
|
16
29
|
end
|
17
30
|
|
31
|
+
# Clear all memory.
|
32
|
+
# @return [void]
|
18
33
|
def clear
|
19
34
|
@storage.clear
|
20
35
|
end
|
21
36
|
|
37
|
+
# Return number of stored items.
|
38
|
+
# @return [Integer]
|
22
39
|
def size
|
23
40
|
@storage.size
|
24
41
|
end
|
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interfaces/memory'
|
4
|
+
|
1
5
|
require 'redis'
|
2
6
|
require 'json'
|
3
7
|
|
4
8
|
module LLMChain
|
5
9
|
module Memory
|
6
|
-
|
10
|
+
# Redis-based memory adapter for LLMChain.
|
11
|
+
# Stores conversation history in a Redis list.
|
12
|
+
class Redis < Interfaces::Memory
|
7
13
|
class Error < StandardError; end
|
8
14
|
|
9
15
|
def initialize(max_size: 10, redis_url: nil, namespace: 'llm_chain')
|
@@ -13,27 +19,38 @@ module LLMChain
|
|
13
19
|
@session_key = "#{namespace}:session"
|
14
20
|
end
|
15
21
|
|
22
|
+
# Store a prompt/response pair in memory.
|
23
|
+
# @param prompt [String]
|
24
|
+
# @param response [String]
|
25
|
+
# @return [void]
|
16
26
|
def store(prompt, response)
|
17
27
|
entry = { prompt: prompt, response: response, timestamp: Time.now.to_i }.to_json
|
18
28
|
@redis.multi do
|
19
29
|
@redis.rpush(@session_key, entry)
|
20
|
-
@redis.ltrim(@session_key, -@max_size, -1)
|
30
|
+
@redis.ltrim(@session_key, -@max_size, -1)
|
21
31
|
end
|
22
32
|
end
|
23
33
|
|
34
|
+
# Recall conversation history (optionally filtered by prompt).
|
35
|
+
# @param prompt [String, nil]
|
36
|
+
# @return [Array<Hash>]
|
24
37
|
def recall(_ = nil)
|
25
38
|
entries = @redis.lrange(@session_key, 0, -1)
|
26
39
|
entries.map { |e| symbolize_keys(JSON.parse(e)) }
|
27
40
|
rescue JSON::ParserError
|
28
41
|
[]
|
29
42
|
rescue ::Redis::CannotConnectError
|
30
|
-
raise
|
43
|
+
raise Error, "Cannot connect to Redis server"
|
31
44
|
end
|
32
45
|
|
46
|
+
# Clear all memory.
|
47
|
+
# @return [void]
|
33
48
|
def clear
|
34
49
|
@redis.del(@session_key)
|
35
50
|
end
|
36
51
|
|
52
|
+
# Return number of stored items.
|
53
|
+
# @return [Integer]
|
37
54
|
def size
|
38
55
|
@redis.llen(@session_key)
|
39
56
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLMChain
|
4
|
+
# System diagnostics utility for checking LLMChain environment
|
5
|
+
class SystemDiagnostics
|
6
|
+
DIAGNOSTICS_HEADER = "🔍 LLMChain System Diagnostics"
|
7
|
+
SEPARATOR = "=" * 50
|
8
|
+
|
9
|
+
def self.run
|
10
|
+
new.run
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
puts_header
|
15
|
+
results = ConfigurationValidator.validate_environment
|
16
|
+
display_results(results)
|
17
|
+
display_recommendations(results)
|
18
|
+
puts_footer
|
19
|
+
results
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def puts_header
|
25
|
+
puts DIAGNOSTICS_HEADER
|
26
|
+
puts SEPARATOR
|
27
|
+
end
|
28
|
+
|
29
|
+
def puts_footer
|
30
|
+
puts SEPARATOR
|
31
|
+
end
|
32
|
+
|
33
|
+
def display_results(results)
|
34
|
+
display_system_components(results)
|
35
|
+
display_api_keys(results)
|
36
|
+
display_warnings(results)
|
37
|
+
end
|
38
|
+
|
39
|
+
def display_system_components(results)
|
40
|
+
puts "\n📋 System Components:"
|
41
|
+
puts " Ruby: #{status_icon(results[:ruby])} (#{RUBY_VERSION})"
|
42
|
+
puts " Python: #{status_icon(results[:python])}"
|
43
|
+
puts " Node.js: #{status_icon(results[:node])}"
|
44
|
+
puts " Internet: #{status_icon(results[:internet])}"
|
45
|
+
puts " Ollama: #{status_icon(results[:ollama])}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def display_api_keys(results)
|
49
|
+
puts "\n🔑 API Keys:"
|
50
|
+
results[:apis].each do |api, available|
|
51
|
+
puts " #{api.to_s.capitalize}: #{status_icon(available)}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def display_warnings(results)
|
56
|
+
return unless results[:warnings].any?
|
57
|
+
|
58
|
+
puts "\n⚠️ Warnings:"
|
59
|
+
results[:warnings].each { |warning| puts " • #{warning}" }
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_recommendations(results)
|
63
|
+
puts "\n💡 Recommendations:"
|
64
|
+
puts " • Install missing components for full functionality"
|
65
|
+
puts " • Configure API keys for enhanced features"
|
66
|
+
puts " • Start Ollama server: ollama serve" unless results[:ollama]
|
67
|
+
end
|
68
|
+
|
69
|
+
def status_icon(status)
|
70
|
+
status ? '✅' : '❌'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -2,6 +2,13 @@ require 'bigdecimal'
|
|
2
2
|
|
3
3
|
module LLMChain
|
4
4
|
module Tools
|
5
|
+
# Performs mathematical calculations and evaluates expressions.
|
6
|
+
# Supports basic arithmetic and common math functions (sqrt, sin, cos, etc).
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# calculator = LLMChain::Tools::Calculator.new
|
10
|
+
# calculator.call('What is sqrt(16) + 2 * 3?')
|
11
|
+
# # => { expression: 'sqrt(16) + 2 * 3', result: 10.0, formatted: 'sqrt(16) + 2 * 3 = 10.0' }
|
5
12
|
class Calculator < Base
|
6
13
|
KEYWORDS = %w[
|
7
14
|
calculate compute math equation formula
|
@@ -11,6 +18,22 @@ module LLMChain
|
|
11
18
|
+ - * / = equals
|
12
19
|
].freeze
|
13
20
|
|
21
|
+
# Regex patterns for expression extraction and validation
|
22
|
+
FUNC_EXPR_PATTERN = /\w+\([^\)]+\)(?:\s*[+\-\*\/]\s*(?:-?\d+(?:\.\d+)?|\w+\([^\)]+\)|\([^\)]+\))*)*/.freeze
|
23
|
+
MATH_EXPR_PATTERN = /((?:-?\d+(?:\.\d+)?|\w+\([^\(\)]+\)|\([^\(\)]+\))\s*(?:[+\-\*\/]\s*(?:-?\d+(?:\.\d+)?|\w+\([^\(\)]+\)|\([^\(\)]+\))\s*)+)/.freeze
|
24
|
+
QUOTED_EXPR_PATTERN = /"([^"]+)"|'([^']+)'/.freeze
|
25
|
+
SIMPLE_MATH_PATTERN = /(\d+(?:\.\d+)?\s*[+\-\*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-\*\/]\s*\d+(?:\.\d+)?)*)/.freeze
|
26
|
+
FUNC_CALL_PATTERN = /\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^\)]+\)/i.freeze
|
27
|
+
MATH_OPERATOR_PATTERN = /\d+\s*[+\-\*\/]\s*\d+/.freeze
|
28
|
+
MATH_FUNCTION_PATTERN = /\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\(/i.freeze
|
29
|
+
CLEAN_EXTRA_WORDS_PATTERN = /\b(is|what|equals?|result|answer|the)\b/i.freeze
|
30
|
+
CLEAN_NON_MATH_PATTERN = /[^\d+\-*\/().\s]/.freeze
|
31
|
+
MULTIPLE_SPACES_PATTERN = /\s+/.freeze
|
32
|
+
VALID_MATH_EXPRESSION_PATTERN = /\d+(?:\.\d+)?\s*[+\-\*\/]\s*\d+(?:\.\d+)?/.freeze
|
33
|
+
SAFE_EVAL_PATTERN = /\A[\d+\-*\/().\s]+\z/.freeze
|
34
|
+
SAFE_EVAL_METHOD_PATTERN = /\.(abs|round|ceil|floor)\b/.freeze
|
35
|
+
|
36
|
+
# Initializes the calculator tool.
|
14
37
|
def initialize
|
15
38
|
super(
|
16
39
|
name: "calculator",
|
@@ -24,14 +47,21 @@ module LLMChain
|
|
24
47
|
)
|
25
48
|
end
|
26
49
|
|
50
|
+
# Checks if the prompt contains a mathematical expression or keyword.
|
51
|
+
# @param prompt [String]
|
52
|
+
# @return [Boolean]
|
27
53
|
def match?(prompt)
|
28
54
|
contains_keywords?(prompt, KEYWORDS) ||
|
29
55
|
contains_math_pattern?(prompt)
|
30
56
|
end
|
31
57
|
|
58
|
+
# Evaluates a mathematical expression found in the prompt.
|
59
|
+
# @param prompt [String]
|
60
|
+
# @param context [Hash]
|
61
|
+
# @return [Hash] result, expression, and formatted string; or error info
|
32
62
|
def call(prompt, context: {})
|
33
63
|
expression = extract_expression(prompt)
|
34
|
-
return "No mathematical expression found" if expression.empty?
|
64
|
+
return { error: "No mathematical expression found" } if expression.empty?
|
35
65
|
|
36
66
|
begin
|
37
67
|
result = evaluate_expression(expression)
|
@@ -49,38 +79,76 @@ module LLMChain
|
|
49
79
|
end
|
50
80
|
end
|
51
81
|
|
82
|
+
# Extracts the parameters for the tool from the prompt.
|
83
|
+
# @param prompt [String]
|
84
|
+
# @return [Hash]
|
52
85
|
def extract_parameters(prompt)
|
53
86
|
{ expression: extract_expression(prompt) }
|
54
87
|
end
|
55
88
|
|
56
89
|
private
|
57
90
|
|
91
|
+
# Checks if the prompt contains a math pattern (numbers and operators or functions).
|
92
|
+
# @param prompt [String]
|
93
|
+
# @return [Boolean]
|
58
94
|
def contains_math_pattern?(prompt)
|
59
|
-
|
60
|
-
prompt.match?(
|
61
|
-
prompt.match?(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\(/i)
|
95
|
+
prompt.match?(MATH_OPERATOR_PATTERN) ||
|
96
|
+
prompt.match?(MATH_FUNCTION_PATTERN)
|
62
97
|
end
|
63
98
|
|
99
|
+
# Extracts a mathematical expression from the prompt using multiple strategies.
|
100
|
+
# @param prompt [String]
|
101
|
+
# @return [String]
|
64
102
|
def extract_expression(prompt)
|
65
|
-
|
66
|
-
|
67
|
-
return
|
103
|
+
extract_math_expression(prompt).tap { |expr| return expr unless expr.empty? }
|
104
|
+
extract_quoted_expression(prompt).tap { |expr| return expr unless expr.empty? }
|
105
|
+
extract_simple_math_expression(prompt).tap { |expr| return expr unless expr.empty? }
|
106
|
+
extract_function_call(prompt).tap { |expr| return expr unless expr.empty? }
|
107
|
+
extract_keyword_expression(prompt).tap { |expr| return expr unless expr.empty? }
|
108
|
+
""
|
109
|
+
end
|
110
|
+
|
111
|
+
# Extracts a complex math expression (numbers, functions, operators, spaces) from the prompt.
|
112
|
+
def extract_math_expression(prompt)
|
113
|
+
# First, try to match an expression starting with a function call and any number of operator+operand pairs
|
114
|
+
func_expr = prompt.match(FUNC_EXPR_PATTERN)
|
115
|
+
if func_expr
|
116
|
+
expr = func_expr[0].strip.gsub(/[.?!]$/, '')
|
117
|
+
puts "[DEBUG_CALC] Extracted math expression (func first): '#{expr}' from prompt: '#{prompt}'" if ENV['DEBUG_CALC']
|
118
|
+
return expr
|
119
|
+
end
|
120
|
+
# Fallback to previous logic
|
121
|
+
match = prompt.match(MATH_EXPR_PATTERN)
|
122
|
+
expr = match ? match[1].strip.gsub(/[.?!]$/, '') : ""
|
123
|
+
puts "[DEBUG_CALC] Extracted math expression: '#{expr}' from prompt: '#{prompt}'" if ENV['DEBUG_CALC']
|
124
|
+
expr
|
125
|
+
end
|
126
|
+
|
127
|
+
# Extracts an expression in quotes from the prompt.
|
128
|
+
def extract_quoted_expression(prompt)
|
129
|
+
quoted = prompt.match(QUOTED_EXPR_PATTERN)
|
130
|
+
quoted ? (quoted[1] || quoted[2]) : ""
|
131
|
+
end
|
68
132
|
|
69
|
-
|
70
|
-
|
71
|
-
|
133
|
+
# Extracts a simple math expression (e.g., 2 + 2) from the prompt.
|
134
|
+
def extract_simple_math_expression(prompt)
|
135
|
+
math_expr = prompt.match(SIMPLE_MATH_PATTERN)
|
136
|
+
math_expr ? math_expr[1].strip : ""
|
137
|
+
end
|
72
138
|
|
73
|
-
|
74
|
-
|
75
|
-
|
139
|
+
# Extracts a function call (e.g., sqrt(16)) from the prompt.
|
140
|
+
def extract_function_call(prompt)
|
141
|
+
func_expr = prompt.match(FUNC_CALL_PATTERN)
|
142
|
+
func_expr ? func_expr[0] : ""
|
143
|
+
end
|
76
144
|
|
77
|
-
|
145
|
+
# Extracts an expression after a math keyword from the prompt.
|
146
|
+
def extract_keyword_expression(prompt)
|
78
147
|
KEYWORDS.each do |keyword|
|
79
148
|
if prompt.downcase.include?(keyword)
|
80
149
|
escaped_keyword = Regexp.escape(keyword)
|
81
150
|
after_keyword = prompt.split(/#{escaped_keyword}/i, 2)[1]
|
82
151
|
if after_keyword
|
83
|
-
# Извлекаем математическое выражение
|
84
152
|
expr = after_keyword.strip.split(/[.!?]/).first
|
85
153
|
if expr
|
86
154
|
cleaned = clean_expression(expr)
|
@@ -89,44 +157,47 @@ module LLMChain
|
|
89
157
|
end
|
90
158
|
end
|
91
159
|
end
|
92
|
-
|
93
160
|
""
|
94
161
|
end
|
95
162
|
|
163
|
+
# Cleans up a candidate expression, removing extra words and keeping only numbers and operators.
|
164
|
+
# @param expr [String]
|
165
|
+
# @return [String]
|
96
166
|
def clean_expression(expr)
|
97
|
-
|
98
|
-
|
99
|
-
.gsub(
|
100
|
-
.gsub(/\s+/, ' ') # убираем множественные пробелы
|
167
|
+
cleaned = expr.gsub(CLEAN_EXTRA_WORDS_PATTERN, '')
|
168
|
+
.gsub(CLEAN_NON_MATH_PATTERN, ' ')
|
169
|
+
.gsub(MULTIPLE_SPACES_PATTERN, ' ')
|
101
170
|
.strip
|
102
|
-
|
103
|
-
# Проверяем что результат похож на математическое выражение
|
104
|
-
if cleaned.match?(/\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?/)
|
171
|
+
if cleaned.match?(VALID_MATH_EXPRESSION_PATTERN)
|
105
172
|
cleaned
|
106
173
|
else
|
107
174
|
""
|
108
175
|
end
|
109
176
|
end
|
110
177
|
|
178
|
+
# Evaluates a mathematical expression, supporting common math functions.
|
179
|
+
# @param expression [String]
|
180
|
+
# @return [Numeric]
|
111
181
|
def evaluate_expression(expression)
|
112
|
-
# Заменяем математические функции на Ruby-методы
|
113
182
|
expr = expression.downcase
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
183
|
+
max_iterations = 10
|
184
|
+
max_iterations.times do
|
185
|
+
before = expr.dup
|
186
|
+
expr.gsub!(/(?<!Math\.)sqrt\s*\((.*?)\)/, 'Math.sqrt(\1)')
|
187
|
+
expr.gsub!(/(?<!Math\.)sin\s*\((.*?)\)/, 'Math.sin(\1)')
|
188
|
+
expr.gsub!(/(?<!Math\.)cos\s*\((.*?)\)/, 'Math.cos(\1)')
|
189
|
+
expr.gsub!(/(?<!Math\.)tan\s*\((.*?)\)/, 'Math.tan(\1)')
|
190
|
+
expr.gsub!(/(?<!Math\.)log\s*\((.*?)\)/, 'Math.log10(\1)')
|
191
|
+
expr.gsub!(/(?<!Math\.)ln\s*\((.*?)\)/, 'Math.log(\1)')
|
192
|
+
expr.gsub!(/(?<!Math\.)exp\s*\((.*?)\)/, 'Math.exp(\1)')
|
193
|
+
expr.gsub!(/(?<!\.)abs\s*\((.*?)\)/, '(\1).abs')
|
194
|
+
expr.gsub!(/(?<!\.)round\s*\((.*?)\)/, '(\1).round')
|
195
|
+
expr.gsub!(/(?<!\.)ceil\s*\((.*?)\)/, '(\1).ceil')
|
196
|
+
expr.gsub!(/(?<!\.)floor\s*\((.*?)\)/, '(\1).floor')
|
197
|
+
break if expr == before
|
198
|
+
end
|
199
|
+
puts "[DEBUG_CALC] Final eval expression: '#{expr}'" if ENV['DEBUG_CALC']
|
127
200
|
result = safe_eval(expr)
|
128
|
-
|
129
|
-
# Округляем результат до разумного количества знаков
|
130
201
|
if result.is_a?(Float)
|
131
202
|
result.round(10)
|
132
203
|
else
|
@@ -134,18 +205,20 @@ module LLMChain
|
|
134
205
|
end
|
135
206
|
end
|
136
207
|
|
208
|
+
# Safely evaluates a mathematical expression.
|
209
|
+
# Only allows numbers, operators, parentheses, and supported Math methods.
|
210
|
+
# @param expression [String]
|
211
|
+
# @return [Numeric]
|
137
212
|
def safe_eval(expression)
|
138
|
-
|
139
|
-
unless expression.match?(/\A[\d+\-*\/().\s]+\z/) ||
|
213
|
+
unless expression.match?(SAFE_EVAL_PATTERN) ||
|
140
214
|
expression.include?('Math.') ||
|
141
|
-
expression.match?(
|
215
|
+
expression.match?(SAFE_EVAL_METHOD_PATTERN)
|
142
216
|
raise "Unsafe expression: #{expression}"
|
143
217
|
end
|
144
|
-
|
145
|
-
# Оцениваем выражение
|
146
218
|
eval(expression)
|
147
219
|
end
|
148
220
|
|
221
|
+
# @return [Array<String>]
|
149
222
|
def required_parameters
|
150
223
|
['expression']
|
151
224
|
end
|