Class: AIA::ModelDirectives

Inherits:
Directive
  • Object
show all
Defined in:
lib/aia/directives/model_directives.rb

Constant Summary

Constants inherited from Directive

Directive::DIRECTIVE_PREFIX

Instance Method Summary collapse

Methods inherited from Directive

build_dispatch_block, help, state_setting!, state_setting_methods

Instance Method Details

#available_models(args = nil, context_manager = nil) ⇒ Object Also known as: am, available, models, all_models, llms



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/aia/directives/model_directives.rb', line 6

def available_models(args = nil, context_manager = nil)
  current_models = AIA.config.models

  model_names = current_models.map do |m|
    m.respond_to?(:name) ? m.name : m.to_s
  end

  positive_terms, negative_terms = parse_search_terms(Array(args))

  using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }

  if using_local_provider
    show_local_models(model_names, positive_terms, negative_terms)
  else
    show_rubyllm_models(positive_terms, negative_terms)
  end

  ""
end

#compare(args, context_manager = nil) ⇒ Object Also known as: cmp

rubocop:disable Metrics/MethodLength



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/aia/directives/model_directives.rb', line 33

def compare(args, context_manager = nil)
  return 'Error: No prompt provided for comparison' if args.empty?

  prompt = nil
  models = []

  i = 0
  while i < args.length
    if args[i] == '--models' && i + 1 < args.length
      models = args[i + 1].split(',')
      i += 2
    else
      prompt ||= args[i]
      i += 1
    end
  end

  return 'Error: No prompt provided for comparison' unless prompt
  return 'Error: No models specified. Use --models model1,model2,model3' if models.empty?

  puts "\nComparing responses for: #{prompt}\n"
  puts '=' * 80

  results = {}

  models.each do |model_name|
    model_name.strip!
    puts "\nšŸ¤– **#{model_name}:**"
    puts '-' * 40

    begin
      chat = RubyLLM.chat(model: model_name)
      response = chat.ask(prompt)
      content = response.content

      puts content
      results[model_name] = content
    rescue StandardError => e
      error_msg = "Error with #{model_name}: #{e.message}"
      puts error_msg
      results[model_name] = error_msg
    end
  end

  puts "\n" + ('=' * 80)
  puts "\nComparison complete!"

  ''
end

#format_bytes(bytes) ⇒ Object

rubocop:enable Metrics/AbcSize, Metrics/MethodLength



213
214
215
216
217
218
219
220
221
# File 'lib/aia/directives/model_directives.rb', line 213

def format_bytes(bytes)
  units = %w[B KB MB GB TB]
  return "0 B" if bytes.zero?

  exp = (Math.log(bytes) / Math.log(1024)).to_i
  exp = [exp, units.length - 1].min

  "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
end

#normalized_model_search_terms(positive_terms, negative_terms = nil) ⇒ Object

rubocop:enable Metrics/AbcSize, Metrics/MethodLength



277
278
279
280
281
282
283
284
# File 'lib/aia/directives/model_directives.rb', line 277

def normalized_model_search_terms(positive_terms, negative_terms = nil)
  return parse_search_terms(Array(positive_terms)) if negative_terms.nil?

  [
    Array(positive_terms).compact.map { |term| term.to_s.downcase },
    Array(negative_terms).compact.map { |term| term.to_s.downcase }
  ]
end

#show_lms_models(api_base, positive_terms = nil, negative_terms = nil) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/aia/directives/model_directives.rb', line 163

