ollama-ruby 0.6.0 → 0.8.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.
data/bin/ollama_chat CHANGED
@@ -4,9 +4,6 @@ require 'ollama'
4
4
  include Ollama
5
5
  require 'term/ansicolor'
6
6
  include Term::ANSIColor
7
- require 'tins'
8
- require 'tins/xt/full'
9
- require 'tins/xt/hash_union'
10
7
  include Tins::GO
11
8
  require 'reline'
12
9
  require 'reverse_markdown'
@@ -31,6 +28,11 @@ class OllamaChatConfig
31
28
  name: <%= ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1') %>
32
29
  options:
33
30
  num_ctx: 8192
31
+ location:
32
+ enabled: false
33
+ name: Berlin
34
+ decimal_degrees: [ 52.514127, 13.475211 ]
35
+ units: SI (International System of Units) # or USCS (United States Customary System)
34
36
  prompts:
35
37
  embed: "This source was now embedded: %{source}"
36
38
  summarize: |
@@ -47,7 +49,7 @@ class OllamaChatConfig
47
49
  voice:
48
50
  enabled: false
49
51
  default: Samantha
50
- list: <%= `say -v ?`.lines.map { _1[/^(.+?)\s+[a-z]{2}_[a-zA-Z0-9]{2,}/, 1] }.uniq.sort.to_s.force_encoding('ASCII-8BIT') %>
52
+ list: <%= `say -v ? 2>/dev/null`.lines.map { _1[/^(.+?)\s+[a-z]{2}_[a-zA-Z0-9]{2,}/, 1] }.uniq.sort.to_s.force_encoding('ASCII-8BIT') %>
51
53
  markdown: true
52
54
  stream: true
53
55
  embedding:
@@ -57,6 +59,7 @@ class OllamaChatConfig
57
59
  options: {}
58
60
  # Retrieval prompt template:
59
61
  prompt: 'Represent this sentence for searching relevant passages: %s'
62
+ batch_size: 10
60
63
  collection: <%= ENV['OLLAMA_CHAT_COLLECTION'] %>
61
64
  found_texts_size: 4096
62
65
  found_texts_count: null
@@ -83,7 +86,7 @@ class OllamaChatConfig
83
86
  if @filename == default_path && !retried
84
87
  retried = true
85
88
  mkdir_p File.dirname(default_path)
86
- File.secure_write(default_path.to_s, DEFAULT_CONFIG)
89
+ File.secure_write(default_path, DEFAULT_CONFIG)
87
90
  retry
88
91
  else
89
92
  raise
@@ -178,7 +181,7 @@ end
178
181
 
179
182
  class Switch
180
183
  def initialize(name, msg:, config: $config)
181
- @value = !!config.send("#{name}?")
184
+ @value = [ false, true ].include?(config) ? config : !!config.send("#{name}?")
182
185
  @msg = msg
183
186
  end
184
187
 
@@ -259,9 +262,21 @@ def setup_switches
259
262
  false => "Embedding is currently not performed.",
260
263
  }
261
264
  )
265
+
266
+ $location = Switch.new(
267
+ :location,
268
+ msg: {
269
+ true => "Location and localtime enabled.",
270
+ false => "Location and localtime disabled.",
271
+ },
272
+ config: $config.location.enabled
273
+ )
262
274
  end
263
275
 
264
276
  def search_web(query, n = nil)
277
+ if l = at_location
278
+ query += " #{at_location}"
279
+ end
265
280
  n = n.to_i
266
281
  n < 1 and n = 1
267
282
  query = URI.encode_uri_component(query)
@@ -401,6 +416,32 @@ def parse_atom(source_io)
401
416
  end
402
417
  end
403
418
 
419
+ def pdf_read(io)
420
+ reader = PDF::Reader.new(io)
421
+ reader.pages.inject(+'') { |result, page| result << page.text }
422
+ end
423
+
424
+ def ps_read(io)
425
+ gs = `which gs`.chomp
426
+ if gs.present?
427
+ Tempfile.create do |tmp|
428
+ IO.popen("#{gs} -q -sDEVICE=pdfwrite -sOutputFile=#{tmp.path} -", 'wb') do |gs_io|
429
+ until io.eof?
430
+ buffer = io.read(1 << 17)
431
+ IO.select(nil, [ gs_io ], nil)
432
+ gs_io.write buffer
433
+ end
434
+ gs_io.close
435
+ File.open(tmp.path, 'rb') do |pdf|
436
+ pdf_read(pdf)
437
+ end
438
+ end
439
+ end
440
+ else
441
+ STDERR.puts "Cannot convert #{io&.content_type} whith ghostscript, gs not in path."
442
+ end
443
+ end
444
+
404
445
  def parse_source(source_io)
