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,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"
@@ -13,7 +13,13 @@ module LLMChain
13
13
  @model = model
14
14
  @memory = memory || Memory::Array.new
15
15
  @tools = tools
16
- @retriever = retriever || Embeddings::Clients::Local::WeaviateRetriever.new
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
- # 1. Сбор контекста
28
- context = memory.recall(prompt)
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
- puts "[RAG Error] #{e.message}"
57
- []
55
+ raise Error, "Cannot retrieve rag context"
58
56
  end
59
57
 
60
58
  def process_tools(prompt)
61
- @tools.each_with_object({}) do |tool, acc|
62
- if tool.match?(prompt)
63
- response = tool.call(prompt)
64
- 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
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
- if memory_context&.any?
73
- parts << "Dialogue history:"
74
- memory_context.each do |item|
75
- parts << "User: #{item[:prompt]}"
76
- parts << "Assistant: #{item[:response]}"
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
- if rag_documents&.any?
81
- parts << "Relevant documents:"
82
- rag_documents.each_with_index do |doc, i|
83
- parts << "Document #{i + 1}: #{doc['content']}"
84
- parts << "Metadata: #{doc['metadata'].to_json}" if doc['metadata']
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
- unless tool_responses.empty?
89
- parts << "Tool results:"
90
- tool_responses.each do |name, response|
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)
@@ -15,6 +15,8 @@ module LLMChain
15
15
  Clients::Qwen
16
16
  when /llama2/
17
17
  Clients::Llama2
18
+ when /gemma3/
19
+ Clients::Gemma3
18
20
  else
19
21
  raise UnknownModelError, "Unknown model: #{model}"
20
22
  end
@@ -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:') ? :qwen3 : :qwen
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