llm_chain 0.5.1 → 0.5.2

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.
@@ -108,41 +108,192 @@ module LLMChain
108
108
  end
109
109
 
110
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
111
+ # Нормализуем line endings
112
+ normalized_prompt = normalize_line_endings(prompt)
113
+
114
+ # 1. Пробуем различные паттерны markdown блоков
115
+ code = extract_markdown_code_blocks(normalized_prompt)
116
+ return clean_code(code) if code && !code.empty?
117
+
118
+ # 2. Ищем код после ключевых команд в одной строке
119
+ code = extract_inline_code_commands(normalized_prompt)
120
+ return clean_code(code) if code && !code.empty?
121
+
122
+ # 3. Ищем код после ключевых слов в разных строках
123
+ code = extract_multiline_code_blocks(normalized_prompt)
124
+ return clean_code(code) if code && !code.empty?
125
+
126
+ # 4. Ищем строки, которые выглядят как код
127
+ code = extract_code_like_lines(normalized_prompt)
128
+ return clean_code(code) if code && !code.empty?
129
+
130
+ # 5. Последняя попытка - весь текст после первого кода
131
+ code = extract_fallback_code(normalized_prompt)
132
+ clean_code(code)
133
+ end
134
+
135
+ private
136
+
137
+ def normalize_line_endings(text)
138
+ text.gsub(/\r\n/, "\n").gsub(/\r/, "\n")
139
+ end
114
140
 
115
- # Ищем код после ключевых слов в той же строке (например, "Execute code: puts ...")
116
- execute_match = prompt.match(/execute\s+code:\s*(.+)/i)
117
- return execute_match[1].strip if execute_match
141
+ def extract_markdown_code_blocks(prompt)
142
+ # Различные паттерны для markdown блоков
143
+ patterns = [
144
+ # Стандартный markdown с указанием языка
145
+ /```(?:ruby|python|javascript|js)\s*\n(.*?)\n```/mi,
146
+ # Markdown без указания языка
147
+ /```\s*\n(.*?)\n```/mi,
148
+ # Markdown с любым языком
149
+ /```\w*\s*\n(.*?)\n```/mi,
150
+ # Тильды вместо backticks
151
+ /~~~(?:ruby|python|javascript|js)?\s*\n(.*?)\n~~~/mi,
152
+ # Без переносов строк
153
+ /```(?:ruby|python|javascript|js)?(.*?)```/mi,
154
+ # Четыре пробела (indented code blocks)
155
+ /^ (.+)$/m
156
+ ]
157
+
158
+ patterns.each do |pattern|
159
+ match = prompt.match(pattern)
160
+ return match[1] if match && match[1].strip.length > 0
161
+ end
162
+
163
+ nil
164
+ end
118
165
 
119
- run_match = prompt.match(/run\s+code:\s*(.+)/i)
120
- return run_match[1].strip if run_match
166
+ def extract_inline_code_commands(prompt)
167
+ # Команды в одной строке
168
+ inline_patterns = [
169
+ /execute\s+code:\s*(.+)/i,
170
+ /run\s+code:\s*(.+)/i,
171
+ /run\s+this:\s*(.+)/i,
172
+ /execute:\s*(.+)/i,
173
+ /run:\s*(.+)/i,
174
+ /code:\s*(.+)/i
175
+ ]
176
+
177
+ inline_patterns.each do |pattern|
178
+ match = prompt.match(pattern)
179
+ return match[1] if match && match[1].strip.length > 0
180
+ end
181
+
182
+ nil
183
+ end
121
184
 
122
- # Ищем код после ключевых слов в разных строках
185
+ def extract_multiline_code_blocks(prompt)
186
+ lines = prompt.split("\n")
187
+
123
188
  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?
