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,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"
|
data/lib/llm_chain/chain.rb
CHANGED
@@ -13,7 +13,13 @@ module LLMChain
|
|
13
13
|
@model = model
|
14
14
|
@memory = memory || Memory::Array.new
|
15
15
|
@tools = tools
|
16
|
-
@retriever = retriever
|
16
|
+
@retriever = if retriever.nil?
|
17
|
+
Embeddings::Clients::Local::WeaviateRetriever.new
|
18
|
+
elsif retriever == false
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
retriever
|
22
|
+
end
|
17
23
|
@client = ClientRegistry.client_for(model, **client_options)
|
18
24
|
end
|
19
25
|
|
@@ -24,27 +30,20 @@ module LLMChain
|
|
24
30
|
# @param rag_options [Hash] Опции для RAG-поиска
|
25
31
|
# @yield [String] Передает чанки ответа если stream=true
|
26
32
|
def ask(prompt, stream: false, rag_context: false, rag_options: {}, &block)
|
27
|
-
|
28
|
-
|
29
|
-
tool_responses = process_tools(prompt)
|
30
|
-
rag_documents = retrieve_rag_context(prompt, rag_options) if rag_context
|
31
|
-
|
32
|
-
# 2. Построение промпта
|
33
|
-
full_prompt = build_prompt(
|
34
|
-
prompt: prompt,
|
35
|
-
memory_context: context,
|
36
|
-
tool_responses: tool_responses,
|
37
|
-
rag_documents: rag_documents
|
38
|
-
)
|
39
|
-
|
40
|
-
# 3. Генерация ответа
|
33
|
+
context = collect_context(prompt, rag_context, rag_options)
|
34
|
+
full_prompt = build_prompt(prompt: prompt, **context)
|
41
35
|
response = generate_response(full_prompt, stream: stream, &block)
|
42
|
-
|
43
|
-
# 4. Сохранение в память
|
44
36
|
memory.store(prompt, response)
|
45
37
|
response
|
46
38
|
end
|
47
39
|
|
40
|
+
def collect_context(prompt, rag_context, rag_options)
|
41
|
+
context = memory.recall(prompt)
|
42
|
+
tool_responses = process_tools(prompt)
|
43
|
+
rag_documents = retrieve_rag_context(prompt, rag_options) if rag_context
|
44
|
+
{ memory_context: context, tool_responses: tool_responses, rag_documents: rag_documents }
|
45
|
+
end
|
46
|
+
|
48
47
|
private
|
49
48
|
|
50
49
|
def retrieve_rag_context(query, options = {})
|
@@ -53,48 +52,65 @@ module LLMChain
|
|
53
52
|
limit = options[:limit] || 3
|
54
53
|
@retriever.search(query, limit: limit)
|
55
54
|
rescue => e
|
56
|
-
|
57
|
-
[]
|
55
|
+
raise Error, "Cannot retrieve rag context"
|
58
56
|
end
|
59
57
|
|
60
58
|
def process_tools(prompt)
|
61
|
-
@tools.
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
65
71
|
end
|
72
|
+
else
|
73
|
+
{}
|
66
74
|
end
|
67
75
|
end
|
68
76
|
|
69
77
|
def build_prompt(prompt:, memory_context: nil, tool_responses: {}, rag_documents: nil)
|
70
78
|
parts = []
|
79
|
+
parts << build_memory_context(memory_context) if memory_context&.any?
|
80
|
+
parts << build_rag_documents(rag_documents) if rag_documents&.any?
|
81
|
+
parts << build_tool_responses(tool_responses) unless tool_responses.empty?
|
82
|
+
parts << "Сurrent question: #{prompt}"
|
83
|
+
parts.join("\n\n")
|
84
|
+
end
|
71
85
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
86
|
+
def build_memory_context(memory_context)
|
87
|
+
parts = ["Dialogue history:"]
|
88
|
+
memory_context.each do |item|
|
89
|
+
parts << "User: #{item[:prompt]}"
|
90
|
+
parts << "Assistant: #{item[:response]}"
|
78
91
|
end
|
92
|
+
parts.join("\n")
|
93
|
+
end
|
79
94
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
95
|
+
def build_rag_documents(rag_documents)
|
96
|
+
parts = ["Relevant documents:"]
|
97
|
+
rag_documents.each_with_index do |doc, i|
|
98
|
+
parts << "Document #{i + 1}: #{doc['content']}"
|
99
|
+
parts << "Metadata: #{doc['metadata'].to_json}" if doc['metadata']
|
86
100
|
end
|
101
|
+
parts.join("\n")
|
102
|
+
end
|
87
103
|
|
88
|
-
|
89
|
-
|
90
|
-
|
104
|
+
def build_tool_responses(tool_responses)
|
105
|
+
parts = ["Tool results:"]
|
106
|
+
tool_responses.each do |name, response|
|
107
|
+
if response.is_a?(Hash) && response[:formatted]
|
108
|
+
parts << "#{name}: #{response[:formatted]}"
|
109
|
+
else
|
91
110
|
parts << "#{name}: #{response}"
|
92
111
|
end
|
93
112
|
end
|
94
|
-
|
95
|
-
parts << "Qurrent question: #{prompt}"
|
96
|
-
|
97
|
-
parts.join("\n\n")
|
113
|
+
parts.join("\n")
|
98
114
|
end
|
99
115
|
|
100
116
|
def generate_response(prompt, stream: false, &block)
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module LLMChain
|
5
|
+
module Clients
|
6
|
+
class Gemma3 < OllamaBase
|
7
|
+
# Доступные версии моделей Gemma3
|
8
|
+
MODEL_VERSIONS = {
|
9
|
+
gemma3: {
|
10
|
+
default: "gemma3:2b",
|
11
|
+
versions: [
|
12
|
+
"gemma3:2b", "gemma3:8b", "gemma3:27b",
|
13
|
+
"gemma3:2b-instruct", "gemma3:8b-instruct", "gemma3:27b-instruct", "gemma3:4b"
|
14
|
+
]
|
15
|
+
}
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# Общие настройки по умолчанию для Gemma3
|
19
|
+
COMMON_DEFAULT_OPTIONS = {
|
20
|
+
temperature: 0.7,
|
21
|
+
top_p: 0.9,
|
22
|
+
top_k: 40,
|
23
|
+
repeat_penalty: 1.1,
|
24
|
+
num_ctx: 8192
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Специфичные настройки для разных версий
|
28
|
+
VERSION_SPECIFIC_OPTIONS = {
|
29
|
+
gemma3: {
|
30
|
+
stop: ["<|im_end|>", "<|endoftext|>", "<|user|>", "<|assistant|>"]
|
31
|
+
}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
# Внутренние теги для очистки ответов
|
35
|
+
INTERNAL_TAGS = {
|
36
|
+
common: {
|
37
|
+
think: /<think>.*?<\/think>\s*/mi,
|
38
|
+
reasoning: /<reasoning>.*?<\/reasoning>\s*/mi
|
39
|
+
},
|
40
|
+
gemma3: {
|
41
|
+
system: /<\|system\|>.*?<\|im_end\|>\s*/mi,
|
42
|
+
user: /<\|user\|>.*?<\|im_end\|>\s*/mi,
|
43
|
+
assistant: /<\|assistant\|>.*?<\|im_end\|>\s*/mi
|
44
|
+
}
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
def initialize(model: nil, base_url: nil, **options)
|
48
|
+
model ||= detect_default_model
|
49
|
+
|
50
|
+
@model = model
|
51
|
+
validate_model_version(@model)
|
52
|
+
|
53
|
+
super(
|
54
|
+
model: @model,
|
55
|
+
base_url: base_url,
|
56
|
+
default_options: default_options_for(@model).merge(options)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def chat(prompt, show_internal: false, stream: false, **options, &block)
|
61
|
+
if stream
|
62
|
+
stream_chat(prompt, show_internal: show_internal, **options, &block)
|
63
|
+
else
|
64
|
+
response = super(prompt, **options)
|
65
|
+
process_response(response, show_internal: show_internal)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def stream_chat(prompt, show_internal: false, **options, &block)
|
70
|
+
buffer = ""
|
71
|
+
connection.post(API_ENDPOINT) do |req|
|
72
|
+
req.headers['Content-Type'] = 'application/json'
|
73
|
+
req.body = build_request_body(prompt, options.merge(stream: true))
|
74
|
+
|
75
|
+
req.options.on_data = Proc.new do |chunk, _bytes, _env|
|
76
|
+
processed = process_stream_chunk(chunk)
|
77
|
+
next unless processed
|
78
|
+
|
79
|
+
buffer << processed
|
80
|
+
block.call(processed) if block_given?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
process_response(buffer, show_internal: show_internal)
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def build_request_body(prompt, options)
|
90
|
+
body = super
|
91
|
+
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
92
|
+
body[:options].merge!(version_specific_options) if version_specific_options
|
93
|
+
body
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def model_version
|
99
|
+
:gemma3
|
100
|
+
end
|
101
|
+
|
102
|
+
def detect_default_model
|
103
|
+
MODEL_VERSIONS[model_version][:default]
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_model_version(model)
|
107
|
+
valid_models = MODEL_VERSIONS.values.flat_map { |v| v[:versions] }
|
108
|
+
unless valid_models.include?(model)
|
109
|
+
raise InvalidModelVersion, "Invalid Gemma3 model version. Available: #{valid_models.join(', ')}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def default_options_for(model)
|
114
|
+
COMMON_DEFAULT_OPTIONS.merge(
|
115
|
+
VERSION_SPECIFIC_OPTIONS[model_version] || {}
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def process_stream_chunk(chunk)
|
120
|
+
parsed = JSON.parse(chunk)
|
121
|
+
parsed["response"] if parsed.is_a?(Hash) && parsed["response"]
|
122
|
+
rescue JSON::ParserError
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_response(response, show_internal: false)
|
127
|
+
return response unless response.is_a?(String)
|
128
|
+
|
129
|
+
if show_internal
|
130
|
+
response
|
131
|
+
else
|
132
|
+
clean_response(response)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def clean_response(text)
|
137
|
+
tags = INTERNAL_TAGS[:common].merge(INTERNAL_TAGS[model_version] || {})
|
138
|
+
tags.values.reduce(text) do |processed, regex|
|
139
|
+
processed.gsub(regex, '')
|
140
|
+
end.gsub(/\n{3,}/, "\n\n").strip
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
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: [
|
@@ -92,16 +98,21 @@ module LLMChain
|
|
92
98
|
|
93
99
|
def build_request_body(prompt, options)
|
94
100
|
body = super
|
95
|
-
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
101
|
+
version_specific_options = VERSION_SPECIFIC_OPTIONS[model_version]
|
96
102
|
body[:options].merge!(version_specific_options) if version_specific_options
|
97
|
-
puts body
|
98
103
|
body
|
99
104
|
end
|
100
105
|
|
101
106
|
private
|
102
107
|
|
103
108
|
def model_version
|
104
|
-
@model.start_with?('qwen3:')
|
109
|
+
if @model.start_with?('qwen3:')
|
110
|
+
:qwen3
|
111
|
+
elsif @model.start_with?('qwen2:')
|
112
|
+
:qwen2
|
113
|
+
else
|
114
|
+
:qwen
|
115
|
+
end
|
105
116
|
end
|
106
117
|
|
107
118
|
def detect_default_model
|