llm_chain 0.3.0 → 0.5.0
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/README.md +466 -101
- data/examples/quick_demo.rb +93 -0
- data/examples/tools_example.rb +255 -0
- data/lib/llm_chain/chain.rb +58 -42
- data/lib/llm_chain/client_registry.rb +2 -0
- data/lib/llm_chain/clients/gemma3.rb +144 -0
- data/lib/llm_chain/clients/qwen.rb +14 -3
- data/lib/llm_chain/tools/base_tool.rb +81 -0
- data/lib/llm_chain/tools/calculator.rb +143 -0
- data/lib/llm_chain/tools/code_interpreter.rb +233 -0
- data/lib/llm_chain/tools/tool_manager.rb +204 -0
- data/lib/llm_chain/tools/web_search.rb +255 -0
- data/lib/llm_chain/version.rb +1 -1
- data/lib/llm_chain.rb +6 -0
- metadata +11 -3
@@ -0,0 +1,255 @@
|
|
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: :duckduckgo)
|
17
|
+
@api_key = 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 :duckduckgo
|
103
|
+
search_duckduckgo(query, num_results)
|
104
|
+
when :google
|
105
|
+
search_google(query, num_results)
|
106
|
+
when :bing
|
107
|
+
search_bing(query, num_results)
|
108
|
+
else
|
109
|
+
raise "Unsupported search engine: #{@search_engine}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def search_duckduckgo(query, num_results)
|
114
|
+
# DuckDuckGo Instant Answer API (бесплатный)
|
115
|
+
uri = URI("https://api.duckduckgo.com/")
|
116
|
+
params = {
|
117
|
+
q: query,
|
118
|
+
format: 'json',
|
119
|
+
no_html: '1',
|
120
|
+
skip_disambig: '1'
|
121
|
+
}
|
122
|
+
uri.query = URI.encode_www_form(params)
|
123
|
+
|
124
|
+
response = Net::HTTP.get_response(uri)
|
125
|
+
raise "DuckDuckGo API error: #{response.code}" unless response.code == '200'
|
126
|
+
|
127
|
+
data = JSON.parse(response.body)
|
128
|
+
|
129
|
+
results = []
|
130
|
+
|
131
|
+
# Основной ответ
|
132
|
+
if data['AbstractText'] && !data['AbstractText'].empty?
|
133
|
+
results << {
|
134
|
+
title: data['AbstractSource'] || 'DuckDuckGo',
|
135
|
+
url: data['AbstractURL'] || '',
|
136
|
+
snippet: data['AbstractText']
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
# Связанные темы
|
141
|
+
if data['RelatedTopics']
|
142
|
+
data['RelatedTopics'].first(num_results - results.length).each do |topic|
|
143
|
+
next unless topic['Text']
|
144
|
+
results << {
|
145
|
+
title: topic['Text'].split(' - ').first || 'Related',
|
146
|
+
url: topic['FirstURL'] || '',
|
147
|
+
snippet: topic['Text']
|
148
|
+
}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Если результатов мало, добавляем информацию из Infobox
|
153
|
+
if results.length < num_results / 2 && data['Infobox']
|
154
|
+
infobox_text = data['Infobox']['content']&.map { |item|
|
155
|
+
"#{item['label']}: #{item['value']}"
|
156
|
+
}&.join('; ')
|
157
|
+
|
158
|
+
if infobox_text
|
159
|
+
results << {
|
160
|
+
title: 'Information',
|
161
|
+
url: data['AbstractURL'] || '',
|
162
|
+
snippet: infobox_text
|
163
|
+
}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
results.first(num_results)
|
168
|
+
end
|
169
|
+
|
170
|
+
def search_google(query, num_results)
|
171
|
+
# Google Custom Search API (требует API ключ)
|
172
|
+
raise "Google API key required" unless @api_key
|
173
|
+
|
174
|
+
uri = URI("https://www.googleapis.com/customsearch/v1")
|
175
|
+
params = {
|
176
|
+
key: @api_key,
|
177
|
+
cx: ENV['GOOGLE_SEARCH_ENGINE_ID'] || raise("GOOGLE_SEARCH_ENGINE_ID required"),
|
178
|
+
q: query,
|
179
|
+
num: [num_results, 10].min
|
180
|
+
}
|
181
|
+
uri.query = URI.encode_www_form(params)
|
182
|
+
|
183
|
+
response = Net::HTTP.get_response(uri)
|
184
|
+
raise "Google API error: #{response.code}" unless response.code == '200'
|
185
|
+
|
186
|
+
data = JSON.parse(response.body)
|
187
|
+
|
188
|
+
(data['items'] || []).map do |item|
|
189
|
+
{
|
190
|
+
title: item['title'],
|
191
|
+
url: item['link'],
|
192
|
+
snippet: item['snippet']
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def search_bing(query, num_results)
|
198
|
+
# Bing Web Search API (требует API ключ)
|
199
|
+
raise "Bing API key required" unless @api_key
|
200
|
+
|
201
|
+
uri = URI("https://api.bing.microsoft.com/v7.0/search")
|
202
|
+
params = {
|
203
|
+
q: query,
|
204
|
+
count: [num_results, 20].min,
|
205
|
+
responseFilter: 'Webpages'
|
206
|
+
}
|
207
|
+
uri.query = URI.encode_www_form(params)
|
208
|
+
|
209
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
210
|
+
http.use_ssl = true
|
211
|
+
|
212
|
+
request = Net::HTTP::Get.new(uri)
|
213
|
+
request['Ocp-Apim-Subscription-Key'] = @api_key
|
214
|
+
|
215
|
+
response = http.request(request)
|
216
|
+
raise "Bing API error: #{response.code}" unless response.code == '200'
|
217
|
+
|
218
|
+
data = JSON.parse(response.body)
|
219
|
+
|
220
|
+
(data.dig('webPages', 'value') || []).map do |item|
|
221
|
+
{
|
222
|
+
title: item['name'],
|
223
|
+
url: item['url'],
|
224
|
+
snippet: item['snippet']
|
225
|
+
}
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def format_search_results(query, results)
|
230
|
+
if results.empty?
|
231
|
+
return {
|
232
|
+
query: query,
|
233
|
+
results: [],
|
234
|
+
formatted: "No results found for '#{query}'"
|
235
|
+
}
|
236
|
+
end
|
237
|
+
|
238
|
+
formatted_results = results.map.with_index(1) do |result, index|
|
239
|
+
"#{index}. #{result[:title]}\n #{result[:snippet]}\n #{result[:url]}"
|
240
|
+
end.join("\n\n")
|
241
|
+
|
242
|
+
{
|
243
|
+
query: query,
|
244
|
+
results: results,
|
245
|
+
count: results.length,
|
246
|
+
formatted: "Search results for '#{query}':\n\n#{formatted_results}"
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
def required_parameters
|
251
|
+
['query']
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
data/lib/llm_chain/version.rb
CHANGED
data/lib/llm_chain.rb
CHANGED
@@ -17,10 +17,16 @@ require "llm_chain/clients/ollama_base"
|
|
17
17
|
require "llm_chain/clients/openai"
|
18
18
|
require "llm_chain/clients/qwen"
|
19
19
|
require "llm_chain/clients/llama2"
|
20
|
+
require "llm_chain/clients/gemma3"
|
20
21
|
require "llm_chain/client_registry"
|
21
22
|
require "llm_chain/memory/array"
|
22
23
|
require "llm_chain/memory/redis"
|
23
24
|
require "llm_chain/embeddings/clients/local/ollama_client"
|
24
25
|
require "llm_chain/embeddings/clients/local/weaviate_vector_store"
|
25
26
|
require "llm_chain/embeddings/clients/local/weaviate_retriever"
|
27
|
+
require "llm_chain/tools/base_tool"
|
28
|
+
require "llm_chain/tools/calculator"
|
29
|
+
require "llm_chain/tools/web_search"
|
30
|
+
require "llm_chain/tools/code_interpreter"
|
31
|
+
require "llm_chain/tools/tool_manager"
|
26
32
|
require "llm_chain/chain"
|
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.0
|
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
|
@@ -121,10 +121,13 @@ files:
|
|
121
121
|
- LICENSE.txt
|
122
122
|
- README.md
|
123
123
|
- Rakefile
|
124
|
+
- examples/quick_demo.rb
|
125
|
+
- examples/tools_example.rb
|
124
126
|
- lib/llm_chain.rb
|
125
127
|
- lib/llm_chain/chain.rb
|
126
128
|
- lib/llm_chain/client_registry.rb
|
127
129
|
- lib/llm_chain/clients/base.rb
|
130
|
+
- lib/llm_chain/clients/gemma3.rb
|
128
131
|
- lib/llm_chain/clients/llama2.rb
|
129
132
|
- lib/llm_chain/clients/ollama_base.rb
|
130
133
|
- lib/llm_chain/clients/openai.rb
|
@@ -134,6 +137,11 @@ files:
|
|
134
137
|
- lib/llm_chain/embeddings/clients/local/weaviate_vector_store.rb
|
135
138
|
- lib/llm_chain/memory/array.rb
|
136
139
|
- lib/llm_chain/memory/redis.rb
|
140
|
+
- lib/llm_chain/tools/base_tool.rb
|
141
|
+
- lib/llm_chain/tools/calculator.rb
|
142
|
+
- lib/llm_chain/tools/code_interpreter.rb
|
143
|
+
- lib/llm_chain/tools/tool_manager.rb
|
144
|
+
- lib/llm_chain/tools/web_search.rb
|
137
145
|
- lib/llm_chain/version.rb
|
138
146
|
- sig/llm_chain.rbs
|
139
147
|
homepage: https://github.com/FuryCow/llm_chain
|
@@ -160,5 +168,5 @@ requirements: []
|
|
160
168
|
rubygems_version: 3.4.10
|
161
169
|
signing_key:
|
162
170
|
specification_version: 4
|
163
|
-
summary: Ruby
|
171
|
+
summary: Ruby-analog LangChain to work with LLM
|
164
172
|
test_files: []
|