llm_chain 0.5.3 → 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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -2
  3. data/README.md +15 -6
  4. data/examples/quick_demo.rb +1 -1
  5. data/examples/tools_example.rb +2 -2
  6. data/exe/llm-chain +7 -4
  7. data/lib/llm_chain/builders/memory_context.rb +24 -0
  8. data/lib/llm_chain/builders/prompt.rb +26 -0
  9. data/lib/llm_chain/builders/rag_documents.rb +25 -0
  10. data/lib/llm_chain/builders/retriever_context.rb +25 -0
  11. data/lib/llm_chain/builders/tool_responses.rb +27 -0
  12. data/lib/llm_chain/chain.rb +89 -88
  13. data/lib/llm_chain/client_registry.rb +2 -0
  14. data/lib/llm_chain/clients/base.rb +24 -2
  15. data/lib/llm_chain/clients/deepseek_coder_v2.rb +32 -0
  16. data/lib/llm_chain/configuration_validator.rb +1 -1
  17. data/lib/llm_chain/interfaces/builders/memory_context_builder.rb +20 -0
  18. data/lib/llm_chain/interfaces/builders/prompt_builder.rb +23 -0
  19. data/lib/llm_chain/interfaces/builders/rag_documents_builder.rb +20 -0
  20. data/lib/llm_chain/interfaces/builders/retriever_context_builder.rb +22 -0
  21. data/lib/llm_chain/interfaces/builders/tool_responses_builder.rb +20 -0
  22. data/lib/llm_chain/interfaces/memory.rb +38 -0
  23. data/lib/llm_chain/interfaces/tool_manager.rb +87 -0
  24. data/lib/llm_chain/memory/array.rb +18 -1
  25. data/lib/llm_chain/memory/redis.rb +20 -3
  26. data/lib/llm_chain/system_diagnostics.rb +73 -0
  27. data/lib/llm_chain/tools/base.rb +103 -0
  28. data/lib/llm_chain/tools/base_tool.rb +6 -76
  29. data/lib/llm_chain/tools/calculator.rb +118 -45
  30. data/lib/llm_chain/tools/code_interpreter.rb +43 -43
  31. data/lib/llm_chain/tools/date_time.rb +58 -0
  32. data/lib/llm_chain/tools/tool_manager.rb +46 -88
  33. data/lib/llm_chain/tools/tool_manager_factory.rb +44 -0
  34. data/lib/llm_chain/tools/web_search.rb +168 -336
  35. data/lib/llm_chain/version.rb +1 -1
  36. data/lib/llm_chain.rb +58 -56
  37. metadata +19 -2
@@ -2,7 +2,14 @@ require 'bigdecimal'
2
2
 
3
3
  module LLMChain
4
4
  module Tools
