ollama-ruby 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/CHANGES.md +84 -0
- data/README.md +10 -4
- data/Rakefile +2 -1
- data/bin/ollama_chat +367 -120
- data/bin/ollama_cli +3 -3
- data/bin/ollama_update +2 -0
- data/docker-compose.yml +3 -4
- data/lib/ollama/client.rb +2 -5
- data/lib/ollama/documents/cache/redis_cache.rb +11 -1
- data/lib/ollama/documents.rb +17 -7
- data/lib/ollama/image.rb +3 -1
- data/lib/ollama/utils/cache_fetcher.rb +38 -0
- data/lib/ollama/utils/chooser.rb +9 -3
- data/lib/ollama/utils/fetcher.rb +63 -36
- data/lib/ollama/utils/file_argument.rb +3 -1
- data/lib/ollama/utils/tags.rb +60 -6
- data/lib/ollama/utils/width.rb +5 -3
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama.rb +4 -0
- data/ollama-ruby.gemspec +8 -7
- data/spec/ollama/image_spec.rb +5 -0
- data/spec/ollama/utils/cache_fetcher_spec.rb +43 -0
- data/spec/ollama/utils/fetcher_spec.rb +2 -2
- data/spec/ollama/utils/tags_spec.rb +26 -2
- data/spec/ollama/utils/width_spec.rb +82 -0
- data/spec/spec_helper.rb +1 -0
- metadata +36 -16
data/bin/ollama_chat
CHANGED
@@ -4,9 +4,6 @@ require 'ollama'
|
|
4
4
|
include Ollama
|
5
5
|
require 'term/ansicolor'
|
6
6
|
include Term::ANSIColor
|
7
|
-
require 'tins'
|
8
|
-
require 'tins/xt/full'
|
9
|
-
require 'tins/xt/hash_union'
|
10
7
|
include Tins::GO
|
11
8
|
require 'reline'
|
12
9
|
require 'reverse_markdown'
|
@@ -17,6 +14,7 @@ require 'nokogiri'
|
|
17
14
|
require 'rss'
|
18
15
|
require 'pdf/reader'
|
19
16
|
require 'csv'
|
17
|
+
require 'xdg'
|
20
18
|
|
21
19
|
class OllamaChatConfig
|
22
20
|
include ComplexConfig
|
@@ -30,8 +28,12 @@ class OllamaChatConfig
|
|
30
28
|
name: <%= ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1') %>
|
31
29
|
options:
|
32
30
|
num_ctx: 8192
|
31
|
+
location:
|
32
|
+
enabled: false
|
33
|
+
name: Berlin
|
34
|
+
decimal_degrees: [ 52.514127, 13.475211 ]
|
35
|
+
units: SI (International System of Units) # or USCS (United States Customary System)
|
33
36
|
prompts:
|
34
|
-
system: <%= ENV.fetch('OLLAMA_CHAT_SYSTEM', 'null') %>
|
35
37
|
embed: "This source was now embedded: %{source}"
|
36
38
|
summarize: |
|
37
39
|
Generate an abstract summary of the content in this document using
|
@@ -42,8 +44,14 @@ class OllamaChatConfig
|
|
42
44
|
Answer the the query %{query} using these sources and summaries:
|
43
45
|
|
44
46
|
%{results}
|
45
|
-
|
47
|
+
system_prompts:
|
48
|
+
default: <%= ENV.fetch('OLLAMA_CHAT_SYSTEM', 'null') %>
|
49
|
+
voice:
|
50
|
+
enabled: false
|
51
|
+
default: Samantha
|
52
|
+
list: <%= `say -v ?`.lines.map { _1[/^(.+?)\s+[a-z]{2}_[a-zA-Z0-9]{2,}/, 1] }.uniq.sort.to_s.force_encoding('ASCII-8BIT') %>
|
46
53
|
markdown: true
|
54
|
+
stream: true
|
47
55
|
embedding:
|
48
56
|
enabled: true
|
49
57
|
model:
|
@@ -57,14 +65,16 @@ class OllamaChatConfig
|
|
57
65
|
splitter:
|
58
66
|
name: RecursiveCharacter
|
59
67
|
chunk_size: 1024
|
60
|
-
cache: Ollama::Documents::
|
68
|
+
cache: Ollama::Documents::RedisBackedMemoryCache
|
61
69
|
redis:
|
62
70
|
documents:
|
63
71
|
url: <%= ENV.fetch('REDIS_URL', 'null') %>
|
64
72
|
expiring:
|
65
73
|
url: <%= ENV.fetch('REDIS_EXPIRING_URL', 'null') %>
|
74
|
+
ex: 86400
|
66
75
|
debug: <%= ENV['OLLAMA_CHAT_DEBUG'].to_i == 1 ? true : false %>
|
67
76
|
ssl_no_verify: []
|
77
|
+
copy: pbcopy
|
68
78
|
EOT
|
69
79
|
|
70
80
|
def initialize(filename = nil)
|
@@ -87,17 +97,11 @@ class OllamaChatConfig
|
|
87
97
|
attr_reader :config
|
88
98
|
|
89
99
|
def default_path
|
90
|
-
|
100
|
+
config_dir_path + 'config.yml'
|
91
101
|
end
|
92
102
|
|
93
103
|
def config_dir_path
|
94
|
-
|
95
|
-
ENV.fetch(
|
96
|
-
'XDG_CONFIG_HOME',
|
97
|
-
File.join(ENV.fetch('HOME'), '.config')
|
98
|
-
),
|
99
|
-
'ollama_chat'
|
100
|
-
)
|
104
|
+
XDG.new.config_home + 'ollama_chat'
|
101
105
|
end
|
102
106
|
end
|
103
107
|
|
@@ -158,12 +162,125 @@ class FollowChat
|
|
158
162
|
end
|
159
163
|
end
|
160
164
|
|
165
|
+
module CheckSwitch
|
166
|
+
extend Tins::Concern
|
167
|
+
|
168
|
+
included do
|
169
|
+
alias_method :on?, :value
|
170
|
+
end
|
171
|
+
|
172
|
+
def off?
|
173
|
+
!on?
|
174
|
+
end
|
175
|
+
|
176
|
+
def show
|
177
|
+
puts @msg[value]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class Switch
|
182
|
+
def initialize(name, msg:, config: $config)
|
183
|
+
@value = [ false, true ].include?(config) ? config : !!config.send("#{name}?")
|
184
|
+
@msg = msg
|
185
|
+
end
|
186
|
+
|
187
|
+
attr_reader :value
|
188
|
+
|
189
|
+
def set(value, show: false)
|
190
|
+
@value = !!value
|
191
|
+
show && self.show
|
192
|
+
end
|
193
|
+
|
194
|
+
def toggle(show: true)
|
195
|
+
@value = !@value
|
196
|
+
show && self.show
|
197
|
+
end
|
198
|
+
|
199
|
+
include CheckSwitch
|
200
|
+
end
|
201
|
+
|
202
|
+
class CombinedSwitch
|
203
|
+
def initialize(value:, msg:)
|
204
|
+
@value = value
|
205
|
+
@msg = msg
|
206
|
+
end
|
207
|
+
|
208
|
+
def value
|
209
|
+
@value.()
|
210
|
+
end
|
211
|
+
|
212
|
+
include CheckSwitch
|
213
|
+
end
|
214
|
+
|
215
|
+
def setup_switches
|
216
|
+
$markdown = Switch.new(
|
217
|
+
:markdown,
|
218
|
+
msg: {
|
219
|
+
true => "Using #{italic{'ANSI'}} markdown to output content.",
|
220
|
+
false => "Using plaintext for outputting content.",
|
221
|
+
}
|
222
|
+
)
|
223
|
+
|
224
|
+
$stream = Switch.new(
|
225
|
+
:stream,
|
226
|
+
msg: {
|
227
|
+
true => "Streaming enabled.",
|
228
|
+
false => "Streaming disabled.",
|
229
|
+
}
|
230
|
+
)
|
231
|
+
|
232
|
+
$voice = Switch.new(
|
233
|
+
:stream,
|
234
|
+
msg: {
|
235
|
+
true => "Voice output enabled.",
|
236
|
+
false => "Voice output disabled.",
|
237
|
+
},
|
238
|
+
config: $config.voice
|
239
|
+
)
|
240
|
+
|
241
|
+
$embedding_enabled = Switch.new(
|
242
|
+
:embedding_enabled,
|
243
|
+
msg: {
|
244
|
+
true => "Embedding enabled.",
|
245
|
+
false => "Embedding disabled.",
|
246
|
+
}
|
247
|
+
)
|
248
|
+
|
249
|
+
$embedding_paused = Switch.new(
|
250
|
+
:embedding_paused,
|
251
|
+
msg: {
|
252
|
+
true => "Embedding paused.",
|
253
|
+
false => "Embedding resumed.",
|
254
|
+
}
|
255
|
+
)
|
256
|
+
|
257
|
+
$embedding = CombinedSwitch.new(
|
258
|
+
value: -> { $embedding_enabled.on? && $embedding_paused.off? },
|
259
|
+
msg: {
|
260
|
+
true => "Embedding is currently performed.",
|
261
|
+
false => "Embedding is currently not performed.",
|
262
|
+
}
|
263
|
+
)
|
264
|
+
|
265
|
+
$location = Switch.new(
|
266
|
+
:location,
|
267
|
+
msg: {
|
268
|
+
true => "Location and localtime enabled.",
|
269
|
+
false => "Location and localtime disabled.",
|
270
|
+
},
|
271
|
+
config: $config.location.enabled
|
272
|
+
)
|
273
|
+
end
|
274
|
+
|
161
275
|
def search_web(query, n = nil)
|
276
|
+
if l = at_location
|
277
|
+
query += " #{at_location}"
|
278
|
+
end
|
162
279
|
n = n.to_i
|
163
280
|
n < 1 and n = 1
|
164
281
|
query = URI.encode_uri_component(query)
|
165
282
|
url = "https://www.duckduckgo.com/html/?q=#{query}"
|
166
|
-
Ollama::Utils::Fetcher.
|
283
|
+
Ollama::Utils::Fetcher.get(url, debug: $config.debug) do |tmp|
|
167
284
|
result = []
|
168
285
|
doc = Nokogiri::HTML(tmp)
|
169
286
|
doc.css('.results_links').each do |link|
|
@@ -196,7 +313,7 @@ def pull_model_unless_present(model, options, retried = false)
|
|
196
313
|
end
|
197
314
|
}
|
198
315
|
rescue Errors::NotFoundError
|
199
|
-
puts "Model #{bold{model}} not found, attempting to pull it now…"
|
316
|
+
puts "Model #{bold{model}} not found locally, attempting to pull it from remote now…"
|
200
317
|
ollama.pull(name: model)
|
201
318
|
if retried
|
202
319
|
exit 1
|
@@ -205,7 +322,7 @@ rescue Errors::NotFoundError
|
|
205
322
|
retry
|
206
323
|
end
|
207
324
|
rescue Errors::Error => e
|
208
|
-
warn "Caught #{e.class}: #{e} => Exiting."
|
325
|
+
warn "Caught #{e.class} while pulling model: #{e} => Exiting."
|
209
326
|
exit 1
|
210
327
|
end
|
211
328
|
|
@@ -242,7 +359,7 @@ def list_conversation(messages, last = nil)
|
|
242
359
|
when 'system' then 213
|
243
360
|
else 210
|
244
361
|
end
|
245
|
-
content = m.content.full? { $markdown ? Utils::ANSIMarkdown.parse(_1) : _1 }
|
362
|
+
content = m.content.full? { $markdown.on? ? Utils::ANSIMarkdown.parse(_1) : _1 }
|
246
363
|
message_text = message_type(m.images) + " "
|
247
364
|
message_text += bold { color(role_color) { m.role } }
|
248
365
|
message_text += ":\n#{content}"
|
@@ -298,6 +415,32 @@ def parse_atom(source_io)
|
|
298
415
|
end
|
299
416
|
end
|
300
417
|
|
418
|
+
def pdf_read(io)
|
419
|
+
reader = PDF::Reader.new(io)
|
420
|
+
reader.pages.inject(+'') { |result, page| result << page.text }
|
421
|
+
end
|
422
|
+
|
423
|
+
def ps_read(io)
|
424
|
+
gs = `which gs`.chomp
|
425
|
+
if gs.present?
|
426
|
+
Tempfile.create do |tmp|
|
427
|
+
IO.popen("#{gs} -q -sDEVICE=pdfwrite -sOutputFile=#{tmp.path} -", 'wb') do |gs_io|
|
428
|
+
until io.eof?
|
429
|
+
buffer = io.read(1 << 17)
|
430
|
+
IO.select(nil, [ gs_io ], nil)
|
431
|
+
gs_io.write buffer
|
432
|
+
end
|
433
|
+
gs_io.close
|
434
|
+
File.open(tmp.path, 'rb') do |pdf|
|
435
|
+
pdf_read(pdf)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
else
|
440
|
+
STDERR.puts "Cannot convert #{io&.content_type} whith ghostscript, gs not in path."
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
301
444
|
def parse_source(source_io)
|
302
445
|
case source_io&.content_type
|
303
446
|
when 'text/html'
|
@@ -319,17 +462,16 @@ def parse_source(source_io)
|
|
319
462
|
result << "\n\n"
|
320
463
|
end
|
321
464
|
result
|
322
|
-
when %r(\Atext/)
|
323
|
-
source_io.read
|
324
465
|
when 'application/rss+xml'
|
325
466
|
parse_rss(source_io)
|
326
467
|
when 'application/atom+xml'
|
327
468
|
parse_atom(source_io)
|
328
|
-
when 'application/
|
329
|
-
source_io
|
469
|
+
when 'application/postscript'
|
470
|
+
ps_read(source_io)
|
330
471
|
when 'application/pdf'
|
331
|
-
|
332
|
-
|
472
|
+
pdf_read(source_io)
|
473
|
+
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
|
474
|
+
source_io.read
|
333
475
|
else
|
334
476
|
STDERR.puts "Cannot embed #{source_io&.content_type} document."
|
335
477
|
return
|
@@ -337,7 +479,7 @@ def parse_source(source_io)
|
|
337
479
|
end
|
338
480
|
|
339
481
|
def embed_source(source_io, source)
|
340
|
-
|
482
|
+
$embedding.on? or return parse_source(source_io)
|
341
483
|
puts "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
|
342
484
|
text = parse_source(source_io) or return
|
343
485
|
text.downcase!
|
@@ -375,7 +517,7 @@ def embed_source(source_io, source)
|
|
375
517
|
length: 10
|
376
518
|
)
|
377
519
|
end
|
378
|
-
$documents.add(inputs, source:
|
520
|
+
$documents.add(inputs, source:)
|
379
521
|
end
|
380
522
|
|
381
523
|
def add_image(images, source_io, source)
|
@@ -404,7 +546,12 @@ def fetch_source(source, &block)
|
|
404
546
|
block.(tmp)
|
405
547
|
end
|
406
548
|
when %r(\Ahttps?://\S+)
|
407
|
-
Utils::Fetcher.get(
|
549
|
+
Utils::Fetcher.get(
|
550
|
+
source,
|
551
|
+
cache: $cache,
|
552
|
+
debug: $config.debug,
|
553
|
+
http_options: http_options(source)
|
554
|
+
) do |tmp|
|
408
555
|
block.(tmp)
|
409
556
|
end
|
410
557
|
when %r(\Afile://(?:(?:[.-]|[[:alnum:]])*)(/\S*)|([~.]?/\S*))
|
@@ -417,7 +564,7 @@ def fetch_source(source, &block)
|
|
417
564
|
raise "invalid source"
|
418
565
|
end
|
419
566
|
rescue => e
|
420
|
-
STDERR.puts "Cannot
|
567
|
+
STDERR.puts "Cannot fetch source #{source.to_s.inspect}: #{e}\n#{e.backtrace * ?\n}"
|
421
568
|
end
|
422
569
|
|
423
570
|
def import(source)
|
@@ -445,7 +592,7 @@ def summarize(source, words: nil)
|
|
445
592
|
end
|
446
593
|
|
447
594
|
def embed(source)
|
448
|
-
if
|
595
|
+
if $embedding.on?
|
449
596
|
puts "Now embedding #{source.to_s.inspect}."
|
450
597
|
fetch_source(source) do |source_io|
|
451
598
|
content = parse_source(source_io)
|
@@ -468,7 +615,7 @@ def parse_content(content, images)
|
|
468
615
|
content.scan(%r([.~]?/\S+|https?://\S+|#\S+)).each do |source|
|
469
616
|
case source
|
470
617
|
when /\A#(\S+)/
|
471
|
-
tags
|
618
|
+
tags.add($1, source:)
|
472
619
|
else
|
473
620
|
source = source.sub(/(["')]|\*+)\z/, '')
|
474
621
|
fetch_source(source) do |source_io|
|
@@ -490,29 +637,37 @@ def parse_content(content, images)
|
|
490
637
|
return content, (tags unless tags.empty?)
|
491
638
|
end
|
492
639
|
|
493
|
-
def choose_model(cli_model,
|
640
|
+
def choose_model(cli_model, current_model)
|
494
641
|
models = ollama.tags.models.map(&:name).sort
|
495
642
|
model = if cli_model == ''
|
496
|
-
Ollama::Utils::Chooser.choose(models) ||
|
643
|
+
Ollama::Utils::Chooser.choose(models) || current_model
|
497
644
|
else
|
498
|
-
cli_model ||
|
645
|
+
cli_model || current_model
|
499
646
|
end
|
500
647
|
ensure
|
501
648
|
puts green { "Connecting to #{model}@#{ollama.base_url} now…" }
|
502
649
|
end
|
503
650
|
|
504
|
-
def
|
505
|
-
|
651
|
+
def ask?(prompt:)
|
652
|
+
print prompt
|
653
|
+
STDIN.gets.chomp
|
654
|
+
end
|
655
|
+
|
656
|
+
def choose_collection(current_collection)
|
657
|
+
collections = [ current_collection ] + $documents.collections
|
506
658
|
collections = collections.compact.map(&:to_s).uniq.sort
|
507
|
-
collections.unshift('[NEW]')
|
508
|
-
collection = Ollama::Utils::Chooser.choose(collections) ||
|
509
|
-
|
510
|
-
|
511
|
-
|
659
|
+
collections.unshift('[EXIT]').unshift('[NEW]')
|
660
|
+
collection = Ollama::Utils::Chooser.choose(collections) || current_collection
|
661
|
+
case collection
|
662
|
+
when '[NEW]'
|
663
|
+
$documents.collection = ask?(prompt: "Enter name of the new collection: ")
|
664
|
+
when nil, '[EXIT]'
|
665
|
+
puts "Exiting chooser."
|
666
|
+
when /./
|
667
|
+
$documents.collection = collection
|
512
668
|
end
|
513
|
-
$documents.collection = collection
|
514
669
|
ensure
|
515
|
-
puts "
|
670
|
+
puts "Using collection #{bold{$documents.collection}}."
|
516
671
|
collection_stats
|
517
672
|
end
|
518
673
|
|
@@ -537,64 +692,101 @@ rescue => e
|
|
537
692
|
Ollama::Documents::MemoryCache
|
538
693
|
end
|
539
694
|
|
540
|
-
def
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
def show_markdown
|
546
|
-
if $markdown
|
547
|
-
puts "Using ANSI markdown to output content."
|
548
|
-
else
|
549
|
-
puts "Using plaintext for outputting content."
|
550
|
-
end
|
551
|
-
$markdown
|
695
|
+
def show_system_prompt
|
696
|
+
puts <<~EOT
|
697
|
+
Configured system prompt is:
|
698
|
+
#{Ollama::Utils::ANSIMarkdown.parse($system.to_s).gsub(/\n+\z/, '').full? || 'n/a'}
|
699
|
+
EOT
|
552
700
|
end
|
553
701
|
|
554
|
-
def
|
555
|
-
$
|
556
|
-
|
702
|
+
def at_location
|
703
|
+
if $location.on?
|
704
|
+
location_name = $config.location.name
|
705
|
+
location_decimal_degrees = $config.location.decimal_degrees * ', '
|
706
|
+
localtime = Time.now.iso8601
|
707
|
+
units = $config.location.units
|
708
|
+
$config.prompts.location % {
|
709
|
+
location_name:, location_decimal_degrees:, localtime:, units:,
|
710
|
+
}
|
711
|
+
end.to_s
|
557
712
|
end
|
558
713
|
|
559
|
-
def
|
560
|
-
|
561
|
-
|
714
|
+
def set_system_prompt(messages, system)
|
715
|
+
$system = system
|
716
|
+
messages.clear
|
717
|
+
messages << Message.new(role: 'system', content: system)
|
562
718
|
end
|
563
719
|
|
564
|
-
def
|
565
|
-
|
720
|
+
def change_system_prompt(messages, default)
|
721
|
+
prompts = $config.system_prompts.attribute_names.compact
|
722
|
+
chosen = Ollama::Utils::Chooser.choose(prompts)
|
723
|
+
system = if chosen
|
724
|
+
$config.system_prompts.send(chosen)
|
725
|
+
else
|
726
|
+
default
|
727
|
+
end
|
728
|
+
set_system_prompt(messages, system)
|
566
729
|
end
|
567
730
|
|
568
|
-
def
|
569
|
-
|
570
|
-
|
731
|
+
def change_voice
|
732
|
+
chosen = Ollama::Utils::Chooser.choose($config.voice.list)
|
733
|
+
$current_voice = chosen.full? || $config.voice.default
|
571
734
|
end
|
572
735
|
|
573
736
|
def info
|
574
737
|
puts "Current model is #{bold{$model}}."
|
575
738
|
collection_stats
|
576
|
-
|
739
|
+
$embedding.show
|
740
|
+
if $embedding.on?
|
577
741
|
puts "Text splitter is #{bold{$config.embedding.splitter.name}}."
|
578
742
|
end
|
579
|
-
puts "Documents database cache is #{$documents.nil? ? 'n/a' : $documents.cache.class}"
|
580
|
-
|
743
|
+
puts "Documents database cache is #{$documents.nil? ? 'n/a' : bold{$documents.cache.class}}"
|
744
|
+
$markdown.show
|
745
|
+
$stream.show
|
746
|
+
$location.show
|
747
|
+
if $voice.on?
|
748
|
+
puts "Using voice #{bold{$current_voice}} to speak."
|
749
|
+
end
|
750
|
+
show_system_prompt
|
581
751
|
end
|
582
752
|
|
583
753
|
def clear_messages(messages)
|
584
754
|
messages.delete_if { _1.role != 'system' }
|
585
755
|
end
|
586
756
|
|
757
|
+
def copy_to_clipboard(messages)
|
758
|
+
if message = messages.last and message.role == 'assistant'
|
759
|
+
copy = `which #{$config.copy}`.chomp
|
760
|
+
if copy.present?
|
761
|
+
IO.popen(copy, 'w') do |clipboard|
|
762
|
+
clipboard.write(message.content)
|
763
|
+
end
|
764
|
+
STDOUT.puts "The last response has been copied to the system clipboard."
|
765
|
+
else
|
766
|
+
STDERR.puts "#{$config.copy.inspect} command not found in system's path!"
|
767
|
+
end
|
768
|
+
else
|
769
|
+
STDERR.puts "No response available to copy to the system clipboard."
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
587
773
|
def display_chat_help
|
588
774
|
puts <<~EOT
|
775
|
+
/copy to copy last response to clipboard
|
589
776
|
/paste to paste content
|
590
777
|
/markdown toggle markdown output
|
778
|
+
/stream toggle stream output
|
779
|
+
/location toggle location submission
|
780
|
+
/voice( change) toggle voice output or change the voice
|
591
781
|
/list [n] list the last n / all conversation exchanges
|
592
782
|
/clear clear the whole conversation
|
593
783
|
/clobber clear the conversation and collection
|
594
784
|
/pop [n] pop the last n exchanges, defaults to 1
|
595
785
|
/model change the model
|
786
|
+
/system change system prompt (clears conversation)
|
596
787
|
/regenerate the last answer message
|
597
|
-
/collection clear
|
788
|
+
/collection( clear|change) change (default) collection or clear
|
789
|
+
/info show information for current session
|
598
790
|
/import source import the source's content
|
599
791
|
/summarize [n] source summarize the source's content in n words
|
600
792
|
/embedding toggle embedding paused or not
|
@@ -609,7 +801,7 @@ end
|
|
609
801
|
|
610
802
|
def usage
|
611
803
|
puts <<~EOT
|
612
|
-
#{File.basename($0)} [OPTIONS]
|
804
|
+
Usage: #{File.basename($0)} [OPTIONS]
|
613
805
|
|
614
806
|
-f CONFIG config file to read
|
615
807
|
-u URL the ollama base url, OLLAMA_URL
|
@@ -620,25 +812,31 @@ def usage
|
|
620
812
|
-D DOCUMENT load document and add to embeddings collection (multiple)
|
621
813
|
-M use (empty) MemoryCache for this chat session
|
622
814
|
-E disable embeddings for this chat session
|
623
|
-
-
|
815
|
+
-V display the current version number and quit
|
624
816
|
-h this help
|
625
817
|
|
626
818
|
EOT
|
627
819
|
exit 0
|
628
820
|
end
|
629
821
|
|
822
|
+
def version
|
823
|
+
puts "%s %s" % [ File.basename($0), Ollama::VERSION ]
|
824
|
+
exit 0
|
825
|
+
end
|
826
|
+
|
630
827
|
def ollama
|
631
828
|
$ollama
|
632
829
|
end
|
633
830
|
|
634
|
-
$opts = go 'f:u:m:s:c:C:D:
|
831
|
+
$opts = go 'f:u:m:s:c:C:D:MEVh'
|
635
832
|
|
636
833
|
config = OllamaChatConfig.new($opts[?f])
|
637
834
|
$config = config.config
|
638
835
|
|
639
|
-
|
836
|
+
setup_switches
|
640
837
|
|
641
|
-
|
838
|
+
$opts[?h] and usage
|
839
|
+
$opts[?V] and version
|
642
840
|
|
643
841
|
base_url = $opts[?u] || $config.url
|
644
842
|
$ollama = Client.new(base_url:, debug: $config.debug)
|
@@ -647,14 +845,21 @@ $model = choose_model($opts[?m], $config.model.name)
|
|
647
845
|
options = Options[$config.model.options]
|
648
846
|
model_system = pull_model_unless_present($model, options)
|
649
847
|
messages = []
|
650
|
-
|
848
|
+
$embedding_enabled.set($config.embedding.enabled && !$opts[?E])
|
651
849
|
|
652
|
-
if
|
653
|
-
|
850
|
+
if $opts[?c]
|
851
|
+
messages.concat load_conversation($opts[?c])
|
852
|
+
else
|
853
|
+
default = $config.system_prompts.default? || model_system
|
854
|
+
if $opts[?s] == ??
|
855
|
+
change_system_prompt(messages, default)
|
856
|
+
else
|
857
|
+
system = Ollama::Utils::FileArgument.get_file_argument($opts[?s], default:)
|
858
|
+
system.present? and set_system_prompt(messages, system)
|
859
|
+
end
|
654
860
|
end
|
655
|
-
$markdown = $config.markdown
|
656
861
|
|
657
|
-
if
|
862
|
+
if $embedding.on?
|
658
863
|
$embedding_model = $config.embedding.model.name
|
659
864
|
embedding_model_options = Options[$config.embedding.model.options]
|
660
865
|
pull_model_unless_present($embedding_model, embedding_model_options)
|
@@ -666,6 +871,7 @@ if embedding_enabled?
|
|
666
871
|
collection:,
|
667
872
|
cache: configure_cache,
|
668
873
|
redis_url: $config.redis.documents.url?,
|
874
|
+
debug: ENV['DEBUG'].to_i == 1,
|
669
875
|
)
|
670
876
|
|
671
877
|
document_list = $opts[?D].to_a
|
@@ -691,24 +897,22 @@ if embedding_enabled?
|
|
691
897
|
end
|
692
898
|
end
|
693
899
|
end
|
694
|
-
collection_stats
|
695
900
|
else
|
696
901
|
$documents = Tins::NULL
|
697
902
|
end
|
698
903
|
|
699
|
-
if $
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
puts <<~EOT
|
706
|
-
Configured system prompt is:
|
707
|
-
#{italic{Ollama::Utils::Width.wrap(system, percentage: 90)}}
|
708
|
-
EOT
|
709
|
-
end
|
904
|
+
if redis_expiring_url = $config.redis.expiring.url?
|
905
|
+
$cache = Ollama::Documents::RedisCache.new(
|
906
|
+
prefix: 'Expiring-',
|
907
|
+
url: redis_expiring_url,
|
908
|
+
ex: $config.redis.expiring.ex,
|
909
|
+
)
|
710
910
|
end
|
711
911
|
|
912
|
+
$current_voice = $config.voice.default
|
913
|
+
|
914
|
+
puts "Configuration read from #{config.filename.inspect} is:", $config
|
915
|
+
info
|
712
916
|
puts "\nType /help to display the chat help."
|
713
917
|
|
714
918
|
images = []
|
@@ -718,11 +922,27 @@ loop do
|
|
718
922
|
content = Reline.readline(input_prompt, true)&.chomp
|
719
923
|
|
720
924
|
case content
|
925
|
+
when %r(^/copy$)
|
926
|
+
copy_to_clipboard(messages)
|
927
|
+
next
|
721
928
|
when %r(^/paste$)
|
722
929
|
puts bold { "Paste your content and then press C-d!" }
|
723
930
|
content = STDIN.read
|
724
931
|
when %r(^/markdown$)
|
725
|
-
$markdown
|
932
|
+
$markdown.toggle
|
933
|
+
next
|
934
|
+
when %r(^/stream$)
|
935
|
+
$stream.toggle
|
936
|
+
next
|
937
|
+
when %r(^/location$)
|
938
|
+
$location.toggle
|
939
|
+
next
|
940
|
+
when %r(^/voice(?:\s+(change))?$)
|
941
|
+
if $1 == 'change'
|
942
|
+
change_voice
|
943
|
+
else
|
944
|
+
$voice.toggle
|
945
|
+
end
|
726
946
|
next
|
727
947
|
when %r(^/list(?:\s+(\d*))?$)
|
728
948
|
last = if $1
|
@@ -735,29 +955,14 @@ loop do
|
|
735
955
|
puts "Cleared messages."
|
736
956
|
next
|
737
957
|
when %r(^/clobber$)
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
case command
|
745
|
-
when 'clear'
|
746
|
-
tags = arg.present? ? arg.sub(/\A#*/, '') : nil
|
747
|
-
if tags
|
748
|
-
$documents.clear(tags:)
|
749
|
-
puts "Cleared tag ##{tags} from collection #{bold{collection}}."
|
750
|
-
else
|
751
|
-
$documents.clear
|
752
|
-
puts "Cleared collection #{bold{collection}}."
|
753
|
-
end
|
754
|
-
when 'change'
|
755
|
-
choose_collection(collection)
|
958
|
+
if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
|
959
|
+
clear_messages(messages)
|
960
|
+
$documents.clear
|
961
|
+
puts "Cleared messages and collection #{bold{$documents.collection}}."
|
962
|
+
else
|
963
|
+
puts 'Cancelled.'
|
756
964
|
end
|
757
965
|
next
|
758
|
-
when %r(/info)
|
759
|
-
info
|
760
|
-
next
|
761
966
|
when %r(^/pop(?:\s+(\d*))?$)
|
762
967
|
if messages.size > 1
|
763
968
|
n = $1.to_i.clamp(1, Float::INFINITY)
|
@@ -772,6 +977,10 @@ loop do
|
|
772
977
|
when %r(^/model$)
|
773
978
|
$model = choose_model('', $model)
|
774
979
|
next
|
980
|
+
when %r(^/system$)
|
981
|
+
change_system_prompt(messages, $system)
|
982
|
+
info
|
983
|
+
next
|
775
984
|
when %r(^/regenerate$)
|
776
985
|
if content = messages[-2]&.content
|
777
986
|
content.gsub!(/\nConsider these chunks for your answer.*\z/, '')
|
@@ -782,6 +991,38 @@ loop do
|
|
782
991
|
end
|
783
992
|
parse_content = false
|
784
993
|
content
|
994
|
+
when %r(^/collection(?:\s+(clear|change))?$)
|
995
|
+
case $1 || 'change'
|
996
|
+
when 'clear'
|
997
|
+
loop do
|
998
|
+
tags = $documents.tags.add('[EXIT]').add('[ALL]')
|
999
|
+
tag = Ollama::Utils::Chooser.choose(tags, prompt: 'Clear? %s')
|
1000
|
+
case tag
|
1001
|
+
when nil, '[EXIT]'
|
1002
|
+
puts "Exiting chooser."
|
1003
|
+
break
|
1004
|
+
when '[ALL]'
|
1005
|
+
if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
|
1006
|
+
$documents.clear
|
1007
|
+
puts "Cleared collection #{bold{$documents.collection}}."
|
1008
|
+
break
|
1009
|
+
else
|
1010
|
+
puts 'Cancelled.'
|
1011
|
+
sleep 3
|
1012
|
+
end
|
1013
|
+
when /./
|
1014
|
+
$documents.clear(tags: [ tag ])
|
1015
|
+
puts "Cleared tag #{tag} from collection #{bold{$documents.collection}}."
|
1016
|
+
sleep 3
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
when 'change'
|
1020
|
+
choose_collection($documents.collection)
|
1021
|
+
end
|
1022
|
+
next
|
1023
|
+
when %r(/info)
|
1024
|
+
info
|
1025
|
+
next
|
785
1026
|
when %r(^/import\s+(.+))
|
786
1027
|
parse_content = false
|
787
1028
|
content = import($1) or next
|
@@ -789,7 +1030,8 @@ loop do
|
|
789
1030
|
parse_content = false
|
790
1031
|
content = summarize($2, words: $1) or next
|
791
1032
|
when %r(^/embedding$)
|
792
|
-
|
1033
|
+
$embedding_paused.toggle(show: false)
|
1034
|
+
$embedding.show
|
793
1035
|
next
|
794
1036
|
when %r(^/embed\s+(.+))
|
795
1037
|
parse_content = false
|
@@ -828,12 +1070,12 @@ loop do
|
|
828
1070
|
end
|
829
1071
|
|
830
1072
|
content, tags = if parse_content
|
831
|
-
parse_content(content, images
|
1073
|
+
parse_content(content, images)
|
832
1074
|
else
|
833
1075
|
[ content, Utils::Tags.new ]
|
834
1076
|
end
|
835
1077
|
|
836
|
-
if
|
1078
|
+
if $embedding.on? && content
|
837
1079
|
records = $documents.find_where(
|
838
1080
|
content.downcase,
|
839
1081
|
tags:,
|
@@ -847,11 +1089,16 @@ loop do
|
|
847
1089
|
end
|
848
1090
|
end
|
849
1091
|
|
850
|
-
|
851
|
-
|
852
|
-
|
1092
|
+
if location = at_location.full?
|
1093
|
+
content += " [#{location} – do not comment on this information, just consider it for eventual queries]"
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
messages << Message.new(role: 'user', content:, images: images.dup)
|
1097
|
+
images.clear
|
1098
|
+
handler = FollowChat.new(messages:, markdown: $markdown.on?, voice: ($current_voice if $voice.on?))
|
1099
|
+
ollama.chat(model: $model, messages:, options:, stream: $stream.on?, &handler)
|
853
1100
|
|
854
|
-
if
|
1101
|
+
if $embedding.on? && !records.empty?
|
855
1102
|
puts "", records.map { |record|
|
856
1103
|
link = if record.source =~ %r(\Ahttps?://)
|
857
1104
|
record.source
|