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
@@ -4,15 +4,29 @@ require 'uri'
4
4
 
5
5
  module LLMChain
6
6
  module Tools
7
- class WebSearch < BaseTool
8
- KEYWORDS = %w[
9
- search find lookup google bing
10
- what is who is where is when is
11
- latest news current information
12
- weather forecast stock price
13
- definition meaning wikipedia
7
+ class WebSearch < Base
8
+ SEARCH_KEYWORDS = %w[
9
+ search find lookup google bing web site news wikipedia
14
10
  ].freeze
15
11
 
12
+ GOOGLE_API_URL = "https://www.googleapis.com/customsearch/v1".freeze
13
+ DEFAULT_NUM_RESULTS = 5
14
+ MAX_GOOGLE_RESULTS = 10
15
+ GOOGLE_TIMEOUT = 20
16
+ GOOGLE_SAFE = 'active'.freeze
17
+
18
+ BING_API_URL = "https://api.bing.microsoft.com/v7.0/search".freeze
19
+ MAX_BING_RESULTS = 20
20
+ BING_TIMEOUT = 20
21
+ BING_SAFE = 'Moderate'.freeze
22
+ BING_RESPONSE_FILTER = 'Webpages'.freeze
23
+
24
+ # --- Приватные константы для парсинга ---
25
+ QUERY_COMMANDS_REGEX = /\b(search for|find|lookup|google|what is|who is|where is|when is)\b/i.freeze
26
+ POLITENESS_REGEX = /\b(please|can you|could you|would you)\b/i.freeze
27
+ NUM_RESULTS_REGEX = /(\d+)\s*(results?|items?|links?)/i.freeze
28
+ MAX_QUERY_WORDS = 10
29
+
16
30
  def initialize(api_key: nil, search_engine: :google)
17
31
  @api_key = api_key || ENV['GOOGLE_API_KEY'] || ENV['SEARCH_API_KEY']
18
32
  @search_engine = search_engine
@@ -34,9 +48,7 @@ module LLMChain
34
48
  end
35
49
 
36
50
  def match?(prompt)
37
- contains_keywords?(prompt, KEYWORDS) ||
38
- contains_question_pattern?(prompt) ||
39
- contains_current_info_request?(prompt)
51
+ contains_keywords?(prompt, SEARCH_KEYWORDS)
40
52
  end
41
53
 
42
54
  def call(prompt, context: {})
@@ -68,35 +80,25 @@ module LLMChain
68
80
 
69
81
  private
70
82
 
71
- def contains_question_pattern?(prompt)
72
- prompt.match?(/\b(what|who|where|when|how|why|which)\b/i)
73
- end
74
-
75
- def contains_current_info_request?(prompt)
76
- prompt.match?(/\b(latest|current|recent|today|now|2024|2023)\b/i)
77
- end
78
-
83
+ # @param prompt [String] Исходный запрос
84
+ # @return [String] Извлечённая суть поискового запроса
79
85
  def extract_query(prompt)
80
- # Удаляем команды поиска и оставляем суть запроса
81
- query = prompt.gsub(/\b(search for|find|lookup|google|what is|who is|where is|when is)\b/i, '')
82
- .gsub(/\b(please|can you|could you|would you)\b/i, '')
83
- .strip
84
-
85
- # Если запрос слишком длинный, берем первые слова
86
+ return "" if prompt.nil? || prompt.strip.empty?
87
+ query = prompt.gsub(QUERY_COMMANDS_REGEX, '')
88
+ .gsub(POLITENESS_REGEX, '')
89
+ .strip
86
90
  words = query.split
87
- if words.length > 10
88
- words.first(10).join(' ')
89
- else
90
- query
91
- end
91
+ return words.first(MAX_QUERY_WORDS).join(' ') if words.length > MAX_QUERY_WORDS
92
+ query
92
93
  end
93
94
 
95
+ # @param prompt [String] Исходный запрос
96
+ # @return [Integer] Количество результатов (по умолчанию)
94
97
  def extract_num_results(prompt)
95
- # Ищем числа в контексте результатов
96
- match = prompt.match(/(\d+)\s*(results?|items?|links?)/i)
97
- return match[1].to_i if match && match[1].to_i.between?(1, 20)
98
-
99
- 5 # default
98
+ return DEFAULT_NUM_RESULTS if prompt.nil? || prompt.empty?
99
+ match = prompt.match(NUM_RESULTS_REGEX)
100
+ return match[1].to_i if match && match[1].to_i.between?(1, MAX_BING_RESULTS)
101
+ DEFAULT_NUM_RESULTS
100
102
  end
101
103
 
102
104
  def perform_search_with_retry(query, num_results, max_retries: 3)
@@ -127,316 +129,153 @@ module LLMChain
127
129
  def perform_search(query, num_results)
128
130
  case @search_engine
129
131
  when :google
130
- search_google(query, num_results)
132
+ search_google_results(query, num_results)
131
133
  when :bing
132
- search_bing(query, num_results)
133
- when :duckduckgo
134
- # Deprecated - use Google instead
135
- fallback_search(query, num_results)
134
+ search_bing_results(query, num_results)
136
135
  else
137
136
  raise "Unsupported search engine: #{@search_engine}. Use :google or :bing"
138
137
  end
139
138
  end
140
139
 
141
- # Fallback поиск когда Google API недоступен
142
- def fallback_search(query, num_results)
143
- return [] if num_results <= 0
144
-
145
- # Сначала пробуем заранее заготовленные данные для популярных запросов
146
- hardcoded_results = get_hardcoded_results(query)
147
- return hardcoded_results unless hardcoded_results.empty?
148
-
149
- # Проверяем, доступен ли интернет
150
- return offline_fallback_results(query) if offline_mode?
151
-
140
+ # --- Google Search SRP decomposition ---
141
+ def search_google_results(query, num_results)
142
+ unless @api_key
143
+ handle_api_error(StandardError.new("No API key"), "Google API key not provided, using fallback")
144
+ return []
145
+ end
146
+ search_engine_id = ENV['GOOGLE_SEARCH_ENGINE_ID'] || ENV['GOOGLE_CX']
147
+ unless search_engine_id && search_engine_id != 'your-search-engine-id'
148
+ handle_api_error(StandardError.new("Missing GOOGLE_SEARCH_ENGINE_ID"), "Google Search Engine ID not configured")
149
+ return []
150
+ end
152
151
  begin
153
- results = search_duckduckgo_html(query, num_results)
154
- return results unless results.empty?
155
-
156
- # Если DuckDuckGo не дал результатов, возвращаем заглушку
157
- offline_fallback_results(query)
152
+ response = fetch_google_response(query, num_results, search_engine_id)
153
+ parse_google_response(response)
158
154
  rescue => e
159
- log_error("Fallback search failed", e)
160
- offline_fallback_results(query)
155
+ handle_api_error(e, "Google search failed")
156
+ []
161
157
  end
162
158
  end
163
159
 
164
- def search_duckduckgo_html(query, num_results)
160
+ def fetch_google_response(query, num_results, search_engine_id)
165
161
  require 'timeout'
166
-
167
- Timeout.timeout(15) do
168
- uri = URI("https://html.duckduckgo.com/html/")
169
- uri.query = URI.encode_www_form(q: query)
170
-
162
+ Timeout.timeout(GOOGLE_TIMEOUT) do
163
+ uri = URI(GOOGLE_API_URL)
164
+ params = {
165
+ key: @api_key,
166
+ cx: search_engine_id,
167
+ q: query,
168
+ num: [num_results, MAX_GOOGLE_RESULTS].min,
169
+ safe: GOOGLE_SAFE
170
+ }
171
+ uri.query = URI.encode_www_form(params)
171
172
  http = Net::HTTP.new(uri.host, uri.port)
172
173
  http.use_ssl = true
173
174
  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)
