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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmChain
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.3.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-16 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
@@ -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-аналог LangChain для работы с LLM
171
+ summary: Ruby-analog LangChain to work with LLM
164
172
  test_files: []