5
- class Calculator < BaseTool
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' }
12
+ class Calculator < Base
6
13
  KEYWORDS = %w[
7
14
  calculate compute math equation formula
8
15
  add subtract multiply divide
@@ -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
@@ -3,7 +3,7 @@ require 'timeout'
3
3
 
4
4
  module LLMChain
5
5
  module Tools
6
- class CodeInterpreter < BaseTool
6
+ class CodeInterpreter < Base
7
7
  KEYWORDS = %w[
8
8
  code run execute script program
9
9
  ruby python javascript
@@ -108,26 +108,26 @@ module LLMChain
108
108
  end
109
109
 
110
110
  def extract_code(prompt)
111
- # Нормализуем line endings
111
+ # Normalize line endings (CRLF -> LF)
112
112
  normalized_prompt = normalize_line_endings(prompt)
113
113
 
114
- # 1. Пробуем различные паттерны markdown блоков
114
+ # 1. Try various markdown block patterns
115
115
  code = extract_markdown_code_blocks(normalized_prompt)
116
116
  return clean_code(code) if code && !code.empty?
117
117
 
118
- # 2. Ищем код после ключевых команд в одной строке
118
+ # 2. Attempt inline "run code:" patterns
119
119
  code = extract_inline_code_commands(normalized_prompt)
120
120
  return clean_code(code) if code && !code.empty?
121
121
 
122
- # 3. Ищем код после ключевых слов в разных строках
122
+ # 3. Look for code after keywords across multiple lines
123
123
  code = extract_multiline_code_blocks(normalized_prompt)
124
124
  return clean_code(code) if code && !code.empty?
125
125
 
126
- # 4. Ищем строки, которые выглядят как код
126
+ # 4. Fallback: detect code-like lines
127
127
  code = extract_code_like_lines(normalized_prompt)
128
128
  return clean_code(code) if code && !code.empty?
129
129
 
130
- # 5. Последняя попытка - весь текст после первого кода
130
+ # 5. Last resort everything after first code-looking line
131
131
  code = extract_fallback_code(normalized_prompt)
132
132
  clean_code(code)
133
133
  end
@@ -139,19 +139,19 @@ module LLMChain
139
139
  end
140
140
 
141
141
  def extract_markdown_code_blocks(prompt)
142
- # Различные паттерны для markdown блоков
142
+ # Pattern list for markdown code blocks
143
143
  patterns = [
144
- # Стандартный markdown с указанием языка
144
+ # Standard fenced block with language tag
145
145
  /```(?:ruby|python|javascript|js)\s*\n(.*?)\n```/mi,
146
- # Markdown без указания языка
146
+ # Fenced block without language tag
147
147
  /```\s*\n(.*?)\n```/mi,
148
- # Markdown с любым языком
148
+ # Fenced block any language
149
149
  /```\w*\s*\n(.*?)\n```/mi,
150
- # Тильды вместо backticks
150
+ # Using ~~~ instead of ```
151
151
  /~~~(?:ruby|python|javascript|js)?\s*\n(.*?)\n~~~/mi,
152
- # Без переносов строк
152
+ # Single-line fenced block
153
153
  /```(?:ruby|python|javascript|js)?(.*?)```/mi,
154
- # Четыре пробела (indented code blocks)
154
+ # Indented code block (4 spaces)
155
155
  /^ (.+)$/m
156
156
  ]
157
157
 
@@ -164,7 +164,7 @@ module LLMChain
164
164
  end
165
165
 
166
166
  def extract_inline_code_commands(prompt)
167
- # Команды в одной строке
167
+ # Inline "run code" commands
168
168
  inline_patterns = [
169
169
  /execute\s+code:\s*(.+)/i,
170
170
  /run\s+code:\s*(.+)/i,
@@ -189,30 +189,30 @@ module LLMChain
189
189
  keyword_line_index = lines.find_index { |line| line.downcase.include?(keyword.downcase) }
190
190
  next unless keyword_line_index
191
191
 
192
- # Берем строки после ключевого слова
192
+ # Take lines after the keyword
193
193
  code_lines = lines[(keyword_line_index + 1)..-1]
194
194
  next unless code_lines
195
195
 
196
- # Найдем первую непустую строку
196
+ # Find the first non-empty line
197
197
  first_code_line = code_lines.find_index { |line| !line.strip.empty? }
198
198
  next unless first_code_line
199
199
 
200
- # Берем все строки начиная с первой непустой
200
+ # Take all lines starting from the first non-empty line
201
201
  relevant_lines = code_lines[first_code_line..-1]
202
202
 
203
- # Определяем отступ первой строки кода
203
+ # Determine indentation of the first code line
204
204
  first_line = relevant_lines.first
205
205
  indent = first_line.match(/^(\s*)/)[1].length
206
206
 
207
- # Собираем все строки с таким же или большим отступом
207
+ # Collect all lines with the same or greater indentation
208
208
  code_block = []
209
209
  relevant_lines.each do |line|
210
210
  if line.strip.empty?
211
- code_block << "" # Сохраняем пустые строки
211
+ code_block << "" # Preserve empty lines
212
212
  elsif line.match(/^(\s*)/)[1].length >= indent
213
213
  code_block << line
214
214
  else
215
- break # Прекращаем при уменьшении отступа
215
+ break # Stop when indentation decreases
216
216
  end
217
217
  end
218
218
 
@@ -229,26 +229,26 @@ module LLMChain
229
229
  stripped = line.strip
230
230
  next false if stripped.empty?
231
231
 
232
- # Проверяем различные паттерны кода
232
+ # Check various code patterns
233
233
  stripped.match?(/^(def|class|function|var|let|const|print|puts|console\.log)/i) ||
234
234
  stripped.match?(/^\w+\s*[=+\-*\/]\s*/) ||
235
235
  stripped.match?(/^\s*(if|for|while|return|import|require)[\s(]/i) ||
236
236
  stripped.match?(/puts\s+/) ||
237
237
  stripped.match?(/print\s*\(/) ||
238
238
  stripped.match?(/^\w+\(.*\)/) ||
239
- stripped.match?(/^\s*#.*/) || # Комментарии
240
- stripped.match?(/^\s*\/\/.*/) || # JS комментарии
241
- stripped.match?(/^\s*\/\*.*\*\//) # Блочные комментарии
239
+ stripped.match?(/^\s*#.*/) || # Comments
240
+ stripped.match?(/^\s*\/\/.*/) || # JS comments
241
+ stripped.match?(/^\s*\/\*.*\*\//) # Block comments
242
242
  end
243
243
 
244
244
  code_lines.join("\n") if code_lines.any?
245
245
  end
246
246
 
247
247
  def extract_fallback_code(prompt)
248
- # Последняя попытка - ищем что-то похожее на код
248
+ # Final attempt look for anything resembling code
249
249
  lines = prompt.split("\n")
250
250
 
251
- # Найдем первую строку, которая выглядит как код
251
+ # Find first line that looks like code
252
252
  start_index = lines.find_index do |line|
253
253
  stripped = line.strip
254
254
  stripped.match?(/^(def|class|function|puts|print|console\.log|var|let|const)/i) ||
@@ -258,14 +258,14 @@ module LLMChain
258
258
 
259
259
  return nil unless start_index
260
260
 
261
- # Берем все строки после найденной
261
+ # Take all subsequent lines
262
262
  code_lines = lines[start_index..-1]
263
263
 
264
- # Останавливаемся на первой строке, которая явно не код
264
+ # Stop when line clearly not code
265
265
  end_index = code_lines.find_index do |line|
266
266
  stripped = line.strip
267
- stripped.match?(/^(что|как|где|когда|зачем|почему|what|how|where|when|why)/i) ||
268
- stripped.length > 100 # Слишком длинная строка
267
+ stripped.match?(/^(что|как|где|когда|зачем|почему|what|how|where|when|why)/i) || # Russian/English question words
268
+ stripped.length > 100 # Too long -> unlikely code
269
269
  end
270
270
 
271
271
  relevant_lines = end_index ? code_lines[0...end_index] : code_lines
@@ -277,16 +277,16 @@ module LLMChain
277
277
 
278
278
  lines = code.strip.lines
279
279
 
280
- # Удаляем только комментарии, которые не являются частью кода
280
+ # Remove pure comment lines, keep inline comments
281
281
  cleaned_lines = lines.reject do |line|
282
282
  stripped = line.strip
283
- # Удаляем только строки, которые содержат ТОЛЬКО комментарии
284
- stripped.match?(/^\s*#[^{]*$/) || # Ruby комментарии (но не интерполяция)
285
- stripped.match?(/^\s*\/\/.*$/) || # JS комментарии
286
- stripped.match?(/^\s*\/\*.*\*\/\s*$/) # Блочные комментарии
283
+ # Remove only lines that contain ONLY comments
284
+ stripped.match?(/^\s*#[^{]*$/) || # Ruby comments (excluding interpolation)
285
+ stripped.match?(/^\s*\/\/.*$/) || # JS comments
286
+ stripped.match?(/^\s*\/\*.*\*\/\s*$/) # Block comments
287
287
  end
288
288
 
289
- # Убираем пустые строки в начале и конце, но сохраняем внутри
289
+ # Remove blank lines at the beginning and end, but keep them inside
290
290
  start_index = cleaned_lines.find_index { |line| !line.strip.empty? }
291
291
  return "" unless start_index
292
292
 
@@ -297,12 +297,12 @@ module LLMChain
297
297
  end
298
298
 
299
299
  def detect_language(code, prompt)
300
- # Явное указание языка
300
+ # Explicit language specification
301
301
  return 'ruby' if prompt.match?(/```ruby/i) || prompt.include?('Ruby')
302
302
  return 'python' if prompt.match?(/```python/i) || prompt.include?('Python')
303
303
  return 'javascript' if prompt.match?(/```(javascript|js)/i) || prompt.include?('JavaScript')
304
304
 
305
- # Определение по синтаксису
305
+ # Determine by syntax
306
306
  return 'ruby' if code.include?('puts') || code.include?('def ') || code.match?(/\bend\b/)
307
307
  return 'python' if code.include?('print(') || code.match?(/def \w+\(.*\):/) || code.include?('import ')
308
308
  return 'javascript' if code.include?('console.log') || code.include?('function ') || code.include?('var ') || code.include?('let ')
@@ -329,12 +329,12 @@ module LLMChain
329
329
 
330
330
  def execute_ruby(code)
331
331
  Timeout.timeout(@timeout) do
332
- # Создаем временный файл
332
+ # Create a temporary file
333
333
  Tempfile.create(['code', '.rb']) do |file|
334
334
  file.write(code)
335
335
  file.flush
336
336
 
337
- # Выполняем код в отдельном процессе
337
+ # Execute code in a separate process
338
338
  result = `ruby #{file.path} 2>&1`
339
339
 
340
340
  if $?.success?
@@ -369,7 +369,7 @@ module LLMChain
369
369
  file.write(code)
370
370
  file.flush
371
371
 
372
- # Пробуем node.js
372
+ # Try node.js
373
373
  result = `node #{file.path} 2>&1`
374
374
 
375
375
  if $?.success?
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require 'time'
3
+
4
+ module LLMChain
5
+ module Tools
6
+ # Simple tool that returns current date and time.
7
+ class DateTime < Base
8
+ KEYWORDS = %w[time date today now current].freeze
9
+
10
+ def initialize
11
+ super(
12
+ name: "date_time",
13
+ description: "Returns current date and time (optionally for given timezone)",
14
+ parameters: {
15
+ timezone: {
16
+ type: "string",
17
+ description: "IANA timezone name, e.g. 'Europe/Moscow'. Defaults to system TZ"
18
+ }
19
+ }
20
+ )
21
+ end
22
+
23
+ # @param prompt [String]
24
+ # @return [Boolean]
25
+ def match?(prompt)
26
+ contains_keywords?(prompt, KEYWORDS)
27
+ end
28
+
29
+ # @param prompt [String]
30
+ # @param context [Hash]
31
+ def call(prompt, context: {})
32
+ params = extract_parameters(prompt)
33
+ tz = params[:timezone]
34
+ time = tz ? Time.now.getlocal(timezone_offset(tz)) : Time.now
35
+ {
36
+ timezone: tz || Time.now.zone,
37
+ iso: time.iso8601,
38
+ formatted: time.strftime("%Y-%m-%d %H:%M:%S %Z")
39
+ }
40
+ end
41
+
42
+ def extract_parameters(prompt)
43
+ tz_match = prompt.match(/in\s+([A-Za-z_\/]+)/)
44
+ { timezone: tz_match && tz_match[1] }
45
+ end
46
+
47
+ private
48
+
49
+ def timezone_offset(tz)
50
+ # Fallback: use TZInfo if available, else default to system
51
+ require 'tzinfo'
52
+ TZInfo::Timezone.get(tz).current_period.offset
53
+ rescue LoadError, TZInfo::InvalidTimezoneIdentifier
54
+ 0
55
+ end
56
+ end
57
+ end
58
+ end