175
+ http.read_timeout = 12
176
+ http.get(uri.request_uri)
184
177
  end
185
- rescue Timeout::Error
186
- log_error("DuckDuckGo search timeout", Timeout::Error.new("Request took longer than 15 seconds"))
187
- []
178
+ rescue Timeout::Error => e
179
+ handle_api_error(e, "Google search timeout")
180
+ nil
188
181
  end
189
182
 
190
- def parse_duckduckgo_results(html, num_results)
191
- results = []
192
-
193
- # Ищем различные паттерны результатов
194
- patterns = [
195
- /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/,
196
- /<a[^>]+href="([^"]+)"[^>]*class="[^"]*result[^"]*"[^>]*>([^<]+)<\/a>/,
197
- /<h3[^>]*><a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a><\/h3>/
198
- ]
199
-
200
- patterns.each do |pattern|
201
- html.scan(pattern) do |url, title|
202
- next if results.length >= num_results
203
- next if url.include?('duckduckgo.com/y.js') # Skip tracking links
204
- next if title.strip.empty?
205
-
206
- results << {
207
- title: clean_html_text(title),
208
- url: clean_url(url),
209
- snippet: "Search result from DuckDuckGo"
210
- }
211
- end
212
- break if results.length >= num_results
183
+ def parse_google_response(response)
184
+ return [] unless response && response.code == '200'
185
+ data = JSON.parse(response.body) rescue nil
186
+ if data.nil? || data['error']
187
+ handle_api_error(StandardError.new(data&.dig('error', 'message') || 'Invalid JSON'), "Google API error")
188
+ return []
213
189
  end
