llm_chain 0.4.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,93 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/llm_chain'
4
+
5
+ puts "🦾 LLMChain v#{LlmChain::VERSION} - Quick Demo"
6
+ puts "=" * 50
7
+
8
+ # 1. Simple chain without tools
9
+ puts "\n1. 💬 Simple conversation"
10
+ begin
11
+ simple_chain = LLMChain::Chain.new(
12
+ model: "qwen3:1.7b",
13
+ retriever: false
14
+ )
15
+
16
+ response = simple_chain.ask("Hello! Tell me briefly about yourself.")
17
+ puts "🤖 #{response}"
18
+ rescue => e
19
+ puts "❌ Error: #{e.message}"
20
+ puts "💡 Make sure Ollama is running and qwen3:1.7b model is downloaded"
21
+ end
22
+
23
+ # 2. Calculator
24
+ puts "\n2. 🧮 Built-in calculator"
25
+ calculator = LLMChain::Tools::Calculator.new
26
+ result = calculator.call("Calculate 25 * 8 + 15")
27
+ puts "📊 #{result[:formatted]}"
28
+
29
+ # 3. Code interpreter
30
+ puts "\n3. 💻 Code interpreter"
31
+ begin
32
+ interpreter = LLMChain::Tools::CodeInterpreter.new
33
+ ruby_code = <<~RUBY_CODE
34
+ ```ruby
35
+ # Simple program
36
+ data = [1, 2, 3, 4, 5]
37
+ total = data.sum
38
+ puts "Sum of numbers: \#{total}"
39
+ puts "Average: \#{total / data.size.to_f}"
40
+ ```
41
+ RUBY_CODE
42
+
43
+ code_result = interpreter.call(ruby_code)
44
+
45
+ if code_result.is_a?(Hash) && code_result[:result]
46
+ puts "✅ Execution result:"
47
+ puts code_result[:result]
48
+ elsif code_result.is_a?(Hash) && code_result[:error]
49
+ puts "❌ #{code_result[:error]}"
50
+ else
51
+ puts "📝 #{code_result}"
52
+ end
53
+ rescue => e
54
+ puts "❌ Interpreter error: #{e.message}"
55
+ end
56
+
57
+ # 4. Web search (may not work without internet)
58
+ puts "\n4. 🔍 Web search"
59
+ search = LLMChain::Tools::WebSearch.new
60
+ search_result = search.call("Ruby programming language")
61
+
62
+ if search_result.is_a?(Hash) && search_result[:results] && !search_result[:results].empty?
63
+ puts "🌐 Found #{search_result[:count]} results:"
64
+ search_result[:results].first(2).each_with_index do |result, i|
65
+ puts " #{i+1}. #{result[:title]}"
66
+ end
67
+ else
68
+ puts "❌ Search failed or no results found"
69
+ end
70
+
71
+ # 5. Chain with tools
72
+ puts "\n5. 🛠️ Chain with automatic tools"
73
+ begin
74
+ tool_manager = LLMChain::Tools::ToolManager.create_default_toolset
75
+ smart_chain = LLMChain::Chain.new(
76
+ model: "qwen3:1.7b",
77
+ tools: tool_manager,
78
+ retriever: false
79
+ )
80
+
81
+ puts "\n🧮 Math test:"
82
+ math_response = smart_chain.ask("How much is 12 * 15?")
83
+ puts "🤖 #{math_response}"
84
+
85
+ rescue => e
86
+ puts "❌ Tools error: #{e.message}"
87
+ end
88
+
89
+ puts "\n" + "=" * 50
90
+ puts "✨ Demo completed!"
91
+ puts "\n📖 More examples:"
92
+ puts " - ruby -I lib examples/tools_example.rb"
93
+ puts " - See README.md for complete documentation"
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/llm_chain'
4
+
5
+ puts "🛠️ LLMChain Tools Demo"
6
+ puts "=" * 40
7
+
8
+ # 1. Individual Tool Usage
9
+ puts "\n1. 🧮 Calculator Tool"
10
+ calculator = LLMChain::Tools::Calculator.new
11
+
12
+ # Test mathematical expressions
13
+ examples = [
14
+ "15 * 7 + 3",
15
+ "sqrt(144)",
16
+ "2 * 2 * 2"
17
+ ]
18
+
19
+ examples.each do |expr|
20
+ result = calculator.call("Calculate: #{expr}")
21
+ puts "📊 #{result[:formatted]}"
22
+ end
23
+
24
+ # 2. Web Search Tool
25
+ puts "\n2. 🔍 Web Search Tool"
26
+ search = LLMChain::Tools::WebSearch.new
27
+
28
+ search_queries = [
29
+ "Ruby programming language",
30
+ "Machine learning basics"
31
+ ]
32
+
33
+ search_queries.each do |query|
34
+ puts "\n🔍 Searching: #{query}"
35
+ result = search.call(query)
36
+
37
+ if result[:results] && result[:results].any?
38
+ puts "Found #{result[:count]} results:"
39
+ result[:results].first(2).each_with_index do |item, i|
40
+ puts " #{i+1}. #{item[:title]}"
41
+ puts " #{item[:url]}" if item[:url]
42
+ end
43
+ else
44
+ puts "No results found"
45
+ end
46
+ end
47
+
48
+ # 3. Code Interpreter Tool
49
+ puts "\n3. 💻 Code Interpreter Tool"
50
+ interpreter = LLMChain::Tools::CodeInterpreter.new
51
+
52
+ # Test Ruby code execution
53
+ ruby_examples = [
54
+ <<~RUBY,
55
+ ```ruby
56
+ # Simple Fibonacci calculation
57
+ a, b = 0, 1
58
+ result = [a, b]
59
+ 8.times do
60
+ c = a + b
61
+ result << c
62
+ a, b = b, c
63
+ end
64
+
65
+ puts "Fibonacci sequence (first 10 numbers):"
66
+ puts result.join(" ")
67
+ ```
68
+ RUBY
69
+
70
+ <<~RUBY
71
+ ```ruby
72
+ # Data analysis example
73
+ numbers = [23, 45, 67, 89, 12, 34, 56, 78]
74
+
75
+ puts "Dataset: \#{numbers}"
76
+ puts "Sum: \#{numbers.sum}"
77
+ puts "Average: \#{numbers.sum.to_f / numbers.size}"
78
+ puts "Max: \#{numbers.max}"
79
+ puts "Min: \#{numbers.min}"
80
+ ```
81
+ RUBY
82
+ ]
83
+
84
+ ruby_examples.each_with_index do |code, i|
85
+ puts "\n💻 Ruby Example #{i+1}:"
86
+ begin
87
+ result = interpreter.call(code)
88
+
89
+ if result.is_a?(Hash) && result[:result]
90
+ puts "✅ Output:"
91
+ puts result[:result]
92
+ elsif result.is_a?(Hash) && result[:error]
93
+ puts "❌ Error: #{result[:error]}"
94
+ else
95
+ puts "📝 #{result}"
96
+ end
97
+ rescue => e
98
+ puts "❌ Execution error: #{e.message}"
99
+ end
100
+ end
101
+
102
+ # 4. Tool Manager Usage
103
+ puts "\n4. 🎯 Tool Manager"
104
+ tool_manager = LLMChain::Tools::ToolManager.create_default_toolset
105
+
106
+ puts "Registered tools: #{tool_manager.list_tools.map(&:name).join(', ')}"
107
+
108
+ # Test tool matching
109
+ test_prompts = [
110
+ "What is 25 squared?",
111
+ "Find information about Ruby gems",
112
+ "Run this code: puts 'Hello World'"
113
+ ]
114
+
115
+ test_prompts.each do |prompt|
116
+ puts "\n🎯 Testing: \"#{prompt}\""
117
+ matched_tools = tool_manager.list_tools.select { |tool| tool.match?(prompt) }
118
+
119
+ if matched_tools.any?
120
+ puts " Matched tools: #{matched_tools.map(&:name).join(', ')}"
121
+
122
+ # Execute with first matched tool
123
+ tool = matched_tools.first
124
+ result = tool.call(prompt)
125
+ puts " Result: #{result[:formatted] || result.inspect}"
126
+ else
127
+ puts " No tools matched"
128
+ end
129
+ end
130
+
131
+ # 5. LLM Chain with Tools
132
+ puts "\n5. 🤖 LLM Chain with Tools"
133
+
134
+ begin
135
+ # Create chain with tools (but without retriever for local testing)
136
+ chain = LLMChain::Chain.new(
137
+ model: "qwen3:1.7b",
138
+ tools: tool_manager,
139
+ retriever: false # Disable RAG for this example
140
+ )
141
+
142
+ # Test queries that should trigger tools
143
+ test_queries = [
144
+ "Calculate the area of a circle with radius 5 (use pi = 3.14159)",
145
+ "What's the latest news about Ruby programming?",
146
+ "Execute this Ruby code: puts (1..100).select(&:even?).sum"
147
+ ]
148
+
149
+ test_queries.each_with_index do |query, i|
150
+ puts "\n🤖 Query #{i+1}: #{query}"
151
+ puts "🔄 Processing..."
152
+
153
+ response = chain.ask(query)
154
+ puts "📝 Response: #{response}"
155
+ puts "-" * 40
156
+ end
157
+
158
+ rescue => e
159
+ puts "❌ Error with LLM Chain: #{e.message}"
160
+ puts "💡 Make sure Ollama is running with qwen3:1.7b model"
161
+ end
162
+
163
+ # 6. Custom Tool Example
164
+ puts "\n6. 🔧 Custom Tool Example"
165
+
166
+ # Create a simple DateTime tool
167
+ class DateTimeTool < LLMChain::Tools::BaseTool
168
+ def initialize
169
+ super(
170
+ name: "datetime",
171
+ description: "Gets current date and time information",
172
+ parameters: {
173
+ format: {
174
+ type: "string",
175
+ description: "Date format (optional)"
176
+ }
177
+ }
178
+ )
179
+ end
180
+
181
+ def match?(prompt)
182
+ contains_keywords?(prompt, ['time', 'date', 'now', 'current', 'today'])
183
+ end
184
+
185
+ def call(prompt, context: {})
186
+ now = Time.now
187
+
188
+ # Try to detect desired format from prompt
189
+ format = if prompt.match?(/iso|standard/i)
190
+ now.iso8601
191
+ elsif prompt.match?(/human|readable/i)
192
+ now.strftime("%B %d, %Y at %I:%M %p")
193
+ else
194
+ now.to_s
195
+ end
196
+
197
+ {
198
+ timestamp: now.to_i,
199
+ formatted_time: format,
200
+ timezone: now.zone,
201
+ formatted: "Current time: #{format} (#{now.zone})"
202
+ }
203
+ end
204
+ end
205
+
206
+ # Test custom tool
207
+ datetime_tool = DateTimeTool.new
208
+ time_queries = [
209
+ "What time is it now?",
210
+ "Give me current date in human readable format",
211
+ "Show me the current time in ISO format"
212
+ ]
213
+
214
+ time_queries.each do |query|
215
+ puts "\n🕐 Query: #{query}"
216
+ if datetime_tool.match?(query)
217
+ result = datetime_tool.call(query)
218
+ puts " #{result[:formatted]}"
219
+ else
220
+ puts " Query didn't match datetime tool"
221
+ end
222
+ end
223
+
224
+ # 7. Configuration-based Tool Setup
225
+ puts "\n7. ⚙️ Configuration-based Tools"
226
+
227
+ tools_config = [
228
+ {
229
+ class: 'calculator'
230
+ },
231
+ {
232
+ class: 'web_search',
233
+ options: {
234
+ search_engine: :duckduckgo
235
+ }
236
+ },
237
+ {
238
+ class: 'code_interpreter',
239
+ options: {
240
+ timeout: 30,
241
+ allowed_languages: ['ruby', 'python']
242
+ }
243
+ }
244
+ ]
245
+
246
+ config_tool_manager = LLMChain::Tools::ToolManager.from_config(tools_config)
247
+ puts "Tools from config: #{config_tool_manager.list_tools.map(&:name).join(', ')}"
248
+
249
+ # Test configuration-based setup
250
+ config_result = config_tool_manager.execute_tool('calculator', 'What is 99 * 99?')
251
+ puts "Config test result: #{config_result[:formatted]}" if config_result
252
+
253
+ puts "\n" + "=" * 40
254
+ puts "✨ Tools demo completed!"
255
+ puts "\nTry running the chain with: ruby -I lib examples/quick_demo.rb"
@@ -56,11 +56,21 @@ module LLMChain
56
56
  end