405
446
  case source_io&.content_type
406
447
  when 'text/html'
@@ -426,12 +467,11 @@ def parse_source(source_io)
426
467
  parse_rss(source_io)
427
468
  when 'application/atom+xml'
428
469
  parse_atom(source_io)
429
- when 'application/json'
430
- source_io.read
470
+ when 'application/postscript'
471
+ ps_read(source_io)
431
472
  when 'application/pdf'
432
- reader = PDF::Reader.new(source_io)
433
- reader.pages.inject(+'') { |result, page| result << page.text }
434
- when %r(\Atext/), nil
473
+ pdf_read(source_io)
474
+ when %r(\Aapplication/(json|ld\+json|x-ruby|x-perl|x-gawk|x-python|x-javascript|x-c?sh|x-dosexec|x-shellscript|x-tex|x-latex|x-lyx|x-bibtex)), %r(\Atext/), nil
435
475
  source_io.read
436
476
  else
437
477
  STDERR.puts "Cannot embed #{source_io&.content_type} document."
@@ -439,9 +479,14 @@ def parse_source(source_io)
439
479
  end
440
480
  end
441
481
 
442
- def embed_source(source_io, source)
482
+ def embed_source(source_io, source, count: nil)
443
483
  $embedding.on? or return parse_source(source_io)
444
- puts "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
484
+ m = "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
485
+ if count
486
+ puts '%u. %s' % [ count, m ]
487
+ else
488
+ puts m
489
+ end
445
490
  text = parse_source(source_io) or return
446
491
  text.downcase!
447
492
  splitter_config = $config.embedding.splitter
@@ -478,7 +523,7 @@ def embed_source(source_io, source)
478
523
  length: 10
479
524
  )
480
525
  end
481
- $documents.add(inputs, source: source)
526
+ $documents.add(inputs, source:, batch_size: $config.embedding.batch_size?)
482
527
  end
483
528
 
484
529
  def add_image(images, source_io, source)
@@ -515,7 +560,7 @@ def fetch_source(source, &block)
515
560
  ) do |tmp|
516
561
  block.(tmp)
517
562
  end
