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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmChain
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/llm_chain.rb CHANGED
@@ -1,6 +1,24 @@
1
- require "llm_chain/version"
2
- require "faraday"
3
- require "json"
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
- end
14
-
15
- require "llm_chain/clients/base"
16
- require "llm_chain/clients/ollama_base"
17
- require "llm_chain/clients/openai"
18
- require "llm_chain/clients/qwen"
19
- require "llm_chain/clients/llama2"
20
- require "llm_chain/clients/gemma3"
21
- require "llm_chain/client_registry"
22
- require "llm_chain/memory/array"
23
- require "llm_chain/memory/redis"
24
- require "llm_chain/embeddings/clients/local/ollama_client"
25
- require "llm_chain/embeddings/clients/local/weaviate_vector_store"
26
- require "llm_chain/embeddings/clients/local/weaviate_retriever"
27
- require "llm_chain/chain"
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.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-23 00:00:00.000000000 Z
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