llm_chain 0.4.0 → 0.5.1
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 +51 -0
- data/README.md +516 -102
- data/examples/quick_demo.rb +93 -0
- data/examples/tools_example.rb +255 -0
- data/lib/llm_chain/chain.rb +24 -5
- data/lib/llm_chain/client_registry.rb +0 -1
- data/lib/llm_chain/clients/qwen.rb +13 -1
- data/lib/llm_chain/tools/base_tool.rb +81 -0
- data/lib/llm_chain/tools/calculator.rb +154 -0
- data/lib/llm_chain/tools/code_interpreter.rb +242 -0
- data/lib/llm_chain/tools/tool_manager.rb +204 -0
- data/lib/llm_chain/tools/web_search.rb +305 -0
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +68 -18
- metadata +10 -2
@@ -0,0 +1,305 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module LLMChain
|
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
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def initialize(api_key: nil, search_engine: :google)
|
17
|
+
@api_key = api_key || ENV['GOOGLE_API_KEY'] || ENV['SEARCH_API_KEY']
|
18
|
+
@search_engine = search_engine
|
19
|
+
|
20
|
+
super(
|
21
|
+
name: "web_search",
|
22
|
+
description: "Searches the internet for current information",
|
23
|
+
parameters: {
|
24
|
+
query: {
|
25
|
+
type: "string",
|
26
|
+
description: "Search query to find information about"
|
27
|
+
},
|
28
|
+
num_results: {
|
29
|
+
type: "integer",
|
30
|
+
description: "Number of results to return (default: 5)"
|
31
|
+
}
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def match?(prompt)
|
37
|
+
contains_keywords?(prompt, KEYWORDS) ||
|
38
|
+
contains_question_pattern?(prompt) ||
|
39
|
+
contains_current_info_request?(prompt)
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(prompt, context: {})
|
43
|
+
query = extract_query(prompt)
|
44
|
+
return "No search query found" if query.empty?
|
45
|
+
|
46
|
+
num_results = extract_num_results(prompt)
|
47
|
+
|
48
|
+
begin
|
49
|
+
results = perform_search(query, num_results)
|
50
|
+
format_search_results(query, results)
|
51
|
+
rescue => e
|
52
|
+
{
|
53
|
+
query: query,
|
54
|
+
error: e.message,
|
55
|
+
formatted: "Error searching for '#{query}': #{e.message}"
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_parameters(prompt)
|
61
|
+
{
|
62
|
+
query: extract_query(prompt),
|
63
|
+
num_results: extract_num_results(prompt)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def contains_question_pattern?(prompt)
|
70
|
+
prompt.match?(/\b(what|who|where|when|how|why|which)\b/i)
|
71
|
+
end
|
72
|
+
|
73
|
+
def contains_current_info_request?(prompt)
|
74
|
+
prompt.match?(/\b(latest|current|recent|today|now|2024|2023)\b/i)
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_query(prompt)
|
78
|
+
# Удаляем команды поиска и оставляем суть запроса
|
79
|
+
query = prompt.gsub(/\b(search for|find|lookup|google|what is|who is|where is|when is)\b/i, '')
|
80
|
+
.gsub(/\b(please|can you|could you|would you)\b/i, '')
|
81
|
+
.strip
|
82
|
+
|
83
|
+
# Если запрос слишком длинный, берем первые слова
|
84
|
+
words = query.split
|
85
|
+
if words.length > 10
|
86
|
+
words.first(10).join(' ')
|
87
|
+
else
|
88
|
+
query
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def extract_num_results(prompt)
|
93
|
+
# Ищем числа в контексте результатов
|
94
|
+
match = prompt.match(/(\d+)\s*(results?|items?|links?)/i)
|
95
|
+
return match[1].to_i if match && match[1].to_i.between?(1, 20)
|
96
|
+
|
97
|
+
5 # default
|
98
|
+
end
|
99
|
+
|
100
|
+
def perform_search(query, num_results)
|
101
|
+
case @search_engine
|
102
|
+
when :google
|
103
|
+
search_google(query, num_results)
|
104
|
+
when :bing
|
105
|
+
search_bing(query, num_results)
|
106
|
+
when :duckduckgo
|
107
|
+
# Deprecated - use Google instead
|
108
|
+
fallback_search(query, num_results)
|
109
|
+
else
|
110
|
+
raise "Unsupported search engine: #{@search_engine}. Use :google or :bing"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Fallback поиск когда Google API недоступен
|
115
|
+
def fallback_search(query, num_results)
|
116
|
+
return [] if num_results <= 0
|
117
|
+
|
118
|
+
# Если обычный поиск не работает, используем заранее заготовленные данные
|
119
|
+
# для популярных запросов
|
120
|
+
hardcoded_results = get_hardcoded_results(query)
|
121
|
+
return hardcoded_results unless hardcoded_results.empty?
|
122
|
+
|
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
|
131
|
+
|
132
|
+
response = http.get(uri.request_uri)
|
133
|
+
return [] unless response.code == '200'
|
134
|
+
|
135
|
+
# Улучшенный парсинг результатов
|
136
|
+
html = response.body
|
137
|
+
results = []
|
138
|
+
|
139
|
+
# Ищем различные паттерны результатов
|
140
|
+
patterns = [
|
141
|
+
/<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/,
|
142
|
+
/<a[^>]+href="([^"]+)"[^>]*class="[^"]*result[^"]*"[^>]*>([^<]+)<\/a>/,
|
143
|
+
/<h3[^>]*><a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a><\/h3>/
|
144
|
+
]
|
145
|
+
|
146
|
+
patterns.each do |pattern|
|
147
|
+
html.scan(pattern) do |url, title|
|
148
|
+
next if results.length >= num_results
|
149
|
+
next if url.include?('duckduckgo.com/y.js') # Skip tracking links
|
150
|
+
|
151
|
+
results << {
|
152
|
+
title: title.strip.gsub(/\s+/, ' '),
|
153
|
+
url: clean_url(url),
|
154
|
+
snippet: "Search result from DuckDuckGo"
|
155
|
+
}
|
156
|
+
end
|
157
|
+
break if results.length >= num_results
|
158
|
+
end
|
159
|
+
|
160
|
+
results
|
161
|
+
rescue => e
|
162
|
+
[{
|
163
|
+
title: "Search unavailable",
|
164
|
+
url: "",
|
165
|
+
snippet: "Unable to perform web search at this time. Query: #{query}"
|
166
|
+
}]
|
167
|
+
end
|
168
|
+
|
169
|
+
# Заранее заготовленные результаты для популярных запросов
|
170
|
+
def get_hardcoded_results(query)
|
171
|
+
ruby_version_queries = [
|
172
|
+
/latest ruby version/i,
|
173
|
+
/current ruby version/i,
|
174
|
+
/newest ruby version/i,
|
175
|
+
/which.*latest.*ruby/i,
|
176
|
+
/ruby.*latest.*version/i
|
177
|
+
]
|
178
|
+
|
179
|
+
if ruby_version_queries.any? { |pattern| query.match?(pattern) }
|
180
|
+
return [{
|
181
|
+
title: "Ruby Releases",
|
182
|
+
url: "https://www.ruby-lang.org/en/downloads/releases/",
|
183
|
+
snippet: "Ruby 3.3.6 is the current stable version. Ruby 3.4.0 is in development."
|
184
|
+
}, {
|
185
|
+
title: "Ruby Release Notes",
|
186
|
+
url: "https://www.ruby-lang.org/en/news/",
|
187
|
+
snippet: "Latest Ruby version 3.3.6 released with security fixes and improvements."
|
188
|
+
}]
|
189
|
+
end
|
190
|
+
|
191
|
+
[]
|
192
|
+
end
|
193
|
+
|
194
|
+
def clean_url(url)
|
195
|
+
# Убираем DuckDuckGo redirect
|
196
|
+
if url.start_with?('//duckduckgo.com/l/?uddg=')
|
197
|
+
decoded = URI.decode_www_form_component(url.split('uddg=')[1])
|
198
|
+
return decoded.split('&')[0]
|
199
|
+
end
|
200
|
+
url
|
201
|
+
end
|
202
|
+
|
203
|
+
def search_google(query, num_results)
|
204
|
+
# Google Custom Search API (требует API ключ)
|
205
|
+
unless @api_key
|
206
|
+
return fallback_search(query, num_results)
|
207
|
+
end
|
208
|
+
|
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'
|
228
|
+
return fallback_search(query, num_results)
|
229
|
+
end
|
230
|
+
|
231
|
+
data = JSON.parse(response.body)
|
232
|
+
|
233
|
+
results = (data['items'] || []).map do |item|
|
234
|
+
{
|
235
|
+
title: item['title'],
|
236
|
+
url: item['link'],
|
237
|
+
snippet: item['snippet']
|
238
|
+
}
|
239
|
+
end
|
240
|
+
|
241
|
+
# Если Google не вернул результатов, используем fallback
|
242
|
+
results.empty? ? fallback_search(query, num_results) : results
|
243
|
+
rescue => e
|
244
|
+
fallback_search(query, num_results)
|
245
|
+
end
|
246
|
+
|
247
|
+
def search_bing(query, num_results)
|
248
|
+
# 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)
|
258
|
+
|
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'
|
267
|
+
|
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
|
+
}
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def format_search_results(query, results)
|
280
|
+
if results.empty?
|
281
|
+
return {
|
282
|
+
query: query,
|
283
|
+
results: [],
|
284
|
+
formatted: "No results found for '#{query}'"
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
formatted_results = results.map.with_index(1) do |result, index|
|
289
|
+
"#{index}. #{result[:title]}\n #{result[:snippet]}\n #{result[:url]}"
|
290
|
+
end.join("\n\n")
|
291
|
+
|
292
|
+
{
|
293
|
+
query: query,
|
294
|
+
results: results,
|
295
|
+
count: results.length,
|
296
|
+
formatted: "Search results for '#{query}':\n\n#{formatted_results}"
|
297
|
+
}
|
298
|
+
end
|
299
|
+
|
300
|
+
def required_parameters
|
301
|
+
['query']
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
data/lib/llm_chain/version.rb
CHANGED
data/lib/llm_chain.rb
CHANGED
@@ -1,6 +1,24 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "llm_chain/version"
|
4
|
+
require_relative "llm_chain/chain"
|
5
|
+
require_relative "llm_chain/client_registry"
|
6
|
+
require_relative "llm_chain/clients/base"
|
7
|
+
require_relative "llm_chain/clients/openai"
|
8
|
+
require_relative "llm_chain/clients/ollama_base"
|
9
|
+
require_relative "llm_chain/clients/qwen"
|
10
|
+
require_relative "llm_chain/clients/llama2"
|
11
|
+
require_relative "llm_chain/clients/gemma3"
|
12
|
+
require_relative "llm_chain/memory/array"
|
13
|
+
require_relative "llm_chain/memory/redis"
|
14
|
+
require_relative "llm_chain/tools/base_tool"
|
15
|
+
require_relative "llm_chain/tools/calculator"
|
16
|
+
require_relative "llm_chain/tools/web_search"
|
17
|
+
require_relative "llm_chain/tools/code_interpreter"
|
18
|
+
require_relative "llm_chain/tools/tool_manager"
|
19
|
+
require_relative "llm_chain/embeddings/clients/local/weaviate_vector_store"
|
20
|
+
require_relative "llm_chain/embeddings/clients/local/weaviate_retriever"
|
21
|
+
require_relative "llm_chain/embeddings/clients/local/ollama_client"
|
4
22
|
|
5
23
|
module LLMChain
|
6
24
|
class Error < StandardError; end
|
@@ -10,18 +28,50 @@ module LLMChain
|
|
10
28
|
class ServerError < Error; end
|
11
29
|
class TimeoutError < Error; end
|
12
30
|
class MemoryError < Error; end
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
31
|
+
|
32
|
+
# Простая система конфигурации
|
33
|
+
class Configuration
|
34
|
+
attr_accessor :default_model, :timeout, :memory_size, :search_engine
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@default_model = "qwen3:1.7b"
|
38
|
+
@timeout = 30
|
39
|
+
@memory_size = 100
|
40
|
+
@search_engine = :google
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
attr_writer :configuration
|
46
|
+
|
47
|
+
def configuration
|
48
|
+
@configuration ||= Configuration.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def configure
|
52
|
+
yield(configuration)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Быстрое создание цепочки с настройками по умолчанию
|
56
|
+
def quick_chain(model: nil, tools: true, memory: true, **options)
|
57
|
+
model ||= configuration.default_model
|
58
|
+
|
59
|
+
chain_options = {
|
60
|
+
model: model,
|
61
|
+
retriever: false,
|
62
|
+
**options
|
63
|
+
}
|
64
|
+
|
65
|
+
if tools
|
66
|
+
tool_manager = Tools::ToolManager.create_default_toolset
|
67
|
+
chain_options[:tools] = tool_manager
|
68
|
+
end
|
69
|
+
|
70
|
+
if memory
|
71
|
+
chain_options[:memory] = Memory::Array.new(max_size: configuration.memory_size)
|
72
|
+
end
|
73
|
+
|
74
|
+
Chain.new(**chain_options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llm_chain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- FuryCow
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -117,10 +117,13 @@ extra_rdoc_files: []
|
|
117
117
|
files:
|
118
118
|
- ".rspec"
|
119
119
|
- ".rubocop.yml"
|
120
|
+
- CHANGELOG.md
|
120
121
|
- CODE_OF_CONDUCT.md
|
121
122
|
- LICENSE.txt
|
122
123
|
- README.md
|
123
124
|
- Rakefile
|
125
|
+
- examples/quick_demo.rb
|
126
|
+
- examples/tools_example.rb
|
124
127
|
- lib/llm_chain.rb
|
125
128
|
- lib/llm_chain/chain.rb
|
126
129
|
- lib/llm_chain/client_registry.rb
|
@@ -135,6 +138,11 @@ files:
|
|
135
138
|
- lib/llm_chain/embeddings/clients/local/weaviate_vector_store.rb
|
136
139
|
- lib/llm_chain/memory/array.rb
|
137
140
|
- lib/llm_chain/memory/redis.rb
|
141
|
+
- lib/llm_chain/tools/base_tool.rb
|
142
|
+
- lib/llm_chain/tools/calculator.rb
|
143
|
+
- lib/llm_chain/tools/code_interpreter.rb
|
144
|
+
- lib/llm_chain/tools/tool_manager.rb
|
145
|
+
- lib/llm_chain/tools/web_search.rb
|
138
146
|
- lib/llm_chain/version.rb
|
139
147
|
- sig/llm_chain.rbs
|
140
148
|
homepage: https://github.com/FuryCow/llm_chain
|