ollama_chat 0.0.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 +7 -0
 - data/.all_images.yml +17 -0
 - data/.gitignore +9 -0
 - data/Gemfile +5 -0
 - data/README.md +159 -0
 - data/Rakefile +58 -0
 - data/VERSION +1 -0
 - data/bin/ollama_chat +5 -0
 - data/lib/ollama_chat/chat.rb +398 -0
 - data/lib/ollama_chat/clipboard.rb +23 -0
 - data/lib/ollama_chat/dialog.rb +94 -0
 - data/lib/ollama_chat/document_cache.rb +16 -0
 - data/lib/ollama_chat/follow_chat.rb +60 -0
 - data/lib/ollama_chat/information.rb +113 -0
 - data/lib/ollama_chat/message_list.rb +216 -0
 - data/lib/ollama_chat/message_type.rb +5 -0
 - data/lib/ollama_chat/model_handling.rb +29 -0
 - data/lib/ollama_chat/ollama_chat_config.rb +103 -0
 - data/lib/ollama_chat/parsing.rb +159 -0
 - data/lib/ollama_chat/source_fetching.rb +173 -0
 - data/lib/ollama_chat/switches.rb +119 -0
 - data/lib/ollama_chat/utils/cache_fetcher.rb +38 -0
 - data/lib/ollama_chat/utils/chooser.rb +53 -0
 - data/lib/ollama_chat/utils/fetcher.rb +175 -0
 - data/lib/ollama_chat/utils/file_argument.rb +34 -0
 - data/lib/ollama_chat/utils.rb +7 -0
 - data/lib/ollama_chat/version.rb +8 -0
 - data/lib/ollama_chat.rb +20 -0
 - data/ollama_chat.gemspec +50 -0
 - data/spec/assets/api_show.json +63 -0
 - data/spec/assets/api_tags.json +21 -0
 - data/spec/assets/conversation.json +14 -0
 - data/spec/assets/duckduckgo.html +757 -0
 - data/spec/assets/example.atom +26 -0
 - data/spec/assets/example.csv +5 -0
 - data/spec/assets/example.html +10 -0
 - data/spec/assets/example.pdf +139 -0
 - data/spec/assets/example.ps +4 -0
 - data/spec/assets/example.rb +1 -0
 - data/spec/assets/example.rss +25 -0
 - data/spec/assets/example.xml +7 -0
 - data/spec/assets/kitten.jpg +0 -0
 - data/spec/assets/prompt.txt +1 -0
 - data/spec/ollama_chat/chat_spec.rb +105 -0
 - data/spec/ollama_chat/clipboard_spec.rb +29 -0
 - data/spec/ollama_chat/follow_chat_spec.rb +46 -0
 - data/spec/ollama_chat/information_spec.rb +50 -0
 - data/spec/ollama_chat/message_list_spec.rb +132 -0
 - data/spec/ollama_chat/model_handling_spec.rb +35 -0
 - data/spec/ollama_chat/parsing_spec.rb +240 -0
 - data/spec/ollama_chat/source_fetching_spec.rb +54 -0
 - data/spec/ollama_chat/switches_spec.rb +167 -0
 - data/spec/ollama_chat/utils/cache_fetcher_spec.rb +43 -0
 - data/spec/ollama_chat/utils/fetcher_spec.rb +137 -0
 - data/spec/ollama_chat/utils/file_argument_spec.rb +17 -0
 - data/spec/spec_helper.rb +46 -0
 - data/tmp/.keep +0 -0
 - metadata +476 -0
 
| 
         @@ -0,0 +1,159 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module OllamaChat::Parsing
         
     | 
| 
      
 2 
     | 
    
         
            +
              def parse_source(source_io)
         
     | 
| 
      
 3 
     | 
    
         
            +
                case source_io&.content_type
         
     | 
| 
      
 4 
     | 
    
         
            +
                when 'text/html'
         
     | 
| 
      
 5 
     | 
    
         
            +
                  reverse_markdown(source_io.read)
         
     | 
| 
      
 6 
     | 
    
         
            +
                when 'text/xml'
         
     | 
| 
      
 7 
     | 
    
         
            +
                  if source_io.read(8192) =~ %r(^\s*<rss\s)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    source_io.rewind
         
     | 
| 
      
 9 
     | 
    
         
            +
                    return parse_rss(source_io)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
                  source_io.rewind
         
     | 
| 
      
 12 
     | 
    
         
            +
                  source_io.read
         
     | 
| 
      
 13 
     | 
    
         
            +
                when 'text/csv'
         
     | 
| 
      
 14 
     | 
    
         
            +
                  parse_csv(source_io)
         
     | 
| 
      
 15 
     | 
    
         
            +
                when 'application/rss+xml'
         
     | 
| 
      
 16 
     | 
    
         
            +
                  parse_rss(source_io)
         
     | 
| 
      
 17 
     | 
    
         
            +
                when 'application/atom+xml'
         
     | 
| 
      
 18 
     | 
    
         
            +
                  parse_atom(source_io)
         
     | 
| 
      
 19 
     | 
    
         
            +
                when 'application/postscript'
         
     | 
| 
      
 20 
     | 
    
         
            +
                  ps_read(source_io)
         
     | 
| 
      
 21 
     | 
    
         
            +
                when 'application/pdf'
         
     | 
| 
      
 22 
     | 
    
         
            +
                  pdf_read(source_io)
         
     | 
| 
      
 23 
     | 
    
         
            +
                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
         
     | 
| 
      
 24 
     | 
    
         
            +
                  source_io.read
         
     | 
| 
      
 25 
     | 
    
         
            +
                else
         
     | 
| 
      
 26 
     | 
    
         
            +
                  STDERR.puts "Cannot embed #{source_io&.content_type} document."
         
     | 
| 
      
 27 
     | 
    
         
            +
                  return
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              def parse_csv(source_io)
         
     | 
| 
      
 32 
     | 
    
         
            +
                result = +''
         
     | 
| 
      
 33 
     | 
    
         
            +
                CSV.table(File.new(source_io), col_sep: ?,).each do |row|
         
     | 
| 
      
 34 
     | 
    
         
            +
                  next if row.fields.select(&:present?).none?
         
     | 
| 
      
 35 
     | 
    
         
            +
                  result << row.map { |pair|
         
     | 
| 
      
 36 
     | 
    
         
            +
                    pair.compact.map { _1.to_s.strip } * ': ' if pair.last.present?
         
     | 
| 
      
 37 
     | 
    
         
            +
                  }.select(&:present?).map { _1.prepend('  ') } * ?\n
         
     | 
