ollama-ruby 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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