57
57
 
58
58
  def process_tools(prompt)
59
- @tools.each_with_object({}) do |tool, acc|
60
- if tool.match?(prompt)
61
- response = tool.call(prompt)
62
- acc[tool.name] = response unless response.nil?
59
+ return {} if @tools.nil? || (@tools.respond_to?(:empty?) && @tools.empty?)
60
+
61
+ # Если @tools - это ToolManager
62
+ if @tools.respond_to?(:auto_execute)
63
+ @tools.auto_execute(prompt)
64
+ elsif @tools.is_a?(Array)
65
+ # Старая логика для массива инструментов
66
+ @tools.each_with_object({}) do |tool, acc|
67
+ if tool.match?(prompt)
68
+ response = tool.call(prompt)
69
+ acc[tool.name] = response unless response.nil?
70
+ end
63
71
  end
72
+ else
73
+ {}
64
74
  end
65
75
  end
66
76
 
@@ -94,7 +104,11 @@ module LLMChain
94
104
  def build_tool_responses(tool_responses)
95
105
  parts = ["Tool results:"]
96
106
  tool_responses.each do |name, response|
97
- parts << "#{name}: #{response}"
107
+ if response.is_a?(Hash) && response[:formatted]
108
+ parts << "#{name}: #{response[:formatted]}"
109
+ else
110
+ parts << "#{name}: #{response}"
111
+ end
98
112
  end
