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.
@@ -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
- class Array
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
- class Redis
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) # Сохраняем только последние max_size записей
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 MemoryError, "Cannot connect to Redis server"
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?(/\d+\s*[+\-*\/]\s*\d+/) ||
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
- quoted = prompt.match(/"([^"]+)"/) || prompt.match(/'([^']+)'/)
67
- return quoted[1] if quoted
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
- math_expr = prompt.match(/(\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-*\/]\s*\d+(?:\.\d+)?)*)/)
71
- return math_expr[1].strip if math_expr
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
- func_expr = prompt.match(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^)]+\)/i)
75
- return func_expr[0] if func_expr
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
- cleaned = expr.gsub(/\b(is|what|equals?|result|answer|the)\b/i, '')
99
- .gsub(/[^\d+\-*\/().\s]/, ' ') # заменяем на пробелы, не удаляем
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
- .gsub(/sqrt\s*\(([^)]+)\)/) { "Math.sqrt(#{$1})" }
115
- .gsub(/sin\s*\(([^)]+)\)/) { "Math.sin(#{$1})" }
116
- .gsub(/cos\s*\(([^)]+)\)/) { "Math.cos(#{$1})" }
117
- .gsub(/tan\s*\(([^)]+)\)/) { "Math.tan(#{$1})" }
118
- .gsub(/log\s*\(([^)]+)\)/) { "Math.log10(#{$1})" }
119
- .gsub(/ln\s*\(([^)]+)\)/) { "Math.log(#{$1})" }
120
- .gsub(/exp\s*\(([^)]+)\)/) { "Math.exp(#{$1})" }
121
- .gsub(/abs\s*\(([^)]+)\)/) { "(#{$1}).abs" }
122
- .gsub(/round\s*\(([^)]+)\)/) { "(#{$1}).round" }
123
- .gsub(/ceil\s*\(([^)]+)\)/) { "(#{$1}).ceil" }
124
- .gsub(/floor\s*\(([^)]+)\)/) { "(#{$1}).floor" }
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?(/\.(abs|round|ceil|floor)\b/)
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