214
-
215
- results
216
- end
217
-
218
- def offline_fallback_results(query)
219
- [{
220
- title: "Search unavailable",
221
- url: "",
222
- snippet: "Unable to perform web search at this time. Query: #{query}. Please check your internet connection."
223
- }]
224
- end
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
-
247
- # Заранее заготовленные результаты для популярных запросов
248
- def get_hardcoded_results(query)
249
- ruby_version_queries = [
250
- /latest ruby version/i,
251
- /current ruby version/i,
252
- /newest ruby version/i,
253
- /which.*latest.*ruby/i,
254
- /ruby.*latest.*version/i
255
- ]
256
-
257
- if ruby_version_queries.any? { |pattern| query.match?(pattern) }
258
- return [{
259
- title: "Ruby Releases",
260
- url: "https://www.ruby-lang.org/en/downloads/releases/",
261
- snippet: "Ruby 3.3.6 is the current stable version. Ruby 3.4.0 is in development."
262
- }, {
263
- title: "Ruby Release Notes",
264
- url: "https://www.ruby-lang.org/en/news/",
265
- snippet: "Latest Ruby version 3.3.6 released with security fixes and improvements."
266
- }]
190
+ (data['items'] || []).map do |item|
191
+ {
192
+ title: item['title']&.strip || 'Untitled',
193
+ url: item['link'] || '',
194
+ snippet: item['snippet']&.strip || 'No description available'
195
+ }
267
196
  end
268
-
197
+ rescue JSON::ParserError => e
198
+ handle_api_error(e, "Invalid JSON response from Google")
269
199
  []
270
200
  end
271
201
 
272
- def clean_url(url)
273
- # Убираем DuckDuckGo redirect
274
- if url.start_with?('//duckduckgo.com/l/?uddg=')
275
- decoded = URI.decode_www_form_component(url.split('uddg=')[1])
276
- return decoded.split('&')[0]
277
- end
278
- url
279
- end
280
-
281
- def search_google(query, num_results)
282
- # Google Custom Search API (требует API ключ)
202
+ # --- Bing Search SRP decomposition ---
203
+ def search_bing_results(query, num_results)
283
204
  unless @api_key
284
- log_error("Google API key not provided, using fallback", StandardError.new("No API key"))
285
- return fallback_search(query, num_results)
286
- end
287
-
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"))
291
- return fallback_search(query, num_results)
205
+ handle_api_error(StandardError.new("No API key"), "Bing API key not provided, using fallback")
206
+ return []
292
207
  end
293
-
294
208
  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)
209
+ response = fetch_bing_response(query, num_results)
210
+ parse_bing_response(response)
351
211
  rescue => e
352
- log_error("Google search failed", e)
353
- fallback_search(query, num_results)
212
+ handle_api_error(e, "Bing search failed")
213
+ []
354
214
  end
355
215
  end
356
216
 
357
- def search_bing(query, num_results)
358
- # Bing Web Search API (требует API ключ)
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)
217
+ def fetch_bing_response(query, num_results)
218
+ require 'timeout'
219
+ Timeout.timeout(BING_TIMEOUT) do
220
+ uri = URI(BING_API_URL)
221
+ params = {
222
+ q: query,
223
+ count: [num_results, MAX_BING_RESULTS].min,
224
+ responseFilter: BING_RESPONSE_FILTER,
225
+ safeSearch: BING_SAFE
226
+ }
227
+ uri.query = URI.encode_www_form(params)
228
+ http = Net::HTTP.new(uri.host, uri.port)
229
+ http.use_ssl = true
230
+ http.open_timeout = 8
231
+ http.read_timeout = 12
232
+ request = Net::HTTP::Get.new(uri)
233
+ request['Ocp-Apim-Subscription-Key'] = @api_key
234
+ request['User-Agent'] = 'LLMChain/1.0'
235
+ http.request(request)
362
236
  end