189
+ keyword_line_index = lines.find_index { |line| line.downcase.include?(keyword.downcase) }
190
+ next unless keyword_line_index
191
+
192
+ # Берем строки после ключевого слова
193
+ code_lines = lines[(keyword_line_index + 1)..-1]
194
+ next unless code_lines
195
+
196
+ # Найдем первую непустую строку
197
+ first_code_line = code_lines.find_index { |line| !line.strip.empty? }
198
+ next unless first_code_line
199
+
200
+ # Берем все строки начиная с первой непустой
201
+ relevant_lines = code_lines[first_code_line..-1]
202
+
203
+ # Определяем отступ первой строки кода
204
+ first_line = relevant_lines.first
205
+ indent = first_line.match(/^(\s*)/)[1].length
206
+
207
+ # Собираем все строки с таким же или большим отступом
208
+ code_block = []
209
+ relevant_lines.each do |line|
210
+ if line.strip.empty?
211
+ code_block << "" # Сохраняем пустые строки
212
+ elsif line.match(/^(\s*)/)[1].length >= indent
213
+ code_block << line
214
+ else
215
+ break # Прекращаем при уменьшении отступа
132
216
  end
133
217
  end
218
+
219
+ return code_block.join("\n") if code_block.any? { |line| !line.strip.empty? }
134
220
  end