| 
      
 38 
     | 
    
         
            +
                  result << "\n\n"
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
                result
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              def parse_rss(source_io)
         
     | 
| 
      
 44 
     | 
    
         
            +
                feed = RSS::Parser.parse(source_io, false, false)
         
     | 
| 
      
 45 
     | 
    
         
            +
                title = <<~EOT
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # #{feed&.channel&.title}
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                EOT
         
     | 
| 
      
 49 
     | 
    
         
            +
                feed.items.inject(title) do |text, item|
         
     | 
| 
      
 50 
     | 
    
         
            +
                  text << <<~EOT
         
     | 
| 
      
 51 
     | 
    
         
            +
                    ## [#{item&.title}](#{item&.link})
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                    updated on #{item&.pubDate}
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    #{reverse_markdown(item&.description)}
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  EOT
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
              def parse_atom(source_io)
         
     | 
| 
      
 62 
     | 
    
         
            +
                feed = RSS::Parser.parse(source_io, false, false)
         
     | 
| 
      
 63 
     | 
    
         
            +
                title = <<~EOT
         
     | 
| 
      
 64 
     | 
    
         
            +
                  # #{feed.title.content}
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                EOT
         
     | 
| 
      
 67 
     | 
    
         
            +
                feed.items.inject(title) do |text, item|
         
     | 
| 
      
 68 
     | 
    
         
            +
                  text << <<~EOT
         
     | 
| 
      
 69 
     | 
    
         
            +
                    ## [#{item&.title&.content}](#{item&.link&.href})
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    updated on #{item&.updated&.content}
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                    #{reverse_markdown(item&.content&.content)}
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  EOT
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
              def pdf_read(io)
         
     | 
| 
      
 80 
     | 
    
         
            +
                reader = PDF::Reader.new(io)
         
     | 
| 
      
 81 
     | 
    
         
            +
                reader.pages.inject(+'') { |result, page| result << page.text }
         
     | 
| 
      
 82 
     | 
    
         
            +
              end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
              def ps_read(io)
         
     | 
| 
      
 85 
     | 
    
         
            +
                gs = `which gs`.chomp
         
     | 
| 
      
 86 
     | 
    
         
            +
                if gs.present?
         
     | 
| 
      
 87 
     | 
    
         
            +
                  Tempfile.create do |tmp|
         
     | 
| 
      
 88 
     | 
    
         
            +
                    IO.popen("#{gs} -q -sDEVICE=pdfwrite -sOutputFile=#{tmp.path} -", 'wb') do |gs_io|
         
     | 
| 
      
 89 
     | 
    
         
            +
                      until io.eof?
         
     | 
| 
      
 90 
     | 
    
         
            +
                        buffer = io.read(1 << 17)
         
     | 
| 
      
 91 
     | 
    
         
            +
                        IO.select(nil, [ gs_io ], nil)
         
     | 
| 
      
 92 
     | 
    
         
            +
                        gs_io.write buffer
         
     | 
| 
      
 93 
     | 
    
         
            +
                      end
         
     | 
| 
      
 94 
     | 
    
         
            +
                      gs_io.close
         
     | 
| 
      
 95 
     | 
    
         
            +
                      File.open(tmp.path, 'rb') do |pdf|
         
     | 
| 
      
 96 
     | 
    
         
            +
                        pdf_read(pdf)
         
     | 
| 
      
 97 
     | 
    
         
            +
                      end
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
      
 100 
     | 
    
         
            +
                else
         
     | 
| 
      
 101 
     | 
    
         
            +
                  STDERR.puts "Cannot convert #{io&.content_type} whith ghostscript, gs not in path."
         
     | 
| 
      
 102 
     | 
    
         
            +
                end
         
     | 
| 
      
 103 
     | 
    
         
            +
              end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
              def reverse_markdown(html)
         
     | 
| 
      
 106 
     | 
    
         
            +
                ReverseMarkdown.convert(
         
     | 
| 
      
 107 
     | 
    
         
            +
                  html,
         
     | 
| 
      
 108 
     | 
    
         
            +
                  unknown_tags: :bypass,
         
     | 
| 
      
 109 
     | 
    
         
            +
                  github_flavored: true,
         
     | 
| 
      
 110 
     | 
    
         
            +
                  tag_border: ''
         
     | 
| 
      
 111 
     | 
    
         
            +
                )
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
              def parse_content(content, images)
         
     | 
| 
      
 115 
     | 
    
         
            +
                images.clear
         
     | 
| 
      
 116 
     | 
    
         
            +
                tags = Documentrix::Utils::Tags.new
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                contents = [ content ]
         
     | 
