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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -2
- data/README.md +15 -6
- data/examples/quick_demo.rb +1 -1
- data/examples/tools_example.rb +2 -2
- data/exe/llm-chain +7 -4
- 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 +89 -88
- data/lib/llm_chain/client_registry.rb +2 -0
- data/lib/llm_chain/clients/base.rb +24 -2
- data/lib/llm_chain/clients/deepseek_coder_v2.rb +32 -0
- data/lib/llm_chain/configuration_validator.rb +1 -1
- 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/base.rb +103 -0
- data/lib/llm_chain/tools/base_tool.rb +6 -76
- data/lib/llm_chain/tools/calculator.rb +118 -45
- data/lib/llm_chain/tools/code_interpreter.rb +43 -43
- data/lib/llm_chain/tools/date_time.rb +58 -0
- data/lib/llm_chain/tools/tool_manager.rb +46 -88
- data/lib/llm_chain/tools/tool_manager_factory.rb +44 -0
- data/lib/llm_chain/tools/web_search.rb +168 -336
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +58 -56
- metadata +19 -2
@@ -4,15 +4,29 @@ require 'uri'
|
|
4
4
|
|
5
5
|
module LLMChain
|
6
6
|
module Tools
|
7
|
-
class WebSearch <
|
8
|
-
|
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,
|
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
|
-
|
72
|
-
|
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(
|
82
|
-
|
83
|
-
|
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 >
|
88
|
-
|
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(
|
97
|
-
return match[1].to_i if match && match[1].to_i.between?(1,
|
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
|
-
|
132
|
+
search_google_results(query, num_results)
|
131
133
|
when :bing
|
132
|
-
|
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
|
-
#
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
160
|
-
|
155
|
+
handle_api_error(e, "Google search failed")
|
156
|
+
[]
|
161
157
|
end
|
162
158
|
end
|
163
159
|
|
164
|
-
def
|
160
|
+
def fetch_google_response(query, num_results, search_engine_id)
|
165
161
|
require 'timeout'
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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 =
|
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
|
-
|
187
|
-
|
178
|
+
rescue Timeout::Error => e
|
179
|
+
handle_api_error(e, "Google search timeout")
|
180
|
+
nil
|
188
181
|
end
|
189
182
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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(/</, '<')
|
240
|
-
.gsub(/>/, '>')
|
241
|
-
.gsub(/&/, '&')
|
242
|
-
.gsub(/"/, '"')
|
243
|
-
.gsub(/'/, "'")
|
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
|
-
|
273
|
-
|
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
|
-
|
285
|
-
return
|
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
|
-
|
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
|
-
|
353
|
-
|
212
|
+
handle_api_error(e, "Bing search failed")
|
213
|
+
[]
|
354
214
|
end
|
355
215
|
end
|
356
216
|
|
357
|
-
def
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
|
-
|
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
|
-
|
500
|
-
|
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
|
data/lib/llm_chain/version.rb
CHANGED