221
+
222
+ nil
223
+ end
224
+
225
+ def extract_code_like_lines(prompt)
226
+ lines = prompt.split("\n")
227
+
228
+ code_lines = lines.select do |line|
229
+ stripped = line.strip
230
+ next false if stripped.empty?
231
+
232
+ # Проверяем различные паттерны кода
233
+ stripped.match?(/^(def|class|function|var|let|const|print|puts|console\.log)/i) ||
234
+ stripped.match?(/^\w+\s*[=+\-*\/]\s*/) ||
235
+ stripped.match?(/^\s*(if|for|while|return|import|require)[\s(]/i) ||
236
+ stripped.match?(/puts\s+/) ||
237
+ stripped.match?(/print\s*\(/) ||
238
+ stripped.match?(/^\w+\(.*\)/) ||
239
+ stripped.match?(/^\s*#.*/) || # Комментарии
240
+ stripped.match?(/^\s*\/\/.*/) || # JS комментарии
241
+ stripped.match?(/^\s*\/\*.*\*\//) # Блочные комментарии
242
+ end
243
+
244
+ code_lines.join("\n") if code_lines.any?
245
+ end
135
246
 
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*\(/)
247
+ def extract_fallback_code(prompt)
248
+ # Последняя попытка - ищем что-то похожее на код
249
+ lines = prompt.split("\n")
250
+
251
+ # Найдем первую строку, которая выглядит как код
252
+ start_index = lines.find_index do |line|
253
+ stripped = line.strip
254
+ stripped.match?(/^(def|class|function|puts|print|console\.log|var|let|const)/i) ||
255
+ stripped.include?('=') ||
256
+ stripped.include?(';')
257
+ end
258
+
259
+ return nil unless start_index
260
+
261
+ # Берем все строки после найденной
262
+ code_lines = lines[start_index..-1]
263
+
264
+ # Останавливаемся на первой строке, которая явно не код
265
+ end_index = code_lines.find_index do |line|
266
+ stripped = line.strip
267
+ stripped.match?(/^(что|как|где|когда|зачем|почему|what|how|where|when|why)/i) ||
268
+ stripped.length > 100 # Слишком длинная строка
143
269
  end
270
+
271
+ relevant_lines = end_index ? code_lines[0...end_index] : code_lines
272
+ relevant_lines.join("\n")
273
+ end
144
274
 
145
- code_lines.join("\n")
275
+ def clean_code(code)
276
+ return "" unless code
277
+
278
+ lines = code.strip.lines
279
+
280
+ # Удаляем только комментарии, которые не являются частью кода
281
+ cleaned_lines = lines.reject do |line|
282
+ stripped = line.strip
283
+ # Удаляем только строки, которые содержат ТОЛЬКО комментарии
284
+ stripped.match?(/^\s*#[^{]*$/) || # Ruby комментарии (но не интерполяция)
285
+ stripped.match?(/^\s*\/\/.*$/) || # JS комментарии
286
+ stripped.match?(/^\s*\/\*.*\*\/\s*$/) # Блочные комментарии
287
+ end
288
+
289
+ # Убираем пустые строки в начале и конце, но сохраняем внутри
290
+ start_index = cleaned_lines.find_index { |line| !line.strip.empty? }
291
+ return "" unless start_index
292
+
293
+ end_index = cleaned_lines.rindex { |line| !line.strip.empty? }
294
+ return "" unless end_index
295
+
296
+ cleaned_lines[start_index..end_index].join
146
297
  end
147
298
 
148
299
  def detect_language(code, prompt)
@@ -46,13 +46,15 @@ module LLMChain
46
46
  num_results = extract_num_results(prompt)
47
47
 
48
48
  begin
49
- results = perform_search(query, num_results)
49
+ results = perform_search_with_retry(query, num_results)
50
50
  format_search_results(query, results)
51
51
  rescue => e
52
+ log_error("Search failed for '#{query}'", e)
52
53
  {
53
54
  query: query,
54
55
  error: e.message,
55
- formatted: "Error searching for '#{query}': #{e.message}"
56
+ results: [],
57
+ formatted: "Search unavailable for '#{query}'. Please try again later or rephrase your query."
56
58
  }
57
59
  end
58
60
  end
@@ -97,6 +99,31 @@ module LLMChain
97
99
  5 # default
98
100
  end
99
101
 
102
+ def perform_search_with_retry(query, num_results, max_retries: 3)
103
+ retries = 0
104
+ last_error = nil
105
+
106
+ begin
107
+ perform_search(query, num_results)
108
+ rescue => e
109
+ last_error = e
110
+ retries += 1
111
+
112
+ if retries <= max_retries && retryable_error?(e)
113
+ sleep_time = [0.5 * (2 ** (retries - 1)), 5.0].min # exponential backoff, max 5 seconds
114
+ log_retry("Retrying search (#{retries}/#{max_retries}) after #{sleep_time}s", e)
115
+ sleep(sleep_time)
116
+ retry
117
+ else
118
+ log_error("Search failed after #{retries} attempts", e)
119
+ # Fallback to hardcoded results as last resort
120
+ hardcoded = get_hardcoded_results(query)
121
+ return hardcoded unless hardcoded.empty?
122
+ raise e
123
+ end
124
+ end
125
+ end
126
+
100
127
  def perform_search(query, num_results)
101
128
  case @search_engine
102
129
  when :google
@@ -115,25 +142,52 @@ module LLMChain
115
142
  def fallback_search(query, num_results)
116
143
  return [] if num_results <= 0
117
144
 
118
- # Если обычный поиск не работает, используем заранее заготовленные данные
119
- # для популярных запросов
145
+ # Сначала пробуем заранее заготовленные данные для популярных запросов
120
146
  hardcoded_results = get_hardcoded_results(query)
121
147
  return hardcoded_results unless hardcoded_results.empty?
122
148
 
123
- # Простой поиск по HTML странице DuckDuckGo
124
- uri = URI("https://html.duckduckgo.com/html/")
125
- uri.query = URI.encode_www_form(q: query)
126
-
127
- http = Net::HTTP.new(uri.host, uri.port)
128
- http.use_ssl = true
129
- http.open_timeout = 10
130
- http.read_timeout = 10
149
+ # Проверяем, доступен ли интернет
150
+ return offline_fallback_results(query) if offline_mode?
131
151
 
132
- response = http.get(uri.request_uri)
133
- return [] unless response.code == '200'
152
+ begin
153
+ results = search_duckduckgo_html(query, num_results)
154
+ return results unless results.empty?
155
+
156
+ # Если DuckDuckGo не дал результатов, возвращаем заглушку
157
+ offline_fallback_results(query)
158
+ rescue => e
159
+ log_error("Fallback search failed", e)
160
+ offline_fallback_results(query)
161
+ end
162
+ end
163
+
164
+ def search_duckduckgo_html(query, num_results)
165
+ require 'timeout'
134
166
 
135
- # Улучшенный парсинг результатов
136
- html = response.body
167
+ Timeout.timeout(15) do
168
+ uri = URI("https://html.duckduckgo.com/html/")
169
+ uri.query = URI.encode_www_form(q: query)
170
+
171
+ http = Net::HTTP.new(uri.host, uri.port)
172
+ http.use_ssl = true
173
+ http.open_timeout = 8
174
+ http.read_timeout = 10
175
+
176
+ response = http.get(uri.request_uri)
177
+
178
+ unless response.code == '200'
179
+ log_error("DuckDuckGo returned #{response.code}", StandardError.new(response.body))
180
+ return []
181
+ end
182
+
183
+ parse_duckduckgo_results(response.body, num_results)
184
+ end
185
+ rescue Timeout::Error
186
+ log_error("DuckDuckGo search timeout", Timeout::Error.new("Request took longer than 15 seconds"))
187
+ []
188
+ end
189
+
190
+ def parse_duckduckgo_results(html, num_results)
137
191
  results = []
138
192
 
139
193
  # Ищем различные паттерны результатов
@@ -147,9 +201,10 @@ module LLMChain
147
201
  html.scan(pattern) do |url, title|
148
202
  next if results.length >= num_results
149
203
  next if url.include?('duckduckgo.com/y.js') # Skip tracking links
204
+ next if title.strip.empty?
150
205
 
151
206
  results << {
152
- title: title.strip.gsub(/\s+/, ' '),
207
+ title: clean_html_text(title),
153
208
  url: clean_url(url),
154
209
  snippet: "Search result from DuckDuckGo"
155
210
  }
@@ -158,14 +213,37 @@ module LLMChain
158
213
  end
159
214
 
160
215
  results
161
- rescue => e
216
+ end
217
+
218
+ def offline_fallback_results(query)
162
219
  [{
163
220
  title: "Search unavailable",
164
221
  url: "",
165
- snippet: "Unable to perform web search at this time. Query: #{query}"
222
+ snippet: "Unable to perform web search at this time. Query: #{query}. Please check your internet connection."
166
223
  }]
167
224
  end
168
225
 
226
+ def offline_mode?
227
+ # Простая проверка доступности интернета
228
+ begin
229
+ require 'socket'
230
+ Socket.tcp("8.8.8.8", 53, connect_timeout: 3) {}
231
+ false
232
+ rescue
233
+ true
234
+ end
235
+ end
236
+
237
+ def clean_html_text(text)
238
+ text.strip
239
+ .gsub(/&lt;/, '<')
240
+ .gsub(/&gt;/, '>')
241
+ .gsub(/&amp;/, '&')
242
+ .gsub(/&quot;/, '"')
243
+ .gsub(/&#39;/, "'")
244
+ .gsub(/\s+/, ' ')
245
+ end
246
+
169
247
  # Заранее заготовленные результаты для популярных запросов
170
248
  def get_hardcoded_results(query)
171
249
  ruby_version_queries = [
@@ -203,76 +281,151 @@ module LLMChain
203
281
  def search_google(query, num_results)
204
282
  # Google Custom Search API (требует API ключ)
205
283
  unless @api_key
284
+ log_error("Google API key not provided, using fallback", StandardError.new("No API key"))
206
285
  return fallback_search(query, num_results)
207
286
  end
208
287
 
209
- search_engine_id = ENV['GOOGLE_SEARCH_ENGINE_ID'] || ENV['GOOGLE_CX'] || 'your-search-engine-id'
210
-
211
- uri = URI("https://www.googleapis.com/customsearch/v1")
212
- params = {
213
- key: @api_key,
214
- cx: search_engine_id,
215
- q: query,
216
- num: [num_results, 10].min
217
- }
218
- uri.query = URI.encode_www_form(params)
219
-
220
- http = Net::HTTP.new(uri.host, uri.port)
221
- http.use_ssl = true
222
- http.open_timeout = 10
223
- http.read_timeout = 10
224
-
225
- response = http.get(uri.request_uri)
226
-
227
- unless response.code == '200'
288
+ search_engine_id = ENV['GOOGLE_SEARCH_ENGINE_ID'] || ENV['GOOGLE_CX']
289
+ unless search_engine_id && search_engine_id != 'your-search-engine-id'
290
+ log_error("Google Search Engine ID not configured", StandardError.new("Missing GOOGLE_SEARCH_ENGINE_ID"))
228
291
  return fallback_search(query, num_results)
229
292
  end
230
-
231
- data = JSON.parse(response.body)
232
293
 
233
- results = (data['items'] || []).map do |item|
234
- {
235
- title: item['title'],
236
- url: item['link'],
237
- snippet: item['snippet']
238
- }
294
+ begin
295
+ require 'timeout'
296
+
297
+ Timeout.timeout(20) do
298
+ uri = URI("https://www.googleapis.com/customsearch/v1")
299
+ params = {
300
+ key: @api_key,
301
+ cx: search_engine_id,
302
+ q: query,
303
+ num: [num_results, 10].min,
304
+ safe: 'active'
305
+ }
306
+ uri.query = URI.encode_www_form(params)
307
+
308
+ http = Net::HTTP.new(uri.host, uri.port)
309
+ http.use_ssl = true
310
+ http.open_timeout = 8
311
+ http.read_timeout = 12
312
+
313
+ response = http.get(uri.request_uri)
314
+
315
+ case response.code
316
+ when '200'
317
+ data = JSON.parse(response.body)
318
+
319
+ if data['error']
320
+ log_error("Google API error: #{data['error']['message']}", StandardError.new(data['error']['message']))
321
+ return fallback_search(query, num_results)
322
+ end
323
+
324
+ results = (data['items'] || []).map do |item|
325
+ {
326
+ title: item['title']&.strip || 'Untitled',
327
+ url: item['link'] || '',
328
+ snippet: item['snippet']&.strip || 'No description available'
329
+ }
330
+ end
331
+
332
+ # Если Google не вернул результатов, используем fallback
333
+ results.empty? ? fallback_search(query, num_results) : results
334
+ when '403'
335
+ log_error("Google API quota exceeded or invalid key", StandardError.new(response.body))
336
+ fallback_search(query, num_results)
337
+ when '400'
338
+ log_error("Google API bad request", StandardError.new(response.body))
339
+ fallback_search(query, num_results)
340
+ else
341
+ log_error("Google API returned #{response.code}", StandardError.new(response.body))
342
+ fallback_search(query, num_results)
343
+ end
344
+ end
345
+ rescue Timeout::Error
346
+ log_error("Google search timeout", Timeout::Error.new("Request took longer than 20 seconds"))
347
+ fallback_search(query, num_results)
348
+ rescue JSON::ParserError => e
349
+ log_error("Invalid JSON response from Google", e)
350
+ fallback_search(query, num_results)
351
+ rescue => e
352
+ log_error("Google search failed", e)
353
+ fallback_search(query, num_results)
239
354
  end
240
-
241
- # Если Google не вернул результатов, используем fallback
242
- results.empty? ? fallback_search(query, num_results) : results
243
- rescue => e
244
- fallback_search(query, num_results)
245
355
  end
246
356
 
247
357
  def search_bing(query, num_results)
248
358
  # Bing Web Search API (требует API ключ)
249
- raise "Bing API key required" unless @api_key
250
-
251
- uri = URI("https://api.bing.microsoft.com/v7.0/search")
252
- params = {
253
- q: query,
254
- count: [num_results, 20].min,
255
- responseFilter: 'Webpages'
256
- }
257
- uri.query = URI.encode_www_form(params)
359
+ unless @api_key
360
+ log_error("Bing API key not provided, using fallback", StandardError.new("No API key"))
361
+ return fallback_search(query, num_results)
362
+ end
258
363
 
259
- http = Net::HTTP.new(uri.host, uri.port)
260
- http.use_ssl = true
261
-
262
- request = Net::HTTP::Get.new(uri)
263
- request['Ocp-Apim-Subscription-Key'] = @api_key
264
-
265
- response = http.request(request)
266
- raise "Bing API error: #{response.code}" unless response.code == '200'
364
+ begin
365
+ require 'timeout'
366
+
367
+ Timeout.timeout(20) do
368
+ uri = URI("https://api.bing.microsoft.com/v7.0/search")
369
+ params = {
370
+ q: query,
371
+ count: [num_results, 20].min,
372
+ responseFilter: 'Webpages',
373
+ safeSearch: 'Moderate'
374
+ }
375
+ uri.query = URI.encode_www_form(params)
267
376
 
268
- data = JSON.parse(response.body)
269
-
270
- (data.dig('webPages', 'value') || []).map do |item|
271
- {
272
- title: item['name'],
273
- url: item['url'],
274
- snippet: item['snippet']
275
- }
377
+ http = Net::HTTP.new(uri.host, uri.port)
378
+ http.use_ssl = true
379
+ http.open_timeout = 8
380
+ http.read_timeout = 12
381
+
382
+ request = Net::HTTP::Get.new(uri)
383
+ request['Ocp-Apim-Subscription-Key'] = @api_key
384
+ request['User-Agent'] = 'LLMChain/1.0'
385
+
386
+ response = http.request(request)
387
+
388
+ case response.code
389
+ when '200'
390
+ data = JSON.parse(response.body)
391
+
392
+ if data['error']
393
+ log_error("Bing API error: #{data['error']['message']}", StandardError.new(data['error']['message']))
394
+ return fallback_search(query, num_results)
395
+ end
396
+
397
+ results = (data.dig('webPages', 'value') || []).map do |item|
398
+ {
399
+ title: item['name']&.strip || 'Untitled',
400
+ url: item['url'] || '',
401
+ snippet: item['snippet']&.strip || 'No description available'
402
+ }
403
+ end
404
+
405
+ results.empty? ? fallback_search(query, num_results) : results
406
+ when '401'
407
+ log_error("Bing API unauthorized - check your subscription key", StandardError.new(response.body))
408
+ fallback_search(query, num_results)
409
+ when '403'
410
+ log_error("Bing API quota exceeded", StandardError.new(response.body))
411
+ fallback_search(query, num_results)
412
+ when '429'
413
+ log_error("Bing API rate limit exceeded", StandardError.new(response.body))
414
+ fallback_search(query, num_results)
415
+ else
416
+ log_error("Bing API returned #{response.code}", StandardError.new(response.body))
417
+ fallback_search(query, num_results)
418
+ end
419
+ end
420
+ rescue Timeout::Error
421
+ log_error("Bing search timeout", Timeout::Error.new("Request took longer than 20 seconds"))
422
+ fallback_search(query, num_results)
423
+ rescue JSON::ParserError => e
424
+ log_error("Invalid JSON response from Bing", e)
425
+ fallback_search(query, num_results)
426
+ rescue => e
427
+ log_error("Bing search failed", e)
428
+ fallback_search(query, num_results)
276
429
  end
277
430
  end
278
431
 
@@ -300,6 +453,52 @@ module LLMChain
300
453
  def required_parameters
301
454
  ['query']
302
455
  end
456
+
457
+ private
458
+
459
+ def retryable_error?(error)
460
+ # Определяем, стоит ли повторять запрос при данной ошибке
461
+ case error
462
+ when Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout
463
+ true
464
+ when SocketError
465
+ # DNS ошибки обычно временные
466
+ true
467
+ when Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH
468
+ true
469
+ when Net::HTTPError
470
+ # Повторяем только для серверных ошибок (5xx)
471
+ error.message.match?(/5\d\d/)
472
+ else
473
+ false
474
+ end
475
+ end
476
+
477
+ def log_error(message, error)
478
+ return unless should_log?
479
+
480
+ if defined?(Rails) && Rails.logger
481
+ Rails.logger.error "[WebSearch] #{message}: #{error.class} - #{error.message}"
482
+ else
483
+ warn "[WebSearch] #{message}: #{error.class} - #{error.message}"
484
+ end
485
+ end
486
+
487
+ def log_retry(message, error)
488
+ return unless should_log?
489
+
490
+ if defined?(Rails) && Rails.logger
491
+ Rails.logger.warn "[WebSearch] #{message}: #{error.class} - #{error.message}"
492
+ else
493
+ warn "[WebSearch] #{message}: #{error.class} - #{error.message}"
494
+ end
495
+ end
496
+
497
+ def should_log?
498
+ ENV['LLM_CHAIN_DEBUG'] == 'true' ||
499
+ ENV['RAILS_ENV'] == 'development' ||
500
+ (defined?(Rails) && Rails.env.development?)
501
+ end
303
502
  end
304
503
  end
305
504
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmChain
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end