237
+ rescue Timeout::Error => e
238
+ handle_api_error(e, "Bing search timeout")
239
+ nil
240
+ end
363
241
 
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)
376
-
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)
242
+ def parse_bing_response(response)
243
+ return [] unless response && response.code == '200'
244
+ data = JSON.parse(response.body) rescue nil
245
+ if data.nil? || data['error']
246
+ handle_api_error(StandardError.new(data&.dig('error', 'message') || 'Invalid JSON'), "Bing API error")
247
+ return []
248
+ end
249
+ (data.dig('webPages', 'value') || []).map do |item|
250
+ {
251
+ title: item['name']&.strip || 'Untitled',
252
+ url: item['url'] || '',
253
+ snippet: item['snippet']&.strip || 'No description available'
254
+ }
429
255
  end
256
+ rescue JSON::ParserError => e
257
+ handle_api_error(e, "Invalid JSON response from Bing")
258
+ []
259
+ end
260
+
261
+ def handle_api_error(error, context = nil)
262
+ log_error(context || "API error", error)
430
263
  end
431
264
 
265
+ # --- Fallback/hardcoded results parsing ---
266
+ def parse_hardcoded_results(query)
267
+ hardcoded = get_hardcoded_results(query)
268
+ return [] if hardcoded.empty?
269
+ hardcoded
270
+ end
271
+
272
+ # --- Форматирование результатов поиска ---
432
273
  def format_search_results(query, results)
433
- if results.empty?
434
- return {
435
- query: query,
436
- results: [],
437
- formatted: "No results found for '#{query}'"
438
- }
439
- end
274
+ return {
275
+ query: query,
276
+ results: [],
277
+ formatted: "No results found for '#{query}'"
278
+ } if results.empty?
440
279
 
441
280
  formatted_results = results.map.with_index(1) do |result, index|
442
281
  "#{index}. #{result[:title]}\n #{result[:snippet]}\n #{result[:url]}"
@@ -450,33 +289,9 @@ module LLMChain
450
289
  }
451
290
  end
452
291
 
453
- def required_parameters
454
- ['query']
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
-
292
+ # --- Логирование и обработка ошибок ---
477
293
  def log_error(message, error)
478
294
  return unless should_log?
479
-
480
295
  if defined?(Rails) && Rails.logger
481
296
  Rails.logger.error "[WebSearch] #{message}: #{error.class} - #{error.message}"
482
297
  else
@@ -486,7 +301,6 @@ module LLMChain
486
301
 
487
302
  def log_retry(message, error)
488
303
  return unless should_log?
489
-
490
304
  if defined?(Rails) && Rails.logger
491
305
  Rails.logger.warn "[WebSearch] #{message}: #{error.class} - #{error.message}"
492
306
  else
@@ -496,8 +310,26 @@ module LLMChain
496
310
 
497
311
  def should_log?
498
312
  ENV['LLM_CHAIN_DEBUG'] == 'true' ||
499
- ENV['RAILS_ENV'] == 'development' ||
500
- (defined?(Rails) && Rails.env.development?)
313
+ ENV['RAILS_ENV'] == 'development' ||
314
+ (defined?(Rails) && Rails.env.development?)
315
+ end
316
+
317
+ def retryable_error?(error)
318
+ # Определяем, стоит ли повторять запрос при данной ошибке
319
+ case error
320
+ when Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout
321
+ true
322
+ when SocketError
323
+ # DNS ошибки обычно временные
324
+ true
325
+ when Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH
326
+ true
327
+ when Net::HTTPError
328
+ # Повторяем только для серверных ошибок (5xx)
329
+ error.message.match?(/5\d\d/)
330
+ else
331
+ false
332
+ end
501
333
  end
502
334
  end
503
335
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmChain
4
- VERSION = "0.5.3"
4
+ VERSION = "0.5.5"
5
5
  end