ollama-ruby 0.13.0 → 0.14.1
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 +4 -4
- data/CHANGES.md +39 -0
- data/README.md +68 -142
- data/Rakefile +3 -15
- data/bin/ollama_cli +37 -6
- data/lib/ollama/dto.rb +4 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama.rb +0 -7
- data/ollama-ruby.gemspec +9 -20
- data/spec/ollama/message_spec.rb +9 -0
- metadata +9 -191
- data/bin/ollama_chat +0 -1249
- data/config/redis.conf +0 -5
- data/docker-compose.yml +0 -10
- data/lib/ollama/utils/cache_fetcher.rb +0 -38
- data/lib/ollama/utils/chooser.rb +0 -52
- data/lib/ollama/utils/fetcher.rb +0 -175
- data/lib/ollama/utils/file_argument.rb +0 -34
- data/spec/assets/prompt.txt +0 -1
- data/spec/ollama/utils/cache_fetcher_spec.rb +0 -43
- data/spec/ollama/utils/fetcher_spec.rb +0 -137
- data/spec/ollama/utils/file_argument_spec.rb +0 -17
data/config/redis.conf
DELETED
data/docker-compose.yml
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'digest/md5'
|
2
|
-
|
3
|
-
class Ollama::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(Ollama::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
|
data/lib/ollama/utils/chooser.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'amatch'
|
2
|
-
require 'search_ui'
|
3
|
-
require 'term/ansicolor'
|
4
|
-
|
5
|
-
module Ollama::Utils::Chooser
|
6
|
-
include SearchUI
|
7
|
-
include Term::ANSIColor
|
8
|
-
|
9
|
-
module_function
|
10
|
-
|
11
|
-
# The choose method presents a list of entries and prompts the user
|
12
|
-
# for input, allowing them to select one entry based on their input.
|
13
|
-
#
|
14
|
-
# @param entries [Array] the list of entries to present to the user
|
15
|
-
# @param prompt [String] the prompt message to display when asking for input (default: 'Search? %s')
|
16
|
-
# @param return_immediately [Boolean] whether to immediately return the 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
|
data/lib/ollama/utils/fetcher.rb
DELETED
@@ -1,175 +0,0 @@
|
|
1
|
-
require 'tempfile'
|
2
|
-
require 'tins/unit'
|
3
|
-
require 'infobar'
|
4
|
-
require 'mime-types'
|
5
|
-
require 'stringio'
|
6
|
-
require 'ollama/utils/cache_fetcher'
|
7
|
-
|
8
|
-
class Ollama::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 = Ollama::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(Ollama::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(Ollama::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' => Ollama::Client.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
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module Ollama::Utils::FileArgument
|
2
|
-
module_function
|
3
|
-
|
4
|
-
# Returns the contents of a file or string, or a default value if neither is provided.
|
5
|
-
#
|
6
|
-
# @param [String] path_or_content The path to a file or a string containing
|
7
|
-
# the content.
|
8
|
-
#
|
9
|
-
# @param [String] default The default value to return if no valid input is
|
10
|
-
# given. Defaults to nil.
|
11
|
-
#
|
12
|
-
# @return [String] The contents of the file, the string, or the default value.
|
13
|
-
#
|
14
|
-
# @example Get the contents of a file
|
15
|
-
# get_file_argument('path/to/file')
|
16
|
-
#
|
17
|
-
# @example Use a string as content
|
18
|
-
# get_file_argument('string content')
|
19
|
-
#
|
20
|
-
# @example Return a default value if no valid input is given
|
21
|
-
# get_file_argument(nil, default: 'default content')
|
22
|
-
def get_file_argument(path_or_content, default: nil)
|
23
|
-
if path_or_content.present? && path_or_content.size < 2 ** 15 &&
|
24
|
-
File.basename(path_or_content).size < 2 ** 8 &&
|
25
|
-
File.exist?(path_or_content)
|
26
|
-
then
|
27
|
-
File.read(path_or_content)
|
28
|
-
elsif path_or_content.present?
|
29
|
-
path_or_content
|
30
|
-
else
|
31
|
-
default
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/spec/assets/prompt.txt
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
You are a test prompt just used for testing.
|
@@ -1,43 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
RSpec.describe Ollama::Utils::CacheFetcher do
|
4
|
-
let :url do
|
5
|
-
'https://www.example.com/hello'
|
6
|
-
end
|
7
|
-
|
8
|
-
let :cache do
|
9
|
-
double('RedisCache')
|
10
|
-
end
|
11
|
-
|
12
|
-
let :fetcher do
|
13
|
-
described_class.new(cache).expose
|
14
|
-
end
|
15
|
-
|
16
|
-
it 'can be instantiated' do
|
17
|
-
expect(fetcher).to be_a described_class
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'has #get' do
|
21
|
-
expect(cache).to receive(:[]).with('body-69ce405ab83f42dffa9fd22bbd47783f').and_return 'world'
|
22
|
-
expect(cache).to receive(:[]).with('content_type-69ce405ab83f42dffa9fd22bbd47783f').and_return 'text/plain'
|
23
|
-
yielded_io = nil
|
24
|
-
block = -> io { yielded_io = io }
|
25
|
-
fetcher.get(url, &block)
|
26
|
-
expect(yielded_io).to be_a StringIO
|
27
|
-
expect(yielded_io.read).to eq 'world'
|
28
|
-
end
|
29
|
-
|
30
|
-
it '#get needs block' do
|
31
|
-
expect { fetcher.get(url) }.to raise_error(ArgumentError)
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'has #put' do
|
35
|
-
io = StringIO.new('world')
|
36
|
-
io.extend(Ollama::Utils::Fetcher::HeaderExtension)
|
37
|
-
io.content_type = MIME::Types['text/plain'].first
|
38
|
-
io.ex = 666
|
39
|
-
expect(cache).to receive(:set).with('body-69ce405ab83f42dffa9fd22bbd47783f', 'world', ex: 666)
|
40
|
-
expect(cache).to receive(:set).with('content_type-69ce405ab83f42dffa9fd22bbd47783f', 'text/plain', ex: 666)
|
41
|
-
fetcher.put(url, io)
|
42
|
-
end
|
43
|
-
end
|
@@ -1,137 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
RSpec.describe Ollama::Utils::Fetcher do
|
4
|
-
let :url do
|
5
|
-
'https://www.example.com/hello'
|
6
|
-
end
|
7
|
-
|
8
|
-
let :fetcher do
|
9
|
-
described_class.new.expose
|
10
|
-
end
|
11
|
-
|
12
|
-
it 'can be instantiated' do
|
13
|
-
expect(fetcher).to be_a described_class
|
14
|
-
end
|
15
|
-
|
16
|
-
it 'has .get' do
|
17
|
-
expect(described_class).to receive(:new).and_return double(get: true)
|
18
|
-
described_class.get(url)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'can #get with streaming' do
|
22
|
-
stub_request(:get, url).
|
23
|
-
with(headers: fetcher.headers).
|
24
|
-
to_return(
|
25
|
-
status: 200,
|
26
|
-
body: 'world',
|
27
|
-
headers: { 'Content-Type' => 'text/plain' },
|
28
|
-
)
|
29
|
-
fetcher.get(url) do |tmp|
|
30
|
-
expect(tmp).to be_a Tempfile
|
31
|
-
expect(tmp.read).to eq 'world'
|
32
|
-
expect(tmp.content_type).to eq 'text/plain'
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'can #get without ssl peer verification' do
|
37
|
-
fetcher = described_class.new(
|
38
|
-
http_options: { ssl_verify_peer: false }
|
39
|
-
).expose
|
40
|
-
stub_request(:get, url).
|
41
|
-
with(headers: fetcher.headers).
|
42
|
-
to_return(
|
43
|
-
status: 200,
|
44
|
-
body: 'world',
|
45
|
-
headers: { 'Content-Type' => 'text/plain' },
|
46
|
-
)
|
47
|
-
expect(Excon).to receive(:new).with(
|
48
|
-
url,
|
49
|
-
hash_including(ssl_verify_peer: false)
|
50
|
-
).and_call_original
|
51
|
-
fetcher.get(url) do |tmp|
|
52
|
-
expect(tmp).to be_a Tempfile
|
53
|
-
expect(tmp.read).to eq 'world'
|
54
|
-
expect(tmp.content_type).to eq 'text/plain'
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'can #get and fallback from streaming' do
|
59
|
-
stub_request(:get, url).
|
60
|
-
with(headers: fetcher.headers).
|
61
|
-
to_return(
|
62
|
-
{ status: 501 },
|
63
|
-
{
|
64
|
-
status: 200,
|
65
|
-
body: 'world',
|
66
|
-
headers: { 'Content-Type' => 'text/plain' },
|
67
|
-
}
|
68
|
-
)
|
69
|
-
fetcher.get(url) do |tmp|
|
70
|
-
expect(tmp).to be_a Tempfile
|
71
|
-
expect(tmp.read).to eq 'world'
|
72
|
-
expect(tmp.content_type).to eq 'text/plain'
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'can #get and finally fail' do
|
77
|
-
stub_request(:get, url).
|
78
|
-
with(headers: fetcher.headers).
|
79
|
-
to_return(status: 500)
|
80
|
-
expect(STDERR).to receive(:puts).with(/cannot.*get.*#{url}/i)
|
81
|
-
fetcher.get(url) do |tmp|
|
82
|
-
expect(tmp).to be_a StringIO
|
83
|
-
expect(tmp.read).to eq ''
|
84
|
-
expect(tmp.content_type).to eq 'text/plain'
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
it 'can redirect' do
|
89
|
-
expect(fetcher.middlewares).to include Excon::Middleware::RedirectFollower
|
90
|
-
end
|
91
|
-
|
92
|
-
it 'can .read' do
|
93
|
-
described_class.read(__FILE__) do |file|
|
94
|
-
expect(file).to be_a File
|
95
|
-
expect(file.read).to include 'can .read'
|
96
|
-
expect(file.content_type).to eq 'application/x-ruby'
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
it 'can .execute' do
|
101
|
-
described_class.execute('echo -n hello world') do |file|
|
102
|
-
expect(file).to be_a Tempfile
|
103
|
-
expect(file.read).to eq 'hello world'
|
104
|
-
expect(file.content_type).to eq 'text/plain'
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
it 'can .execute and fail' do
|
109
|
-
expect(IO).to receive(:popen).and_raise StandardError
|
110
|
-
expect(STDERR).to receive(:puts).with(/cannot.*execute.*foobar/i)
|
111
|
-
described_class.execute('foobar') do |file|
|
112
|
-
expect(file).to be_a StringIO
|
113
|
-
expect(file.read).to be_empty
|
114
|
-
expect(file.content_type).to eq 'text/plain'
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
describe '.normalize_url' do
|
119
|
-
it 'can handle umlauts' do
|
120
|
-
expect(described_class.normalize_url('https://foo.de/bär')).to eq(
|
121
|
-
'https://foo.de/b%C3%A4r'
|
122
|
-
)
|
123
|
-
end
|
124
|
-
|
125
|
-
it 'can handle escaped umlauts' do
|
126
|
-
expect(described_class.normalize_url('https://foo.de/b%C3%A4r')).to eq(
|
127
|
-
'https://foo.de/b%C3%A4r'
|
128
|
-
)
|
129
|
-
end
|
130
|
-
|
131
|
-
it 'can remove #anchors' do
|
132
|
-
expect(described_class.normalize_url('https://foo.de#bar')).to eq(
|
133
|
-
'https://foo.de'
|
134
|
-
)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
RSpec.describe Ollama::Utils::FileArgument do
|
4
|
-
it 'it can return content' do
|
5
|
-
expect(described_class.get_file_argument('foo')).to eq 'foo'
|
6
|
-
end
|
7
|
-
|
8
|
-
it 'it can return content at path' do
|
9
|
-
expect(described_class.get_file_argument(asset('prompt.txt'))).to include\
|
10
|
-
'test prompt'
|
11
|
-
end
|
12
|
-
|
13
|
-
it 'it can return default content' do
|
14
|
-
expect(described_class.get_file_argument('', default: 'foo')).to eq 'foo'
|
15
|
-
expect(described_class.get_file_argument(nil, default: 'foo')).to eq 'foo'
|
16
|
-
end
|
17
|
-
end
|