def show_lms_models(api_base, positive_terms = nil, negative_terms = nil)
  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)

  begin
    uri = URI("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
    http = Net::HTTP.new(uri.host, uri.port)
    http.open_timeout = 5
    http.read_timeout = 5
    response = http.request(Net::HTTP::Get.new(uri))

    unless response.is_a?(Net::HTTPSuccess)
      puts "āŒ Cannot connect to LM Studio at #{api_base}"
      return
    end

    data = JSON.parse(response.body)
    models = data['data'] || []

    if models.empty?
      puts "No LM Studio models found"
      return
    end

    puts "LM Studio Models (#{api_base}):"
    puts "-" * 60

    counter = 0
    models.each do |model|
      name = model['id']
      entry = "- lms/#{name}"
      entry_lc = entry.downcase

      # entry_lc is a String; Array#intersect? would raise TypeError
      show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
      show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
      if show_it
        puts entry
        counter += 1
      end
    end

    puts
    puts "#{counter} LM Studio model(s) available"
    puts
  rescue StandardError => e
    puts "āŒ Error fetching LM Studio models: #{e.message}"
  end
end

#show_local_models(current_models, positive_terms = nil, negative_terms = nil) ⇒ Object

--- helpers (no desc → not registered) ---



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/aia/directives/model_directives.rb', line 87

def show_local_models(current_models, positive_terms = nil, negative_terms = nil)
  require 'net/http'
  require 'json'

  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)

  puts "\nLocal LLM Models:"
  puts

  current_models.each do |model_spec|
    if model_spec.start_with?('ollama/')
      api_base = ENV.fetch('OLLAMA_API_BASE', 'http://localhost:11434')
      api_base = api_base.gsub(%r{/v1/?$}, '')
      show_ollama_models(api_base, positive_terms, negative_terms)
    elsif model_spec.start_with?('lms/')
      api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234')
      show_lms_models(api_base, positive_terms, negative_terms)
    end
  end
end

#show_ollama_models(api_base, positive_terms = nil, negative_terms = nil) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/aia/directives/model_directives.rb', line 109

def show_ollama_models(api_base, positive_terms = nil, negative_terms = nil)
  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)

  begin
    uri = URI("#{api_base}/api/tags")
    http = Net::HTTP.new(uri.host, uri.port)
    http.open_timeout = 5
    http.read_timeout = 5
    response = http.request(Net::HTTP::Get.new(uri))

    unless response.is_a?(Net::HTTPSuccess)
      puts "āŒ Cannot connect to Ollama at #{api_base}"
      return
    end

    data = JSON.parse(response.body)
    models = data['models'] || []

    if models.empty?
      puts "No Ollama models found"
      return
    end

    puts "Ollama Models (#{api_base}):"
    puts "-" * 60

    counter = 0
    models.each do |model|
      name = model['name']
      size = model['size'] ? format_bytes(model['size']) : 'unknown'
      modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'

      entry = "- ollama/#{name} (size: #{size}, modified: #{modified})"
      entry_lc = entry.downcase

      # entry_lc is a String; Array#intersect? would raise TypeError
      show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
      show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
      if show_it
        puts entry
        counter += 1
      end
    end

    puts
    puts "#{counter} Ollama model(s) available"
    puts
  rescue StandardError => e
    puts "āŒ Error fetching Ollama models: #{e.message}"
  end
end

#show_rubyllm_models(positive_terms = nil, negative_terms = nil) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/aia/directives/model_directives.rb', line 224

def show_rubyllm_models(positive_terms = nil, negative_terms = nil)
  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)

  # expand comma-separated terms passed as a single token
  if positive_terms.size == 1
    positive_terms = positive_terms.first.split(',')
  end

  header = "\nAvailable LLMs"
  header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
  header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?

  puts header + ':'
  puts

  # modality terms (e.g. "text_to_text") trigger capability checks; the rest
  # are plain substring filters applied to the formatted entry string
  q1, q2 = positive_terms.partition { |q| q.include?('_to_') }

  counter = 0

  RubyLLM.models.all.each do |llm|
    cw = llm.context_window
    caps = llm.capabilities.join(',')
    inputs = llm.modalities.input.join(',')
    outputs = llm.modalities.output.join(',')
    mode = "#{inputs} to #{outputs}"
    in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
    entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"

    if positive_terms.empty? && negative_terms.empty?
      counter += 1
      puts entry
      next
    end

    show_it = true
    q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
    q2.each { |q| show_it &&= entry.include?(q) }
    negative_terms.each { |q| show_it &&= !entry.downcase.include?(q) }

    if show_it
      counter += 1
      puts entry
    end
  end

  puts if counter.positive?
  puts "#{counter} LLMs matching your query"
  puts
end