518
- when %r(\Afile://(?:(?:[.-]|[[:alnum:]])*)(/\S*)|([~.]?/\S*))
563
+ when %r(\Afile://(/\S*)|\A((?:\.\.|[~.]?)/\S*))
519
564
  filename = $~.captures.compact.first
520
565
  filename = File.expand_path(filename)
521
566
  Utils::Fetcher.read(filename) do |tmp|
@@ -573,10 +618,10 @@ def parse_content(content, images)
573
618
  images.clear
574
619
  tags = Utils::Tags.new
575
620
 
576
- content.scan(%r([.~]?/\S+|https?://\S+|#\S+)).each do |source|
621
+ content.scan(%r((?:\.\.|[.~])?/\S+|https?://\S+|#\S+)).each do |source|
577
622
  case source
578
623
  when /\A#(\S+)/
579
- tags << $1
624
+ tags.add($1, source:)
580
625
  else
581
626
  source = source.sub(/(["')]|\*+)\z/, '')
582
627
  fetch_source(source) do |source_io|
@@ -598,39 +643,53 @@ def parse_content(content, images)
598
643
  return content, (tags unless tags.empty?)
599
644
  end
600
645
 
601
- def choose_model(cli_model, default_model)
646
+ def choose_model(cli_model, current_model)
602
647
  models = ollama.tags.models.map(&:name).sort
603
648
  model = if cli_model == ''
604
- Ollama::Utils::Chooser.choose(models) || default_model
649
+ Ollama::Utils::Chooser.choose(models) || current_model
605
650
  else
606
- cli_model || default_model
651
+ cli_model || current_model
607
652
  end
608
653
  ensure
609
654
  puts green { "Connecting to #{model}@#{ollama.base_url} now…" }
610
655
  end
611
656
 
612
- def choose_collection(default_collection)
613
- collections = [ default_collection ] + $documents.collections
657
+ def ask?(prompt:)
658
+ print prompt
659
+ STDIN.gets.chomp
660
+ end
661
+
662
+ def choose_collection(current_collection)
663
+ collections = [ current_collection ] + $documents.collections
614
664
  collections = collections.compact.map(&:to_s).uniq.sort
615
- collections.unshift('[NEW]')
616
- collection = Ollama::Utils::Chooser.choose(collections) || default_collection
617
- if collection == '[NEW]'
618
- print "Enter name of the new collection: "
619
- collection = STDIN.gets.chomp
665
+ collections.unshift('[EXIT]').unshift('[NEW]')
666
+ collection = Ollama::Utils::Chooser.choose(collections) || current_collection
667
+ case collection
668
+ when '[NEW]'
669
+ $documents.collection = ask?(prompt: "Enter name of the new collection: ")
670
+ when nil, '[EXIT]'
671
+ puts "Exiting chooser."
672
+ when /./
673
+ $documents.collection = collection
620
674
  end
621
- $documents.collection = collection
622
675
  ensure
623
- puts "Changing to collection #{bold{collection}}."
676
+ puts "Using collection #{bold{$documents.collection}}."
624
677
  collection_stats
625
678
  end
626
679
 
627
680
  def collection_stats
681
+ list = $documents.collections.sort.map { |c|
682
+ ' ' + ($documents.collection == c ? bold { c } : c).to_s
683
+ }.join(?\n)
628
684
  puts <<~EOT
629
- Collection
685
+ Current Collection
630
686
  Name: #{bold{$documents.collection}}
631
687
  Embedding model: #{bold{$embedding_model}}
632
688
  #Embeddings: #{$documents.size}
689
+ #Tags: #{$documents.tags.size}
633
690
  Tags: #{$documents.tags}
691
+ List:
692
+ #{list}
634
693
  EOT
635
694
  end
636
695
 
@@ -652,13 +711,25 @@ def show_system_prompt
652
711
  EOT
653
712
  end
654
713
 
714
+ def at_location
715
+ if $location.on?
716
+ location_name = $config.location.name
717
+ location_decimal_degrees = $config.location.decimal_degrees * ', '
718
+ localtime = Time.now.iso8601
719
+ units = $config.location.units
720
+ $config.prompts.location % {
721
+ location_name:, location_decimal_degrees:, localtime:, units:,
722
+ }
723
+ end.to_s
724
+ end
725
+
655
726
  def set_system_prompt(messages, system)
656
727
  $system = system
657
728
  messages.clear
658
729
  messages << Message.new(role: 'system', content: system)
659
730
  end
660
731
 
661
- def change_system_prompt(messages)
732
+ def change_system_prompt(messages, default)
662
733
  prompts = $config.system_prompts.attribute_names.compact
663
734
  chosen = Ollama::Utils::Chooser.choose(prompts)
664
735
  system = if chosen
@@ -684,6 +755,7 @@ def info
684
755
  puts "Documents database cache is #{$documents.nil? ? 'n/a' : bold{$documents.cache.class}}"
685
756
  $markdown.show
686
757
  $stream.show
758
+ $location.show
687
759
  if $voice.on?
688
760
  puts "Using voice #{bold{$current_voice}} to speak."
689
761
  end
@@ -716,6 +788,7 @@ def display_chat_help
716
788
  /paste to paste content
717
789
  /markdown toggle markdown output
718
790
  /stream toggle stream output
791
+ /location toggle location submission
719
792
  /voice( change) toggle voice output or change the voice
720
793
  /list [n] list the last n / all conversation exchanges
721
794
  /clear clear the whole conversation
@@ -724,7 +797,8 @@ def display_chat_help
724
797
  /model change the model
725
798
  /system change system prompt (clears conversation)
726
799
  /regenerate the last answer message
727
- /collection clear [tag]|change clear or show stats of current collection
800
+ /collection( clear|change) change (default) collection or clear
801
+ /info show information for current session
728
802
  /import source import the source's content
729
803
  /summarize [n] source summarize the source's content in n words
730
804
  /embedding toggle embedding paused or not
@@ -739,7 +813,7 @@ end
739
813
 
740
814
  def usage
741
815
  puts <<~EOT
742
- #{File.basename($0)} [OPTIONS]
816
+ Usage: #{File.basename($0)} [OPTIONS]
743
817
 
744
818
  -f CONFIG config file to read
745
819
  -u URL the ollama base url, OLLAMA_URL
@@ -790,7 +864,7 @@ if $opts[?c]
790
864
  else
791
865
  default = $config.system_prompts.default? || model_system
792
866
  if $opts[?s] == ??
793
- change_system_prompt(messages)
867
+ change_system_prompt(messages, default)
794
868
  else
795
869
  system = Ollama::Utils::FileArgument.get_file_argument($opts[?s], default:)
796
870
  system.present? and set_system_prompt(messages, system)
@@ -827,11 +901,13 @@ if $embedding.on?
827
901
  end
828
902
  end
829
903
  puts "Collection #{bold{collection}}: Adding #{document_list.size} documents…"
904
+ count = 1
830
905
  document_list.each_slice(25) do |docs|
831
906
  docs.each do |doc|
832
907
  fetch_source(doc) do |doc_io|
833
- embed_source(doc_io, doc)
908
+ embed_source(doc_io, doc, count:)
834
909
  end
910
+ count += 1
835
911
  end
836
912
  end
837
913
  end
@@ -860,18 +936,21 @@ loop do
860
936
  content = Reline.readline(input_prompt, true)&.chomp
861
937
 
862
938
  case content
863
- when %r(^/paste$)
864
- puts bold { "Paste your content and then press C-d!" }
865
- content = STDIN.read
866
939
  when %r(^/copy$)
867
940
  copy_to_clipboard(messages)
868
941
  next
942
+ when %r(^/paste$)
943
+ puts bold { "Paste your content and then press C-d!" }
944
+ content = STDIN.read
869
945
  when %r(^/markdown$)
870
946
  $markdown.toggle
871
947
  next
872
948
  when %r(^/stream$)
873
949
  $stream.toggle
874
950
  next
951
+ when %r(^/location$)
952
+ $location.toggle
953
+ next
875
954
  when %r(^/voice(?:\s+(change))?$)
876
955
  if $1 == 'change'
877
956
  change_voice
@@ -890,33 +969,14 @@ loop do
890
969
  puts "Cleared messages."
891
970
  next
892
971
  when %r(^/clobber$)
893
- clear_messages(messages)
894
- $documents.clear
895
- puts "Cleared messages and collection."
896
- next
897
- when %r(^/collection\s+(clear|change)(?:\s+(.+))?$)
898
- command, arg = $1, $2
899
- case command
900
- when 'clear'
901
- tags = arg.present? ? arg.sub(/\A#*/, '') : nil
902
- if tags
903
- $documents.clear(tags:)
904
- puts "Cleared tag ##{tags} from collection #{bold{collection}}."
905
- else
906
- $documents.clear
907
- puts "Cleared collection #{bold{collection}}."
908
- end
909
- when 'change'
910
- choose_collection(collection)
972
+ if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
973
+ clear_messages(messages)
974
+ $documents.clear
975
+ puts "Cleared messages and collection #{bold{$documents.collection}}."
976
+ else
977
+ puts 'Cancelled.'
911
978
  end
912
979
  next
913
- when %r(^/system$)
914
- change_system_prompt(messages)
915
- info
916
- next
917
- when %r(/info)
918
- info
919
- next
920
980
  when %r(^/pop(?:\s+(\d*))?$)
921
981
  if messages.size > 1
922
982
  n = $1.to_i.clamp(1, Float::INFINITY)
@@ -931,6 +991,10 @@ loop do
931
991
  when %r(^/model$)
932
992
  $model = choose_model('', $model)
933
993
  next
994
+ when %r(^/system$)
995
+ change_system_prompt(messages, $system)
996
+ info
997
+ next
934
998
  when %r(^/regenerate$)
935
999
  if content = messages[-2]&.content
936
1000
  content.gsub!(/\nConsider these chunks for your answer.*\z/, '')
@@ -941,6 +1005,38 @@ loop do
941
1005
  end
942
1006
  parse_content = false
943
1007
  content
1008
+ when %r(^/collection(?:\s+(clear|change))?$)
1009
+ case $1 || 'change'
1010
+ when 'clear'
1011
+ loop do
1012
+ tags = $documents.tags.add('[EXIT]').add('[ALL]')
1013
+ tag = Ollama::Utils::Chooser.choose(tags, prompt: 'Clear? %s')
1014
+ case tag
1015
+ when nil, '[EXIT]'
1016
+ puts "Exiting chooser."
1017
+ break
1018
+ when '[ALL]'
1019
+ if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
1020
+ $documents.clear
1021
+ puts "Cleared collection #{bold{$documents.collection}}."
1022
+ break
1023
+ else
1024
+ puts 'Cancelled.'
1025
+ sleep 3
1026
+ end
1027
+ when /./
1028
+ $documents.clear(tags: [ tag ])
1029
+ puts "Cleared tag #{tag} from collection #{bold{$documents.collection}}."
1030
+ sleep 3
1031
+ end
1032
+ end
1033
+ when 'change'
1034
+ choose_collection($documents.collection)
1035
+ end
1036
+ next
1037
+ when %r(/info)
1038
+ info
1039
+ next
944
1040
  when %r(^/import\s+(.+))
945
1041
  parse_content = false
946
1042
  content = import($1) or next
@@ -1007,6 +1103,10 @@ loop do
1007
1103
  end
1008
1104
  end
1009
1105
 
1106
+ if location = at_location.full?
1107
+ content += " [#{location} – do not comment on this information, just consider it for eventual queries]"
1108
+ end
1109
+
1010
1110
  messages << Message.new(role: 'user', content:, images: images.dup)
1011
1111
  images.clear
1012
1112
  handler = FollowChat.new(messages:, markdown: $markdown.on?, voice: ($current_voice if $voice.on?))
data/bin/ollama_cli CHANGED
@@ -8,8 +8,8 @@ include Tins::GO
8
8
  require 'json'
9
9
 
10
10
  def usage
11
- puts <<~end
12
- #{File.basename($0)} [OPTIONS]
11
+ puts <<~EOT
12
+ Usage: #{File.basename($0)} [OPTIONS]
13
13
 
14
14
  -u URL the ollama base url, OLLAMA_URL
15
15
  -m MODEL the ollama model to chat with, OLLAMA_MODEL
@@ -22,7 +22,7 @@ def usage
22
22
  -S use streaming for generation
23
23
  -h this help
24
24
 
25
- end
25
+ EOT
26
26
  exit 0
27
27
  end
28
28
 
data/bin/ollama_update CHANGED
@@ -14,4 +14,6 @@ ollama.tags.models.each do |model|
14
14
  "Updating model #{bold {name}} (last modified at #{modified_at.iso8601}):"
15
15
  )
16
16
  ollama.pull(name:)
17
+ rescue Ollama::Errors::Error => e
18
+ infobar.puts "Caught #{e.class} for model #{bold { model.name }}: #{e} => Continuing."
17
19
  end
data/docker-compose.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  redis:
3
- image: redis:7.2.5-alpine
3
+ image: valkey/valkey:7.2.7-alpine
4
4
  restart: unless-stopped
5
5
  ports: [ "127.0.0.1:9736:6379" ]
6
6
  volumes:
data/lib/ollama/client.rb CHANGED
@@ -1,6 +1,3 @@
1
- require 'tins/xt/string_camelize'
2
- require 'tins/annotate'
3
-
4
1
  class Ollama::Client
5
2
  end
6
3
  require 'ollama/client/doc'
@@ -23,7 +23,17 @@ class Ollama::Documents::RedisCache
23
23
  end
24
24
 
25
25
  def []=(key, value)
26
- redis.set(pre(key), JSON.generate(value), ex: @ex)
26
+ set(key, value)
27
+ end
28
+
29
+ def set(key, value, ex: nil)
30
+ ex ||= @ex
31
+ if !ex.nil? && ex < 1
32
+ redis.del(pre(key))
33
+ else
34
+ redis.set(pre(key), JSON.generate(value), ex:)
35
+ end
36
+ value
27
37
  end
28
38
 
29
39
  def ttl(key)
@@ -25,7 +25,7 @@ class Ollama::Documents
25
25
  end
26
26
 
27
27
  def tags_set
28
- Ollama::Utils::Tags.new(tags)
28
+ Ollama::Utils::Tags.new(tags, source:)
29
29
  end
30
30
 
31
31
  def ==(other)
@@ -55,10 +55,13 @@ class Ollama::Documents
55
55
  @cache.prefix = prefix
56
56
  end
57
57
 
58
- def add(inputs, batch_size: 10, source: nil, tags: [])
58
+ def add(inputs, batch_size: nil, source: nil, tags: [])
59
59
  inputs = Array(inputs)
60
- tags = Ollama::Utils::Tags.new(tags)
61
- source and tags.add File.basename(source).gsub(/\?.*/, '')
60
+ batch_size ||= 10
61
+ tags = Ollama::Utils::Tags.new(tags, source:)
62
+ if source
63
+ tags.add(File.basename(source).gsub(/\?.*/, ''), source:)
64
+ end
62
65
  inputs.map! { |i|
63
66
  text = i.respond_to?(:read) ? i.read : i.to_s
64
67
  text
@@ -70,7 +73,7 @@ class Ollama::Documents
70
73
  end
71
74
  batches = inputs.each_slice(batch_size).
72
75
  with_infobar(
73
- label: "Add #{truncate(tags.to_s, percentage: 25)}",
76
+ label: "Add #{truncate(tags.to_s(link: false), percentage: 25)}",
74
77
  total: inputs.size
75
78
  )
76
79
  batches.each do |batch|
@@ -159,7 +162,11 @@ class Ollama::Documents
159
162
  end
160
163
 
161
164
  def tags
162
- @cache.inject(Ollama::Utils::Tags.new) { |t, (_, record)| t.merge(record.tags) }
165
+ @cache.each_with_object(Ollama::Utils::Tags.new) do |(_, record), t|
166
+ record.tags.each do |tag|
167
+ t.add(tag, source: record.source)
168
+ end
169
+ end
163
170
  end
164
171
 
165
172
  private
data/lib/ollama/image.rb CHANGED
@@ -7,6 +7,8 @@ class Ollama::Image
7
7
 
8
8
  attr_accessor :path
9
9
 
10
+ attr_reader :data
11
+
10
12
  class << self
11
13
  def for_base64(data, path: nil)
12
14
  obj = new(data)
@@ -31,7 +33,7 @@ class Ollama::Image
31
33
  end
32
34
 
33
35
  def ==(other)
34
- @data == other..data
36
+ @data == other.data
35
37
  end
36
38
 
37
39
  def to_s
@@ -13,7 +13,7 @@ class Ollama::Utils::CacheFetcher
13
13
  if body && content_type
14
14
  io = StringIO.new(body)
15
15
  io.rewind
16
- io.extend(Ollama::Utils::Fetcher::ContentType)
16
+ io.extend(Ollama::Utils::Fetcher::HeaderExtension)
17
17
  io.content_type = content_type
18
18
  block.(io)
19
19
  end
@@ -25,8 +25,8 @@ class Ollama::Utils::CacheFetcher
25
25
  body.empty? and return
26
26
  content_type = io.content_type
27
27
  content_type.nil? and return
28
- @cache[key(:body, url)] = body
29
- @cache[key(:content_type, url)] = content_type.to_s
28
+ @cache.set(key(:body, url), body, ex: io.ex)
29
+ @cache.set(key(:content_type, url), content_type.to_s, ex: io.ex)
30
30
  self
31
31
  end
32
32
 
@@ -8,8 +8,9 @@ module Ollama::Utils::Chooser
8
8
 
9
9
  module_function
10
10
 
11
- def choose(entries)
11
+ def choose(entries, prompt: 'Search? %s')
12
12
  entry = Search.new(
13
+ prompt:,
13
14
  match: -> answer {
14
15
  matcher = Amatch::PairDistance.new(answer.downcase)
15
16
  matches = entries.map { |n| [ n, -matcher.similar(n.to_s.downcase) ] }.
@@ -19,7 +20,7 @@ module Ollama::Utils::Chooser
19
20
  },
20
21
  query: -> _answer, matches, selector {
21
22
  matches.each_with_index.map { |m, i|
22
- i == selector ? "#{blue{?⮕}} #{on_blue{m}}" : " #{m}"
23
+ i == selector ? "#{blue{?⮕}} #{on_blue{m}}" : " #{m.to_s}"
23
24
  } * ?\n
24
25
  },
25
26
  found: -> _answer, matches, selector {
@@ -27,6 +28,11 @@ module Ollama::Utils::Chooser
27
28
  },
28
29
  output: STDOUT
29
30
  ).start
30
- return entry if entry
31
+ if entry
32
+ entry
33
+ else
34
+ print clear_screen, move_home
35
+ nil
36
+ end
31
37
  end
32
38
  end