| 
      
 119 
     | 
    
         
            +
                content.scan(%r((https?://\S+)|(#\S+)|(?:file://)?(\S*\/\S+))).each do |url, tag, file|
         
     | 
| 
      
 120 
     | 
    
         
            +
                  case
         
     | 
| 
      
 121 
     | 
    
         
            +
                  when tag
         
     | 
| 
      
 122 
     | 
    
         
            +
                    tags.add(tag)
         
     | 
| 
      
 123 
     | 
    
         
            +
                    next
         
     | 
| 
      
 124 
     | 
    
         
            +
                  when file
         
     | 
| 
      
 125 
     | 
    
         
            +
                    file = file.sub(/#.*/, '')
         
     | 
| 
      
 126 
     | 
    
         
            +
                    file =~ %r(\A[~./]) or file.prepend('./')
         
     | 
| 
      
 127 
     | 
    
         
            +
                    File.exist?(file) or next
         
     | 
| 
      
 128 
     | 
    
         
            +
                    source = file
         
     | 
| 
      
 129 
     | 
    
         
            +
                  when url
         
     | 
| 
      
 130 
     | 
    
         
            +
                    links.add(url.to_s)
         
     | 
| 
      
 131 
     | 
    
         
            +
                    source = url
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
      
 133 
     | 
    
         
            +
                  fetch_source(source) do |source_io|
         
     | 
| 
      
 134 
     | 
    
         
            +
                    case source_io&.content_type&.media_type
         
     | 
| 
      
 135 
     | 
    
         
            +
                    when 'image'
         
     | 
| 
      
 136 
     | 
    
         
            +
                      add_image(images, source_io, source)
         
     | 
| 
      
 137 
     | 
    
         
            +
                    when 'text', 'application', nil
         
     | 
| 
      
 138 
     | 
    
         
            +
                      case @document_policy
         
     | 
| 
      
 139 
     | 
    
         
            +
                      when 'ignoring'
         
     | 
| 
      
 140 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 141 
     | 
    
         
            +
                      when 'importing'
         
     | 
| 
      
 142 
     | 
    
         
            +
                        contents << import_source(source_io, source)
         
     | 
| 
      
 143 
     | 
    
         
            +
                      when 'embedding'
         
     | 
| 
      
 144 
     | 
    
         
            +
                        embed_source(source_io, source)
         
     | 
| 
      
 145 
     | 
    
         
            +
                      when 'summarizing'
         
     | 
| 
      
 146 
     | 
    
         
            +
                        contents << summarize_source(source_io, source)
         
     | 
| 
      
 147 
     | 
    
         
            +
                      end
         
     | 
| 
      
 148 
     | 
    
         
            +
                    else
         
     | 
| 
      
 149 
     | 
    
         
            +
                      STDERR.puts(
         
     | 
| 
      
 150 
     | 
    
         
            +
                        "Cannot fetch #{source.to_s.inspect} with content type "\
         
     | 
| 
      
 151 
     | 
    
         
            +
                        "#{source_io&.content_type.inspect}"
         
     | 
| 
      
 152 
     | 
    
         
            +
                      )
         
     | 
| 
      
 153 
     | 
    
         
            +
                    end
         
     | 
| 
      
 154 
     | 
    
         
            +
                  end
         
     | 
| 
      
 155 
     | 
    
         
            +
                end
         
     | 
| 
      
 156 
     | 
    
         
            +
                new_content = contents.select { _1.present? rescue nil }.compact * "\n\n"
         
     | 
| 
      
 157 
     | 
    
         
            +
                return new_content, (tags unless tags.empty?)
         
     | 
| 
      
 158 
     | 
    
         
            +
              end
         
     | 
| 
      
 159 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,173 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module OllamaChat::SourceFetching
         
     | 
| 
      
 2 
     | 
    
         
            +
              def http_options(url)
         
     | 
| 
      
 3 
     | 
    
         
            +
                options = {}
         
     | 
| 
      
 4 
     | 
    
         
            +
                if ssl_no_verify = config.ssl_no_verify?
         
     | 
| 
      
 5 
     | 
    
         
            +
                  hostname = URI.parse(url).hostname
         
     | 
| 
      
 6 
     | 
    
         
            +
                  options |= { ssl_verify_peer: !ssl_no_verify.include?(hostname) }
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
                if proxy = config.proxy?
         
     | 
| 
      
 9 
     | 
    
         
            +
                  options |= { proxy: }
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
                options
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              def fetch_source(source, &block)
         
     | 
| 
      
 15 
     | 
    
         
            +
                case source
         
     | 
| 
      
 16 
     | 
    
         
            +
                when %r(\A!(.*))
         
     | 
| 
      
 17 
     | 
    
         
            +
                  command = $1
         
     | 
| 
      
 18 
     | 
    
         
            +
                  OllamaChat::Utils::Fetcher.execute(command) do |tmp|
         
     | 
| 
      
 19 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
                when %r(\Ahttps?://\S+)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  links.add(source.to_s)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  OllamaChat::Utils::Fetcher.get(
         
     | 
| 
      
 24 
     | 
    
         
            +
                    source,
         
     | 
| 
      
 25 
     | 
    
         
            +
                    cache:        @cache,
         
     | 
| 
      
 26 
     | 
    
         
            +
                    debug:        config.debug,
         
     | 
| 
      
 27 
     | 
    
         
            +
                    http_options: http_options(OllamaChat::Utils::Fetcher.normalize_url(source))
         
     | 
| 
      
 28 
     | 
    
         
            +
                  ) do |tmp|
         
     | 
| 
      
 29 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                when %r(\Afile://(/\S*?)#|\A((?:\.\.|[~.]?)/\S*))
         
     | 
| 
      
 32 
     | 
    
         
            +
                  filename = $~.captures.compact.first
         
     | 
| 
      
 33 
     | 
    
         
            +
                  filename = File.expand_path(filename)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  OllamaChat::Utils::Fetcher.read(filename) do |tmp|
         
     | 
| 
      
 35 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                else
         
     | 
| 
      
 38 
     | 
    
         
            +
                  raise "invalid source"
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
              rescue => e
         
     | 
| 
      
 41 
     | 
    
         
            +
                STDERR.puts "Cannot fetch source #{source.to_s.inspect}: #{e.class} #{e}\n#{e.backtrace * ?\n}"
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              def add_image(images, source_io, source)
         
     | 
| 
      
 45 
     | 
    
         
            +
                STDERR.puts "Adding #{source_io&.content_type} image #{source.to_s.inspect}."
         
     | 
| 
      
 46 
     | 
    
         
            +
                image = Ollama::Image.for_io(source_io, path: source.to_s)
         
     | 
| 
      
 47 
     | 
    
         
            +
                (images << image).uniq!
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
              def import_source(source_io, source)
         
     | 
| 
      
 51 
     | 
    
         
            +
                source = source.to_s
         
     | 
| 
      
 52 
     | 
    
         
            +
                STDOUT.puts "Importing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
         
     | 
| 
      
 53 
     | 
    
         
            +
                source_content = parse_source(source_io)
         
     | 
| 
      
 54 
     | 
    
         
            +
                "Imported #{source.inspect}:\n\n#{source_content}\n\n"
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
              def import(source)
         
     | 
| 
      
 58 
     | 
    
         
            +
                fetch_source(source) do |source_io|
         
     | 
| 
      
 59 
     | 
    
         
            +
                  content = import_source(source_io, source) or return
         
     | 
| 
      
 60 
     | 
    
         
            +
                  source_io.rewind
         
     | 
| 
      
 61 
     | 
    
         
            +
                  content
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
              def summarize_source(source_io, source, words: nil)
         
     | 
| 
      
 66 
     | 
    
         
            +
                STDOUT.puts "Summarizing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
         
     | 
| 
      
 67 
     | 
    
         
            +
                words = words.to_i
         
     | 
| 
      
 68 
     | 
    
         
            +
                words < 1 and words = 100
         
     | 
| 
      
 69 
     | 
    
         
            +
                source_content = parse_source(source_io)
         
     | 
| 
      
 70 
     | 
    
         
            +
                source_content.present? or return
         
     | 
| 
      
 71 
     | 
    
         
            +
                config.prompts.summarize % { source_content:, words: }
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
              def summarize(source, words: nil)
         
     | 
| 
      
 75 
     | 
    
         
            +
                fetch_source(source) do |source_io|
         
     | 
| 
      
 76 
     | 
    
         
            +
                  content = summarize_source(source_io, source, words:) or return
         
     | 
| 
      
 77 
     | 
    
         
            +
                  source_io.rewind
         
     | 
| 
      
 78 
     | 
    
         
            +
                  content
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
              end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
              def embed_source(source_io, source, count: nil)
         
     | 
| 
      
 83 
     | 
    
         
            +
                @embedding.on? or return parse_source(source_io)
         
     | 
| 
      
 84 
     | 
    
         
            +
                m = "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
         
     | 
| 
      
 85 
     | 
    
         
            +
                if count
         
     | 
| 
      
 86 
     | 
    
         
            +
                  STDOUT.puts '%u. %s' % [ count, m ]
         
     | 
| 
      
 87 
     | 
    
         
            +
                else
         
     | 
| 
      
 88 
     | 
    
         
            +
                  STDOUT.puts m
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
                text = parse_source(source_io) or return
         
     | 
| 
      
 91 
     | 
    
         
            +
                text.downcase!
         
     | 
| 
      
 92 
     | 
    
         
            +
                splitter_config = config.embedding.splitter
         
     | 
| 
      
 93 
     | 
    
         
            +
                inputs = nil
         
     | 
| 
      
 94 
     | 
    
         
            +
                case splitter_config.name
         
     | 
| 
      
 95 
     | 
    
         
            +
                when 'Character'
         
     | 
| 
      
 96 
     | 
    
         
            +
                  splitter = Documentrix::Documents::Splitters::Character.new(
         
     | 
| 
      
 97 
     | 
    
         
            +
                    chunk_size: splitter_config.chunk_size,
         
     | 
| 
      
 98 
     | 
    
         
            +
                  )
         
     | 
| 
      
 99 
     | 
    
         
            +
                  inputs = splitter.split(text)
         
     | 
| 
      
 100 
     | 
    
         
            +
                when 'RecursiveCharacter'
         
     | 
| 
      
 101 
     | 
    
         
            +
                  splitter = Documentrix::Documents::Splitters::RecursiveCharacter.new(
         
     | 
| 
      
 102 
     | 
    
         
            +
                    chunk_size: splitter_config.chunk_size,
         
     | 
| 
      
 103 
     | 
    
         
            +
                  )
         
     | 
| 
      
 104 
     | 
    
         
            +
                  inputs = splitter.split(text)
         
     | 
| 
      
 105 
     | 
    
         
            +
                when 'Semantic'
         
     | 
| 
      
 106 
     | 
    
         
            +
                  splitter = Documentrix::Documents::Splitters::Semantic.new(
         
     | 
| 
      
 107 
     | 
    
         
            +
                    ollama:, model: config.embedding.model.name,
         
     | 
| 
      
 108 
     | 
    
         
            +
                    chunk_size: splitter_config.chunk_size,
         
     | 
| 
      
 109 
     | 
    
         
            +
                  )
         
     | 
| 
      
 110 
     | 
    
         
            +
                  inputs = splitter.split(
         
     | 
| 
      
 111 
     | 
    
         
            +
                    text,
         
     | 
| 
      
 112 
     | 
    
         
            +
                    breakpoint: splitter_config.breakpoint.to_sym,
         
     | 
| 
      
 113 
     | 
    
         
            +
                    percentage: splitter_config.percentage?,
         
     | 
| 
      
 114 
     | 
    
         
            +
                    percentile: splitter_config.percentile?,
         
     | 
| 
      
 115 
     | 
    
         
            +
                  )
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
                inputs or return
         
     | 
| 
      
 118 
     | 
    
         
            +
                source = source.to_s
         
     | 
| 
      
 119 
     | 
    
         
            +
                if source.start_with?(?!)
         
     | 
| 
      
 120 
     | 
    
         
            +
                  source = Kramdown::ANSI::Width.truncate(
         
     | 
| 
      
 121 
     | 
    
         
            +
                    source[1..-1].gsub(/\W+/, ?_),
         
     | 
| 
      
 122 
     | 
    
         
            +
                    length: 10
         
     | 
| 
      
 123 
     | 
    
         
            +
                  )
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
      
 125 
     | 
    
         
            +
                @documents.add(inputs, source:, batch_size: config.embedding.batch_size?)
         
     | 
| 
      
 126 
     | 
    
         
            +
              end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
              def embed(source)
         
     | 
| 
      
 129 
     | 
    
         
            +
                if @embedding.on?
         
     | 
| 
      
 130 
     | 
    
         
            +
                  STDOUT.puts "Now embedding #{source.to_s.inspect}."
         
     | 
| 
      
 131 
     | 
    
         
            +
                  fetch_source(source) do |source_io|
         
     | 
| 
      
 132 
     | 
    
         
            +
                    content = parse_source(source_io)
         
     | 
| 
      
 133 
     | 
    
         
            +
                    content.present? or return
         
     | 
| 
      
 134 
     | 
    
         
            +
                    source_io.rewind
         
     | 
| 
      
 135 
     | 
    
         
            +
                    embed_source(source_io, source)
         
     | 
| 
      
 136 
     | 
    
         
            +
                  end
         
     | 
| 
      
 137 
     | 
    
         
            +
                  config.prompts.embed % { source: }
         
     | 
| 
      
 138 
     | 
    
         
            +
                else
         
     | 
| 
      
 139 
     | 
    
         
            +
                  STDOUT.puts "Embedding is off, so I will just give a small summary of this source."
         
     | 
| 
      
 140 
     | 
    
         
            +
                  summarize(source)
         
     | 
| 
      
 141 
     | 
    
         
            +
                end
         
     | 
| 
      
 142 
     | 
    
         
            +
              end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
              def search_web(query, n = nil)
         
     | 
| 
      
 145 
     | 
    
         
            +
                if l = @messages.at_location.full?
         
     | 
| 
      
 146 
     | 
    
         
            +
                  query += " #{l}"
         
     | 
| 
      
 147 
     | 
    
         
            +
                end
         
     | 
| 
      
 148 
     | 
    
         
            +
                n = n.to_i
         
     | 
| 
      
 149 
     | 
    
         
            +
                n < 1 and n = 1
         
     | 
| 
      
 150 
     | 
    
         
            +
                query = URI.encode_uri_component(query)
         
     | 
| 
      
 151 
     | 
    
         
            +
                url = "https://www.duckduckgo.com/html/?q=#{query}"
         
     | 
| 
      
 152 
     | 
    
         
            +
                OllamaChat::Utils::Fetcher.get(url, debug: config.debug) do |tmp|
         
     | 
| 
      
 153 
     | 
    
         
            +
                  result = []
         
     | 
| 
      
 154 
     | 
    
         
            +
                  doc = Nokogiri::HTML(tmp)
         
     | 
| 
      
 155 
     | 
    
         
            +
                  doc.css('.results_links').each do |link|
         
     | 
| 
      
 156 
     | 
    
         
            +
                    if n > 0
         
     | 
| 
      
 157 
     | 
    
         
            +
                      url = link.css('.result__a').first&.[]('href')
         
     | 
| 
      
 158 
     | 
    
         
            +
                      url.sub!(%r(\A(//duckduckgo\.com)?/l/\?uddg=), '')
         
     | 
| 
      
 159 
     | 
    
         
            +
                      url.sub!(%r(&rut=.*), '')
         
     | 
| 
      
 160 
     | 
    
         
            +
                      url = URI.decode_uri_component(url)
         
     | 
| 
      
 161 
     | 
    
         
            +
                      url = URI.parse(url)
         
     | 
| 
      
 162 
     | 
    
         
            +
                      url.host =~ /duckduckgo\.com/ and next
         
     | 
| 
      
 163 
     | 
    
         
            +
                      links.add(url.to_s)
         
     | 
| 
      
 164 
     | 
    
         
            +
                      result << url
         
     | 
| 
      
 165 
     | 
    
         
            +
                      n -= 1
         
     | 
| 
      
 166 
     | 
    
         
            +
                    else
         
     | 
| 
      
 167 
     | 
    
         
            +
                      break
         
     | 
| 
      
 168 
     | 
    
         
            +
                    end
         
     | 
| 
      
 169 
     | 
    
         
            +
                  end
         
     | 
| 
      
 170 
     | 
    
         
            +
                  result
         
     | 
| 
      
 171 
     | 
    
         
            +
                end
         
     | 
| 
      
 172 
     | 
    
         
            +
              end
         
     | 
| 
      
 173 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,119 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module OllamaChat::Switches
         
     | 
| 
      
 2 
     | 
    
         
            +
              module CheckSwitch
         
     | 
| 
      
 3 
     | 
    
         
            +
                extend Tins::Concern
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                included do
         
     | 
| 
      
 6 
     | 
    
         
            +
                  alias_method :on?, :value
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def off?
         
     | 
| 
      
 10 
     | 
    
         
            +
                  !on?
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def show
         
     | 
| 
      
 14 
     | 
    
         
            +
                  STDOUT.puts @msg[value]
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              class Switch
         
     | 
| 
      
 19 
     | 
    
         
            +
                def initialize(name, msg:, config:)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @value = [ false, true ].include?(config) ? config : !!config.send("#{name}?")
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @msg   = msg
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                attr_reader :value
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def set(value, show: false)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @value = !!value
         
     | 
| 
      
 28 
     | 
    
         
            +
                  show && self.show
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def toggle(show: true)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @value = !@value
         
     | 
| 
      
 33 
     | 
    
         
            +
                  show && self.show
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                include CheckSwitch
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              class CombinedSwitch
         
     | 
| 
      
 40 
     | 
    
         
            +
                def initialize(value:, msg:)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  @value = value
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @msg   = msg
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def value
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @value.()
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                include CheckSwitch
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              attr_reader :markdown
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
              attr_reader :location
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
              def setup_switches(config)
         
     | 
| 
      
 57 
     | 
    
         
            +
                @markdown = Switch.new(
         
     | 
| 
      
 58 
     | 
    
         
            +
                  :markdown,
         
     | 
| 
      
 59 
     | 
    
         
            +
                  config:,
         
     | 
| 
      
 60 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 61 
     | 
    
         
            +
                    true  => "Using #{italic{'ANSI'}} markdown to output content.",
         
     | 
| 
      
 62 
     | 
    
         
            +
                    false => "Using plaintext for outputting content.",
         
     | 
| 
      
 63 
     | 
    
         
            +
                  }
         
     | 
| 
      
 64 
     | 
    
         
            +
                )
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                @stream = Switch.new(
         
     | 
| 
      
 67 
     | 
    
         
            +
                  :stream,
         
     | 
| 
      
 68 
     | 
    
         
            +
                  config:,
         
     | 
| 
      
 69 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 70 
     | 
    
         
            +
                    true  => "Streaming enabled.",
         
     | 
| 
      
 71 
     | 
    
         
            +
                    false => "Streaming disabled.",
         
     | 
| 
      
 72 
     | 
    
         
            +
                  }
         
     | 
| 
      
 73 
     | 
    
         
            +
                )
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                @voice = Switch.new(
         
     | 
| 
      
 76 
     | 
    
         
            +
                  :stream,
         
     | 
| 
      
 77 
     | 
    
         
            +
                  config: config.voice,
         
     | 
| 
      
 78 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 79 
     | 
    
         
            +
                    true  => "Voice output enabled.",
         
     | 
| 
      
 80 
     | 
    
         
            +
                    false => "Voice output disabled.",
         
     | 
| 
      
 81 
     | 
    
         
            +
                  }
         
     | 
| 
      
 82 
     | 
    
         
            +
                )
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                @embedding_enabled = Switch.new(
         
     | 
| 
      
 85 
     | 
    
         
            +
                  :embedding_enabled,
         
     | 
| 
      
 86 
     | 
    
         
            +
                  config:,
         
     | 
| 
      
 87 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 88 
     | 
    
         
            +
                    true  => "Embedding enabled.",
         
     | 
| 
      
 89 
     | 
    
         
            +
                    false => "Embedding disabled.",
         
     | 
| 
      
 90 
     | 
    
         
            +
                  }
         
     | 
| 
      
 91 
     | 
    
         
            +
                )
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                @embedding_paused = Switch.new(
         
     | 
| 
      
 94 
     | 
    
         
            +
                  :embedding_paused,
         
     | 
| 
      
 95 
     | 
    
         
            +
                  config:,
         
     | 
| 
      
 96 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 97 
     | 
    
         
            +
                    true  => "Embedding paused.",
         
     | 
| 
      
 98 
     | 
    
         
            +
                    false => "Embedding resumed.",
         
     | 
| 
      
 99 
     | 
    
         
            +
                  }
         
     | 
| 
      
 100 
     | 
    
         
            +
                )
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                @embedding = CombinedSwitch.new(
         
     | 
| 
      
 103 
     | 
    
         
            +
                  value: -> { @embedding_enabled.on? && @embedding_paused.off? },
         
     | 
| 
      
 104 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 105 
     | 
    
         
            +
                    true  => "Embedding is currently performed.",
         
     | 
| 
      
 106 
     | 
    
         
            +
                    false => "Embedding is currently not performed.",
         
     | 
| 
      
 107 
     | 
    
         
            +
                  }
         
     | 
| 
      
 108 
     | 
    
         
            +
                )
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                @location = Switch.new(
         
     | 
| 
      
 111 
     | 
    
         
            +
                  :location,
         
     | 
| 
      
 112 
     | 
    
         
            +
                  config: config.location.enabled,
         
     | 
| 
      
 113 
     | 
    
         
            +
                  msg: {
         
     | 
| 
      
 114 
     | 
    
         
            +
                    true  => "Location and localtime enabled.",
         
     | 
| 
      
 115 
     | 
    
         
            +
                    false => "Location and localtime disabled.",
         
     | 
| 
      
 116 
     | 
    
         
            +
                  }
         
     | 
| 
      
 117 
     | 
    
         
            +
                )
         
     | 
| 
      
 118 
     | 
    
         
            +
              end
         
     | 
| 
      
 119 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'digest/md5'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class OllamaChat::Utils::CacheFetcher
         
     | 
| 
      
 4 
     | 
    
         
            +
              def initialize(cache)
         
     | 
| 
      
 5 
     | 
    
         
            +
                @cache = cache
         
     | 
| 
      
 6 
     | 
    
         
            +
              end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def get(url, &block)
         
     | 
| 
      
 9 
     | 
    
         
            +
                block or raise ArgumentError, 'require block argument'
         
     | 
| 
      
 10 
     | 
    
         
            +
                body         = @cache[key(:body, url)]
         
     | 
| 
      
 11 
     | 
    
         
            +
                content_type = @cache[key(:content_type, url)]
         
     | 
| 
      
 12 
     | 
    
         
            +
                content_type = MIME::Types[content_type].first
         
     | 
| 
      
 13 
     | 
    
         
            +
                if body && content_type
         
     | 
| 
      
 14 
     | 
    
         
            +
                  io = StringIO.new(body)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  io.rewind
         
     | 
| 
      
 16 
     | 
    
         
            +
                  io.extend(OllamaChat::Utils::Fetcher::HeaderExtension)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  io.content_type = content_type
         
     | 
| 
      
 18 
     | 
    
         
            +
                  block.(io)
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
              end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
              def put(url, io)
         
     | 
| 
      
 23 
     | 
    
         
            +
                io.rewind
         
     | 
| 
      
 24 
     | 
    
         
            +
                body = io.read
         
     | 
| 
      
 25 
     | 
    
         
            +
                body.empty? and return
         
     | 
| 
      
 26 
     | 
    
         
            +
                content_type = io.content_type
         
     | 
| 
      
 27 
     | 
    
         
            +
                content_type.nil? and return
         
     | 
| 
      
 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 
     | 
    
         
            +
                self
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              private
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def key(type, url)
         
     | 
| 
      
 36 
     | 
    
         
            +
                [ type, Digest::MD5.hexdigest(url) ] * ?-
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'amatch'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'search_ui'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'term/ansicolor'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module OllamaChat::Utils::Chooser
         
     | 
| 
      
 6 
     | 
    
         
            +
              class << self
         
     | 
| 
      
 7 
     | 
    
         
            +
                include SearchUI
         
     | 
| 
      
 8 
     | 
    
         
            +
                include Term::ANSIColor
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                # The choose method presents a list of entries and prompts the user
         
     | 
| 
      
 11 
     | 
    
         
            +
                # for input, allowing them to select one entry based on their input.
         
     | 
| 
      
 12 
     | 
    
         
            +
                #
         
     | 
| 
      
 13 
     | 
    
         
            +
                # @param entries [Array] the list of entries to present to the user
         
     | 
| 
      
 14 
     | 
    
         
            +
                # @param prompt [String] the prompt message to display when asking for input (default: 'Search? %s')
         
     | 
| 
      
 15 
     | 
    
         
            +
                # @param return_immediately [Boolean] whether to immediately return the
         
     | 
| 
      
 16 
     | 
    
         
            +
                #        first entry if there is only one or nil when there is none (default: false)
         
     | 
| 
      
 17 
     | 
    
         
            +
                #
         
     | 
| 
      
 18 
     | 
    
         
            +
                # @return [Object] the selected entry, or nil if no entry was chosen
         
     | 
| 
      
 19 
     | 
    
         
            +
                #
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 21 
     | 
    
         
            +
                #   choose(['entry1', 'entry2'], prompt: 'Choose an option:')
         
     | 
| 
      
 22 
     | 
    
         
            +
                def choose(entries, prompt: 'Search? %s', return_immediately: false)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  if return_immediately && entries.size <= 1
         
     | 
| 
      
 24 
     | 
    
         
            +
                    return entries.first
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                  entry = Search.new(
         
     | 
| 
      
 27 
     | 
    
         
            +
                    prompt:,
         
     | 
| 
      
 28 
     | 
    
         
            +
                    match: -> answer {
         
     | 
| 
      
 29 
     | 
    
         
            +
                      matcher = Amatch::PairDistance.new(answer.downcase)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      matches = entries.map { |n| [ n, -matcher.similar(n.to_s.downcase) ] }.
         
     | 
| 
      
 31 
     | 
    
         
            +
                        select { |_, s| s < 0 }.sort_by(&:last).map(&:first)
         
     | 
| 
      
 32 
     | 
    
         
            +
                      matches.empty? and matches = entries
         
     | 
| 
      
 33 
     | 
    
         
            +
                      matches.first(Tins::Terminal.lines - 1)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    },
         
     | 
| 
      
 35 
     | 
    
         
            +
                    query: -> _answer, matches, selector {
         
     | 
| 
      
 36 
     | 
    
         
            +
                      matches.each_with_index.map { |m, i|
         
     | 
| 
      
 37 
     | 
    
         
            +
                        i == selector ? "#{blue{?⮕}} #{on_blue{m}}" : "  #{m.to_s}"
         
     | 
| 
      
 38 
     | 
    
         
            +
                      } * ?\n
         
     | 
| 
      
 39 
     | 
    
         
            +
                    },
         
     | 
| 
      
 40 
     | 
    
         
            +
                    found: -> _answer, matches, selector {
         
     | 
| 
      
 41 
     | 
    
         
            +
                      matches[selector]
         
     | 
| 
      
 42 
     | 
    
         
            +
                    },
         
     | 
| 
      
 43 
     | 
    
         
            +
                    output: STDOUT
         
     | 
| 
      
 44 
     | 
    
         
            +
                  ).start
         
     | 
| 
      
 45 
     | 
    
         
            +
                  if entry
         
     | 
| 
      
 46 
     | 
    
         
            +
                    entry
         
     | 
| 
      
 47 
     | 
    
         
            +
                  else
         
     | 
| 
      
 48 
     | 
    
         
            +
                    print clear_screen, move_home
         
     | 
| 
      
 49 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,175 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'tempfile'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'tins/unit'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'infobar'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'mime-types'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'stringio'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'ollama_chat/utils/cache_fetcher'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            class OllamaChat::Utils::Fetcher
         
     | 
| 
      
 9 
     | 
    
         
            +
              module HeaderExtension
         
     | 
| 
      
 10 
     | 
    
         
            +
                attr_accessor :content_type
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                attr_accessor :ex
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def self.failed
         
     | 
| 
      
 15 
     | 
    
         
            +
                  object = StringIO.new.extend(self)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  object.content_type = MIME::Types['text/plain'].first
         
     | 
| 
      
 17 
     | 
    
         
            +
                  object
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              class RetryWithoutStreaming < StandardError; end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def self.get(url, **options, &block)
         
     | 
| 
      
 24 
     | 
    
         
            +
                cache = options.delete(:cache) and
         
     | 
| 
      
 25 
     | 
    
         
            +
                  cache = OllamaChat::Utils::CacheFetcher.new(cache)
         
     | 
| 
      
 26 
     | 
    
         
            +
                if result = cache&.get(url, &block)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  infobar.puts "Getting #{url.to_s.inspect} from cache."
         
     | 
| 
      
 28 
     | 
    
         
            +
                  return result
         
     | 
| 
      
 29 
     | 
    
         
            +
                else
         
     | 
| 
      
 30 
     | 
    
         
            +
                  new(**options).send(:get, url) do |tmp|
         
     | 
| 
      
 31 
     | 
    
         
            +
                    result = block.(tmp)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    if cache && !tmp.is_a?(StringIO)
         
     | 
| 
      
 33 
     | 
    
         
            +
                      tmp.rewind
         
     | 
| 
      
 34 
     | 
    
         
            +
                      cache.put(url, tmp)
         
     | 
| 
      
 35 
     | 
    
         
            +
                    end
         
     | 
| 
      
 36 
     | 
    
         
            +
                    result
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
              def self.normalize_url(url)
         
     | 
| 
      
 42 
     | 
    
         
            +
                url = url.to_s
         
     | 
| 
      
 43 
     | 
    
         
            +
                url = URI.decode_uri_component(url)
         
     | 
| 
      
 44 
     | 
    
         
            +
                url = url.sub(/#.*/, '')
         
     | 
| 
      
 45 
     | 
    
         
            +
                URI::Parser.new.escape(url).to_s
         
     | 
| 
      
 46 
     | 
    
         
            +
              end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
              def self.read(filename, &block)
         
     | 
| 
      
 49 
     | 
    
         
            +
                if File.exist?(filename)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  File.open(filename) do |file|
         
     | 
| 
      
 51 
     | 
    
         
            +
                    file.extend(OllamaChat::Utils::Fetcher::HeaderExtension)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    file.content_type = MIME::Types.type_for(filename).first
         
     | 
| 
      
 53 
     | 
    
         
            +
                    block.(file)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
                else
         
     | 
| 
      
 56 
     | 
    
         
            +
                  STDERR.puts "File #{filename.to_s.inspect} doesn't exist."
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
              def self.execute(command, &block)
         
     | 
| 
      
 61 
     | 
    
         
            +
                Tempfile.open do |tmp|
         
     | 
| 
      
 62 
     | 
    
         
            +
                  IO.popen(command) do |command|
         
     | 
| 
      
 63 
     | 
    
         
            +
                    until command.eof?
         
     | 
| 
      
 64 
     | 
    
         
            +
                      tmp.write command.read(1 << 14)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end
         
     | 
| 
      
 66 
     | 
    
         
            +
                    tmp.rewind
         
     | 
| 
      
 67 
     | 
    
         
            +
                    tmp.extend(OllamaChat::Utils::Fetcher::HeaderExtension)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    tmp.content_type = MIME::Types['text/plain'].first
         
     | 
| 
      
 69 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  end
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
              rescue => e
         
     | 
| 
      
 73 
     | 
    
         
            +
                STDERR.puts "Cannot execute #{command.inspect} (#{e})"
         
     | 
| 
      
 74 
     | 
    
         
            +
                if @debug && !e.is_a?(RuntimeError)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  STDERR.puts "#{e.backtrace * ?\n}"
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
                yield HeaderExtension.failed
         
     | 
| 
      
 78 
     | 
    
         
            +
              end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
              def initialize(debug: false, http_options: {})
         
     | 
| 
      
 81 
     | 
    
         
            +
                @debug        = debug
         
     | 
| 
      
 82 
     | 
    
         
            +
                @started      = false
         
     | 
| 
      
 83 
     | 
    
         
            +
                @streaming    = true
         
     | 
| 
      
 84 
     | 
    
         
            +
                @http_options = http_options
         
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
              private
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
              def excon(url, **options)
         
     | 
| 
      
 90 
     | 
    
         
            +
                url = self.class.normalize_url(url)
         
     | 
| 
      
 91 
     | 
    
         
            +
                Excon.new(url, options.merge(@http_options))
         
     | 
| 
      
 92 
     | 
    
         
            +
              end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
              def get(url, &block)
         
     | 
| 
      
 95 
     | 
    
         
            +
                response = nil
         
     | 
| 
      
 96 
     | 
    
         
            +
                Tempfile.open do |tmp|
         
     | 
| 
      
 97 
     | 
    
         
            +
                  infobar.label = 'Getting'
         
     | 
| 
      
 98 
     | 
    
         
            +
                  if @streaming
         
     | 
| 
      
 99 
     | 
    
         
            +
                    response = excon(url, headers:, response_block: callback(tmp)).request(method: :get)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    response.status != 200 || !@started and raise RetryWithoutStreaming
         
     | 
| 
      
 101 
     | 
    
         
            +
                    decorate_io(tmp, response)
         
     | 
| 
      
 102 
     | 
    
         
            +
                    infobar.finish
         
     | 
| 
      
 103 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 104 
     | 
    
         
            +
                  else
         
     | 
| 
      
 105 
     | 
    
         
            +
                    response = excon(url, headers:, middlewares:).request(method: :get)
         
     | 
| 
      
 106 
     | 
    
         
            +
                    if response.status != 200
         
     | 
| 
      
 107 
     | 
    
         
            +
                      raise "invalid response status code"
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
                    body = response.body
         
     | 
| 
      
 110 
     | 
    
         
            +
                    tmp.print body
         
     | 
| 
      
 111 
     | 
    
         
            +
                    infobar.update(message: message(body.size, body.size), force: true)
         
     | 
| 
      
 112 
     | 
    
         
            +
                    decorate_io(tmp, response)
         
     | 
| 
      
 113 
     | 
    
         
            +
                    infobar.finish
         
     | 
| 
      
 114 
     | 
    
         
            +
                    block.(tmp)
         
     | 
| 
      
 115 
     | 
    
         
            +
                  end
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
              rescue RetryWithoutStreaming
         
     | 
| 
      
 118 
     | 
    
         
            +
                @streaming = false
         
     | 
| 
      
 119 
     | 
    
         
            +
                retry
         
     | 
| 
      
 120 
     | 
    
         
            +
              rescue => e
         
     | 
| 
      
 121 
     | 
    
         
            +
                STDERR.puts "Cannot get #{url.to_s.inspect} (#{e}): #{response&.status_line || 'n/a'}"
         
     | 
| 
      
 122 
     | 
    
         
            +
                if @debug && !e.is_a?(RuntimeError)
         
     | 
| 
      
 123 
     | 
    
         
            +
                  STDERR.puts "#{e.backtrace * ?\n}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
      
 125 
     | 
    
         
            +
                yield HeaderExtension.failed
         
     | 
| 
      
 126 
     | 
    
         
            +
              end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
              def headers
         
     | 
| 
      
 129 
     | 
    
         
            +
                {
         
     | 
| 
      
 130 
     | 
    
         
            +
                  'User-Agent' => OllamaChat::Chat.user_agent,
         
     | 
| 
      
 131 
     | 
    
         
            +
                }
         
     | 
| 
      
 132 
     | 
    
         
            +
              end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
              def middlewares
         
     | 
| 
      
 135 
     | 
    
         
            +
                (Excon.defaults[:middlewares] + [ Excon::Middleware::RedirectFollower ]).uniq
         
     | 
| 
      
 136 
     | 
    
         
            +
              end
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
              private
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
              def decorate_io(tmp, response)
         
     | 
| 
      
 141 
     | 
    
         
            +
                tmp.rewind
         
     | 
| 
      
 142 
     | 
    
         
            +
                tmp.extend(HeaderExtension)
         
     | 
| 
      
 143 
     | 
    
         
            +
                if content_type = MIME::Types[response.headers['content-type']].first
         
     | 
| 
      
 144 
     | 
    
         
            +
                  tmp.content_type = content_type
         
     | 
| 
      
 145 
     | 
    
         
            +
                end
         
     | 
| 
      
 146 
     | 
    
         
            +
                if cache_control = response.headers['cache-control'] and
         
     | 
| 
      
 147 
     | 
    
         
            +
                    cache_control !~ /no-store|no-cache/ and
         
     | 
| 
      
 148 
     | 
    
         
            +
                    ex = cache_control[/s-maxage\s*=\s*(\d+)/, 1] || cache_control[/max-age\s*=\s*(\d+)/, 1]
         
     | 
| 
      
 149 
     | 
    
         
            +
                then
         
     | 
| 
      
 150 
     | 
    
         
            +
                  tmp.ex = ex.to_i
         
     | 
| 
      
 151 
     | 
    
         
            +
                end
         
     | 
| 
      
 152 
     | 
    
         
            +
              end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
              def callback(tmp)
         
     | 
| 
      
 155 
     | 
    
         
            +
                -> chunk, remaining_bytes, total_bytes do
         
     | 
| 
      
 156 
     | 
    
         
            +
                  total   = total_bytes or next
         
     | 
| 
      
 157 
     | 
    
         
            +
                  current = total_bytes - remaining_bytes
         
     | 
| 
      
 158 
     | 
    
         
            +
                  if @started
         
     | 
| 
      
 159 
     | 
    
         
            +
                    infobar.counter.progress(by: total - current)
         
     | 
| 
      
 160 
     | 
    
         
            +
                  else
         
     | 
| 
      
 161 
     | 
    
         
            +
                    @started = true
         
     | 
| 
      
 162 
     | 
    
         
            +
                    infobar.counter.reset(total:, current:)
         
     | 
| 
      
 163 
     | 
    
         
            +
                  end
         
     | 
| 
      
 164 
     | 
    
         
            +
                  infobar.update(message: message(current, total), force: true)
         
     | 
| 
      
 165 
     | 
    
         
            +
                  tmp.print(chunk)
         
     | 
| 
      
 166 
     | 
    
         
            +
                end
         
     | 
| 
      
 167 
     | 
    
         
            +
              end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
              def message(current, total)
         
     | 
| 
      
 170 
     | 
    
         
            +
                progress = '%s/%s' % [ current, total ].map {
         
     | 
| 
      
 171 
     | 
    
         
            +
                  Tins::Unit.format(_1, format: '%.2f %U')
         
     | 
| 
      
 172 
     | 
    
         
            +
                }
         
     | 
| 
      
 173 
     | 
    
         
            +
                '%l ' + progress + ' in %te, ETA %e @%E'
         
     | 
| 
      
 174 
     | 
    
         
            +
              end
         
     | 
| 
      
 175 
     | 
    
         
            +
            end
         
     |