99
113
  parts.join("\n")
100
114
  end
@@ -10,6 +10,12 @@ module LLMChain
10
10
  default: "qwen:7b",
11
11
  versions: ["qwen:7b", "qwen:14b", "qwen:72b", "qwen:0.5b"]
12
12
  },
13
+ qwen2: {
14
+ default: "qwen2:1.5b",
15
+ versions: [
16
+ "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b"
17
+ ]
18
+ },
13
19
  qwen3: {
14
20
  default: "qwen3:latest",
15
21
  versions: [
@@ -100,7 +106,13 @@ module LLMChain
100
106
  private
101
107
 
102
108
  def model_version
103
- @model.start_with?('qwen3:') ? :qwen3 : :qwen
109
+ if @model.start_with?('qwen3:')
110
+ :qwen3
111
+ elsif @model.start_with?('qwen2:')
112
+ :qwen2
113
+ else
114
+ :qwen
115
+ end
104
116
  end
105
117
 
106
118
  def detect_default_model
@@ -0,0 +1,81 @@
1
+ module LLMChain
2
+ module Tools
3
+ class BaseTool
4
+ attr_reader :name, :description, :parameters
5
+
6
+ def initialize(name:, description:, parameters: {})
7
+ @name = name
8
+ @description = description
9
+ @parameters = parameters
10
+ end
11
+
12
+ # Проверяет, подходит ли инструмент для данного промпта
13
+ # @param prompt [String] Входной промпт от пользователя
14
+ # @return [Boolean] true если инструмент должен быть вызван
15
+ def match?(prompt)
16
+ raise NotImplementedError, "Subclasses must implement #match?"
17
+ end
18
+
19
+ # Выполняет инструмент
20
+ # @param prompt [String] Входной промпт от пользователя
21
+ # @param context [Hash] Дополнительный контекст
22
+ # @return [String, Hash] Результат выполнения инструмента
23
+ def call(prompt, context: {})
24
+ raise NotImplementedError, "Subclasses must implement #call"
25
+ end
26
+
27
+ # Возвращает JSON-схему для LLM
28
+ def to_schema
29
+ {
30
+ name: @name,
31
+ description: @description,
32
+ parameters: {
33
+ type: "object",
34
+ properties: @parameters,
35
+ required: required_parameters
36
+ }
37
+ }
38
+ end
39
+
40
+ # Извлекает параметры из промпта (для автоматического парсинга)
41
+ # @param prompt [String] Входной промпт
42
+ # @return [Hash] Извлеченные параметры
43
+ def extract_parameters(prompt)
44
+ {}
45
+ end
46
+
47
+ # Форматирует результат для включения в промпт
48
+ # @param result [Object] Результат выполнения инструмента
49
+ # @return [String] Форматированный результат
50
+ def format_result(result)
51
+ case result
52
+ when String then result
53
+ when Hash, Array then JSON.pretty_generate(result)
54
+ else result.to_s
55
+ end
56
+ end
57
+
58
+ protected
59
+
60
+ # Список обязательных параметров
61
+ def required_parameters
62
+ []
63
+ end
64
+
65
+ # Помощник для проверки ключевых слов в промпте
66
+ def contains_keywords?(prompt, keywords)
67
+ keywords.any? { |keyword| prompt.downcase.include?(keyword.downcase) }
68
+ end
69
+
70
+ # Помощник для извлечения числовых значений
71
+ def extract_numbers(text)
72
+ text.scan(/-?\d+\.?\d*/).map(&:to_f)
73
+ end
74
+
75
+ # Помощник для извлечения URL
76
+ def extract_urls(text)
77
+ text.scan(%r{https?://[^\s]+})
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,143 @@
1
+ require 'bigdecimal'
2
+
3
+ module LLMChain
4
+ module Tools
5
+ class Calculator < BaseTool
6
+ KEYWORDS = %w[
7
+ calculate compute math equation formula
8
+ add subtract multiply divide
9
+ sum difference product quotient
10
+ plus minus times divided
11
+ + - * / = equals
12
+ ].freeze
13
+
14
+ def initialize
15
+ super(
16
+ name: "calculator",
17
+ description: "Performs mathematical calculations and evaluates expressions",
18
+ parameters: {
19
+ expression: {
20
+ type: "string",
21
+ description: "Mathematical expression to evaluate (e.g., '2 + 2', '15 * 3.14', 'sqrt(16)')"
22
+ }
23
+ }
24
+ )
25
+ end
26
+
27
+ def match?(prompt)
28
+ contains_keywords?(prompt, KEYWORDS) ||
29
+ contains_math_pattern?(prompt)
30
+ end
31
+
32
+ def call(prompt, context: {})
33
+ expression = extract_expression(prompt)
34
+ return "No mathematical expression found" if expression.empty?
35
+
36
+ begin
37
+ result = evaluate_expression(expression)
38
+ {
39
+ expression: expression,
40
+ result: result,
41
+ formatted: "#{expression} = #{result}"
42
+ }
43
+ rescue => e
44
+ {
45
+ expression: expression,
46
+ error: e.message,
47
+ formatted: "Error calculating '#{expression}': #{e.message}"
48
+ }
49
+ end
50
+ end
51
+
52
+ def extract_parameters(prompt)
53
+ { expression: extract_expression(prompt) }
54
+ end
55
+
56
+ private
57
+
58
+ def contains_math_pattern?(prompt)
59
+ # Проверяем наличие математических операторов и чисел
60
+ prompt.match?(/\d+\s*[+\-*\/]\s*\d+/) ||
61
+ prompt.match?(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\(/i)
62
+ end
63
+
64
+ def extract_expression(prompt)
65
+ # Пробуем найти выражение в кавычках
66
+ quoted = prompt.match(/"([^"]+)"/) || prompt.match(/'([^']+)'/)
67
+ return quoted[1] if quoted
68
+
69
+ # Ищем выражение после ключевых слов
70
+ KEYWORDS.each do |keyword|
71
+ if prompt.downcase.include?(keyword)
72
+ escaped_keyword = Regexp.escape(keyword)
73
+ after_keyword = prompt.split(/#{escaped_keyword}/i, 2)[1]
74
+ if after_keyword
75
+ # Извлекаем математическое выражение
76
+ expr = after_keyword.strip.split(/[.!?]/).first
77
+ return clean_expression(expr) if expr
78
+ end
79
+ end
80
+ end
81
+
82
+ # Пробуем найти простое выражение в тексте
83
+ math_expr = prompt.match(/(\d+(?:\.\d+)?\s*[+\-*\/]\s*\d+(?:\.\d+)?(?:\s*[+\-*\/]\s*\d+(?:\.\d+)?)*)/)
84
+ return math_expr[1] if math_expr
85
+
86
+ # Ищем функции
87
+ func_expr = prompt.match(/\b(sqrt|sin|cos|tan|log|ln|exp|abs|round|ceil|floor)\s*\([^)]+\)/i)
88
+ return func_expr[0] if func_expr
89
+
90
+ ""
91
+ end
92
+
93
+ def clean_expression(expr)
94
+ # Удаляем лишние слова и оставляем только математическое выражение
95
+ expr.gsub(/\b(is|what|equals?|result|answer)\b/i, '')
96
+ .gsub(/[^\d+\-*\/().\s]/, '')
97
+ .strip
98
+ end
99
+
100
+ def evaluate_expression(expression)
101
+ # Заменяем математические функции на Ruby-методы
102
+ expr = expression.downcase
103
+ .gsub(/sqrt\s*\(([^)]+)\)/) { "Math.sqrt(#{$1})" }
104
+ .gsub(/sin\s*\(([^)]+)\)/) { "Math.sin(#{$1})" }
105
+ .gsub(/cos\s*\(([^)]+)\)/) { "Math.cos(#{$1})" }
106
+ .gsub(/tan\s*\(([^)]+)\)/) { "Math.tan(#{$1})" }
107
+ .gsub(/log\s*\(([^)]+)\)/) { "Math.log10(#{$1})" }
108
+ .gsub(/ln\s*\(([^)]+)\)/) { "Math.log(#{$1})" }
109
+ .gsub(/exp\s*\(([^)]+)\)/) { "Math.exp(#{$1})" }
110
+ .gsub(/abs\s*\(([^)]+)\)/) { "(#{$1}).abs" }
111
+ .gsub(/round\s*\(([^)]+)\)/) { "(#{$1}).round" }
112
+ .gsub(/ceil\s*\(([^)]+)\)/) { "(#{$1}).ceil" }
113
+ .gsub(/floor\s*\(([^)]+)\)/) { "(#{$1}).floor" }
114
+
115
+ # Безопасная оценка выражения
116
+ result = safe_eval(expr)
117
+
118
+ # Округляем результат до разумного количества знаков
119
+ if result.is_a?(Float)
120
+ result.round(10)
121
+ else
122
+ result
123
+ end
124
+ end
125
+
126
+ def safe_eval(expression)
127
+ # Проверяем, что выражение содержит только безопасные символы
128
+ unless expression.match?(/\A[\d+\-*\/().\s]+\z/) ||
129
+ expression.include?('Math.') ||
130
+ expression.match?(/\.(abs|round|ceil|floor)\b/)
131
+ raise "Unsafe expression: #{expression}"
132
+ end
133
+
134
+ # Оцениваем выражение
135
+ eval(expression)
136
+ end
137
+
138
+ def required_parameters
139
+ ['expression']
140
+ end
141
+ end
142
+ end
143
+ end