ollama-ruby 0.3.2 → 0.5.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 +4 -4
- data/CHANGES.md +131 -0
- data/README.md +21 -16
- data/Rakefile +6 -1
- data/bin/ollama_chat +303 -168
- data/bin/ollama_cli +11 -9
- data/lib/ollama/documents/cache/common.rb +17 -0
- data/lib/ollama/documents/{memory_cache.rb → cache/memory_cache.rb} +8 -10
- data/lib/ollama/documents/cache/redis_backed_memory_cache.rb +38 -0
- data/lib/ollama/documents/{redis_cache.rb → cache/redis_cache.rb} +18 -11
- data/lib/ollama/documents/splitters/character.rb +8 -6
- data/lib/ollama/documents/splitters/semantic.rb +1 -1
- data/lib/ollama/documents.rb +25 -19
- data/lib/ollama/utils/colorize_texts.rb +21 -1
- data/lib/ollama/utils/fetcher.rb +43 -10
- data/lib/ollama/utils/file_argument.rb +20 -4
- data/lib/ollama/utils/tags.rb +1 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama.rb +1 -0
- data/ollama-ruby.gemspec +11 -7
- data/spec/ollama/documents/memory_cache_spec.rb +16 -16
- data/spec/ollama/documents/redis_backed_memory_cache_spec.rb +106 -0
- data/spec/ollama/documents/redis_cache_spec.rb +36 -16
- data/spec/ollama/documents/splitters/character_spec.rb +28 -14
- data/spec/ollama/utils/fetcher_spec.rb +42 -1
- metadata +70 -8
data/bin/ollama_chat
CHANGED
@@ -5,6 +5,8 @@ include Ollama
|
|
5
5
|
require 'term/ansicolor'
|
6
6
|
include Term::ANSIColor
|
7
7
|
require 'tins'
|
8
|
+
require 'tins/xt/full'
|
9
|
+
require 'tins/xt/hash_union'
|
8
10
|
include Tins::GO
|
9
11
|
require 'reline'
|
10
12
|
require 'reverse_markdown'
|
@@ -14,6 +16,7 @@ require 'uri'
|
|
14
16
|
require 'nokogiri'
|
15
17
|
require 'rss'
|
16
18
|
require 'pdf/reader'
|
19
|
+
require 'csv'
|
17
20
|
|
18
21
|
class OllamaChatConfig
|
19
22
|
include ComplexConfig
|
@@ -22,16 +25,23 @@ class OllamaChatConfig
|
|
22
25
|
DEFAULT_CONFIG = <<~EOT
|
23
26
|
---
|
24
27
|
url: <%= ENV['OLLAMA_URL'] || 'http://%s' % ENV.fetch('OLLAMA_HOST') %>
|
28
|
+
proxy: null # http://localhost:8080
|
25
29
|
model:
|
26
30
|
name: <%= ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1') %>
|
27
31
|
options:
|
28
32
|
num_ctx: 8192
|
29
33
|
prompts:
|
30
34
|
system: <%= ENV.fetch('OLLAMA_CHAT_SYSTEM', 'null') %>
|
35
|
+
embed: "This source was now embedded: %{source}"
|
31
36
|
summarize: |
|
32
|
-
Generate an abstract summary of the content in this document
|
37
|
+
Generate an abstract summary of the content in this document using
|
38
|
+
%{words} words:
|
33
39
|
|
34
|
-
%
|
40
|
+
%{source_content}
|
41
|
+
web: |
|
42
|
+
Answer the the query %{query} using these sources and summaries:
|
43
|
+
|
44
|
+
%{results}
|
35
45
|
voice: Samantha
|
36
46
|
markdown: true
|
37
47
|
embedding:
|
@@ -41,16 +51,20 @@ class OllamaChatConfig
|
|
41
51
|
options: {}
|
42
52
|
# Retrieval prompt template:
|
43
53
|
prompt: 'Represent this sentence for searching relevant passages: %s'
|
44
|
-
collection: <%= ENV
|
54
|
+
collection: <%= ENV['OLLAMA_CHAT_COLLECTION'] %>
|
45
55
|
found_texts_size: 4096
|
46
56
|
found_texts_count: null
|
47
57
|
splitter:
|
48
58
|
name: RecursiveCharacter
|
49
59
|
chunk_size: 1024
|
50
|
-
cache: Ollama::Documents::
|
60
|
+
cache: Ollama::Documents::Cache::RedisBackedMemoryCache
|
51
61
|
redis:
|
52
|
-
|
62
|
+
documents:
|
63
|
+
url: <%= ENV.fetch('REDIS_URL', 'null') %>
|
64
|
+
expiring:
|
65
|
+
url: <%= ENV.fetch('REDIS_EXPIRING_URL', 'null') %>
|
53
66
|
debug: <%= ENV['OLLAMA_CHAT_DEBUG'].to_i == 1 ? true : false %>
|
67
|
+
ssl_no_verify: []
|
54
68
|
EOT
|
55
69
|
|
56
70
|
def initialize(filename = nil)
|
@@ -111,8 +125,8 @@ class FollowChat
|
|
111
125
|
end
|
112
126
|
content = response.message&.content
|
113
127
|
@messages.last.content << content
|
114
|
-
if @markdown and @messages.last.content.
|
115
|
-
markdown_content = Utils::ANSIMarkdown.parse(
|
128
|
+
if @markdown and content = @messages.last.content.full?
|
129
|
+
markdown_content = Utils::ANSIMarkdown.parse(content)
|
116
130
|
@output.print clear_screen, move_home, @user, ?\n, markdown_content
|
117
131
|
else
|
118
132
|
@output.print content
|
@@ -130,11 +144,11 @@ class FollowChat
|
|
130
144
|
prompt_eval_duration = response.prompt_eval_duration / 1e9
|
131
145
|
stats_text = {
|
132
146
|
eval_duration: Tins::Duration.new(eval_duration),
|
133
|
-
eval_count: response.eval_count,
|
134
|
-
eval_rate: bold { "%.2f c/s" % (response.eval_count / eval_duration) } + color(111),
|
147
|
+
eval_count: response.eval_count.to_i,
|
148
|
+
eval_rate: bold { "%.2f c/s" % (response.eval_count.to_i / eval_duration) } + color(111),
|
135
149
|
prompt_eval_duration: Tins::Duration.new(prompt_eval_duration),
|
136
|
-
prompt_eval_count: response.prompt_eval_count,
|
137
|
-
prompt_eval_rate: bold { "%.2f c/s" % (response.prompt_eval_count / prompt_eval_duration) } + color(111),
|
150
|
+
prompt_eval_count: response.prompt_eval_count.to_i,
|
151
|
+
prompt_eval_rate: bold { "%.2f c/s" % (response.prompt_eval_count.to_i / prompt_eval_duration) } + color(111),
|
138
152
|
total_duration: Tins::Duration.new(response.total_duration / 1e9),
|
139
153
|
load_duration: Tins::Duration.new(response.load_duration / 1e9),
|
140
154
|
}.map { _1 * '=' } * ' '
|
@@ -149,7 +163,7 @@ def search_web(query, n = nil)
|
|
149
163
|
n < 1 and n = 1
|
150
164
|
query = URI.encode_uri_component(query)
|
151
165
|
url = "https://www.duckduckgo.com/html/?q=#{query}"
|
152
|
-
Ollama::Utils::Fetcher.new.get(url) do |tmp|
|
166
|
+
Ollama::Utils::Fetcher.new(debug: $config.debug).get(url) do |tmp|
|
153
167
|
result = []
|
154
168
|
doc = Nokogiri::HTML(tmp)
|
155
169
|
doc.css('.results_links').each do |link|
|
@@ -216,32 +230,25 @@ def save_conversation(filename, messages)
|
|
216
230
|
end
|
217
231
|
|
218
232
|
def message_type(images)
|
219
|
-
|
220
|
-
?📸
|
221
|
-
else
|
222
|
-
?📨
|
223
|
-
end
|
233
|
+
images.present? ? ?📸 : ?📨
|
224
234
|
end
|
225
235
|
|
226
|
-
def list_conversation(messages,
|
227
|
-
messages.
|
236
|
+
def list_conversation(messages, last = nil)
|
237
|
+
last = (last || messages.size).clamp(0, messages.size)
|
238
|
+
messages[-last..-1].to_a.each do |m|
|
228
239
|
role_color = case m.role
|
229
240
|
when 'user' then 172
|
230
241
|
when 'assistant' then 111
|
231
242
|
when 'system' then 213
|
232
243
|
else 210
|
233
244
|
end
|
234
|
-
content =
|
235
|
-
Utils::ANSIMarkdown.parse(m.content)
|
236
|
-
else
|
237
|
-
m.content
|
238
|
-
end
|
245
|
+
content = m.content.full? { $markdown ? Utils::ANSIMarkdown.parse(_1) : _1 }
|
239
246
|
message_text = message_type(m.images) + " "
|
240
247
|
message_text += bold { color(role_color) { m.role } }
|
241
248
|
message_text += ":\n#{content}"
|
242
|
-
|
243
|
-
message_text += "\nImages: " + italic {
|
244
|
-
|
249
|
+
m.images.full? { |images|
|
250
|
+
message_text += "\nImages: " + italic { images.map(&:path) * ', ' }
|
251
|
+
}
|
245
252
|
puts message_text
|
246
253
|
end
|
247
254
|
end
|
@@ -257,37 +264,37 @@ end
|
|
257
264
|
|
258
265
|
def parse_rss(source_io)
|
259
266
|
feed = RSS::Parser.parse(source_io, false, false)
|
260
|
-
title = <<~
|
267
|
+
title = <<~EOT
|
261
268
|
# #{feed&.channel&.title}
|
262
269
|
|
263
|
-
|
270
|
+
EOT
|
264
271
|
feed.items.inject(title) do |text, item|
|
265
|
-
text << <<~
|
272
|
+
text << <<~EOT
|
266
273
|
## [#{item&.title}](#{item&.link})
|
267
274
|
|
268
275
|
updated on #{item&.pubDate}
|
269
276
|
|
270
277
|
#{reverse_markdown(item&.description)}
|
271
278
|
|
272
|
-
|
279
|
+
EOT
|
273
280
|
end
|
274
281
|
end
|
275
282
|
|
276
283
|
def parse_atom(source_io)
|
277
284
|
feed = RSS::Parser.parse(source_io, false, false)
|
278
|
-
title = <<~
|
285
|
+
title = <<~EOT
|
279
286
|
# #{feed.title.content}
|
280
287
|
|
281
|
-
|
288
|
+
EOT
|
282
289
|
feed.items.inject(title) do |text, item|
|
283
|
-
text << <<~
|
290
|
+
text << <<~EOT
|
284
291
|
## [#{item&.title&.content}](#{item&.link&.href})
|
285
292
|
|
286
293
|
updated on #{item&.updated&.content}
|
287
294
|
|
288
295
|
#{reverse_markdown(item&.content&.content)}
|
289
296
|
|
290
|
-
|
297
|
+
EOT
|
291
298
|
end
|
292
299
|
end
|
293
300
|
|
@@ -302,6 +309,16 @@ def parse_source(source_io)
|
|
302
309
|
end
|
303
310
|
source_io.rewind
|
304
311
|
source_io.read
|
312
|
+
when 'text/csv'
|
313
|
+
result = +''
|
314
|
+
CSV.table(File.new(source_io), col_sep: ?,).each do |row|
|
315
|
+
next if row.fields.select(&:present?).size == 0
|
316
|
+
result << row.map { |pair|
|
317
|
+
pair.compact.map { _1.to_s.strip } * ': ' if pair.last.present?
|
318
|
+
}.select(&:present?).map { _1.prepend(' ') } * ?\n
|
319
|
+
result << "\n\n"
|
320
|
+
end
|
321
|
+
result
|
305
322
|
when %r(\Atext/)
|
306
323
|
source_io.read
|
307
324
|
when 'application/rss+xml'
|
@@ -312,47 +329,53 @@ def parse_source(source_io)
|
|
312
329
|
source_io.read
|
313
330
|
when 'application/pdf'
|
314
331
|
reader = PDF::Reader.new(source_io)
|
315
|
-
result
|
316
|
-
reader.pages.each do |page|
|
317
|
-
result << page.text
|
318
|
-
end
|
319
|
-
result
|
332
|
+
reader.pages.inject(+'') { |result, page| result << page.text }
|
320
333
|
else
|
321
|
-
STDERR.puts "Cannot
|
334
|
+
STDERR.puts "Cannot embed #{source_io&.content_type} document."
|
322
335
|
return
|
323
336
|
end
|
324
337
|
end
|
325
338
|
|
326
|
-
def
|
327
|
-
|
328
|
-
|
329
|
-
return
|
330
|
-
end
|
331
|
-
puts "Importing #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
|
339
|
+
def embed_source(source_io, source)
|
340
|
+
embedding_enabled? or return parse_source(source_io)
|
341
|
+
puts "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
|
332
342
|
text = parse_source(source_io) or return
|
333
343
|
text.downcase!
|
334
344
|
splitter_config = $config.embedding.splitter
|
335
|
-
inputs =
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
345
|
+
inputs = nil
|
346
|
+
case splitter_config.name
|
347
|
+
when 'Character'
|
348
|
+
splitter = Ollama::Documents::Splitters::Character.new(
|
349
|
+
chunk_size: splitter_config.chunk_size,
|
350
|
+
)
|
351
|
+
inputs = splitter.split(text)
|
352
|
+
when 'RecursiveCharacter'
|
353
|
+
splitter = Ollama::Documents::Splitters::RecursiveCharacter.new(
|
354
|
+
chunk_size: splitter_config.chunk_size,
|
355
|
+
)
|
356
|
+
inputs = splitter.split(text)
|
357
|
+
when 'Semantic'
|
358
|
+
splitter = Ollama::Documents::Splitters::Semantic.new(
|
359
|
+
ollama:, model: $config.embedding.model.name,
|
360
|
+
chunk_size: splitter_config.chunk_size,
|
361
|
+
)
|
362
|
+
inputs = splitter.split(
|
363
|
+
text,
|
364
|
+
breakpoint: splitter_config.breakpoint.to_sym,
|
365
|
+
percentage: splitter_config.percentage?,
|
366
|
+
percentile: splitter_config.percentile?,
|
367
|
+
)
|
368
|
+
inputs = splitter.split(text)
|
369
|
+
end
|
370
|
+
inputs or return
|
371
|
+
source = source.to_s
|
372
|
+
if source.start_with?(?!)
|
373
|
+
source = Ollama::Utils::Width.truncate(
|
374
|
+
source[1..-1].gsub(/\W+/, ?_),
|
375
|
+
length: 10
|
376
|
+
)
|
377
|
+
end
|
378
|
+
$documents.add(inputs, source: source)
|
356
379
|
end
|
357
380
|
|
358
381
|
def add_image(images, source_io, source)
|
@@ -361,10 +384,27 @@ def add_image(images, source_io, source)
|
|
361
384
|
(images << image).uniq!
|
362
385
|
end
|
363
386
|
|
387
|
+
def http_options(url)
|
388
|
+
options = {}
|
389
|
+
if ssl_no_verify = $config.ssl_no_verify?
|
390
|
+
hostname = URI.parse(url).hostname
|
391
|
+
options |= { ssl_verify_peer: !ssl_no_verify.include?(hostname) }
|
392
|
+
end
|
393
|
+
if proxy = $config.proxy?
|
394
|
+
options |= { proxy: }
|
395
|
+
end
|
396
|
+
options
|
397
|
+
end
|
398
|
+
|
364
399
|
def fetch_source(source, &block)
|
365
400
|
case source
|
401
|
+
when %r(\A!(.*))
|
402
|
+
command = $1
|
403
|
+
Utils::Fetcher.execute(command) do |tmp|
|
404
|
+
block.(tmp)
|
405
|
+
end
|
366
406
|
when %r(\Ahttps?://\S+)
|
367
|
-
Utils::Fetcher.get(source) do |tmp|
|
407
|
+
Utils::Fetcher.get(source, debug: $config.debug, http_options: http_options(source)) do |tmp|
|
368
408
|
block.(tmp)
|
369
409
|
end
|
370
410
|
when %r(\Afile://(?:(?:[.-]|[[:alnum:]])*)(/\S*)|([~.]?/\S*))
|
@@ -380,16 +420,45 @@ rescue => e
|
|
380
420
|
STDERR.puts "Cannot add source #{source.to_s.inspect}: #{e}\n#{e.backtrace * ?\n}"
|
381
421
|
end
|
382
422
|
|
383
|
-
def
|
423
|
+
def import(source)
|
424
|
+
puts "Now importing #{source.to_s.inspect}."
|
425
|
+
fetch_source(source) do |source_io|
|
426
|
+
content = parse_source(source_io)
|
427
|
+
content.present? or return
|
428
|
+
source_io.rewind
|
429
|
+
content
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def summarize(source, words: nil)
|
434
|
+
words = words.to_i
|
435
|
+
words < 1 and words = 100
|
384
436
|
puts "Now summarizing #{source.to_s.inspect}."
|
385
437
|
source_content =
|
386
438
|
fetch_source(source) do |source_io|
|
387
|
-
content = parse_source(source_io)
|
439
|
+
content = parse_source(source_io)
|
440
|
+
content.present? or return
|
388
441
|
source_io.rewind
|
389
|
-
import_document(source_io, source)
|
390
442
|
content
|
391
443
|
end
|
392
|
-
$config.prompts.summarize % source_content
|
444
|
+
$config.prompts.summarize % { source_content:, words: }
|
445
|
+
end
|
446
|
+
|
447
|
+
def embed(source)
|
448
|
+
if embedding_enabled?
|
449
|
+
puts "Now embedding #{source.to_s.inspect}."
|
450
|
+
fetch_source(source) do |source_io|
|
451
|
+
content = parse_source(source_io)
|
452
|
+
content.present? or return
|
453
|
+
source_io.rewind
|
454
|
+
embed_source(source_io, source)
|
455
|
+
content
|
456
|
+
end
|
457
|
+
$config.prompts.embed % { source: }
|
458
|
+
else
|
459
|
+
puts "Embedding is off, so I will just give a small summary of this source."
|
460
|
+
summarize(source)
|
461
|
+
end
|
393
462
|
end
|
394
463
|
|
395
464
|
def parse_content(content, images)
|
@@ -407,7 +476,7 @@ def parse_content(content, images)
|
|
407
476
|
when 'image'
|
408
477
|
add_image(images, source_io, source)
|
409
478
|
when 'text', 'application'
|
410
|
-
|
479
|
+
embed_source(source_io, source)
|
411
480
|
else
|
412
481
|
STDERR.puts(
|
413
482
|
"Cannot fetch #{source.to_s.inspect} with content type "\
|
@@ -434,62 +503,112 @@ end
|
|
434
503
|
|
435
504
|
def choose_collection(default_collection)
|
436
505
|
collections = [ default_collection ] + $documents.collections
|
437
|
-
collections = collections.uniq.sort
|
438
|
-
|
439
|
-
|
506
|
+
collections = collections.compact.map(&:to_s).uniq.sort
|
507
|
+
collections.unshift('[NEW]')
|
508
|
+
collection = Ollama::Utils::Chooser.choose(collections) || default_collection
|
509
|
+
if collection == '[NEW]'
|
510
|
+
print "Enter name of the new collection: "
|
511
|
+
collection = STDIN.gets.chomp
|
512
|
+
end
|
513
|
+
$documents.collection = collection
|
440
514
|
ensure
|
441
515
|
puts "Changing to collection #{bold{collection}}."
|
442
516
|
collection_stats
|
443
517
|
end
|
444
518
|
|
445
519
|
def collection_stats
|
446
|
-
puts <<~
|
520
|
+
puts <<~EOT
|
447
521
|
Collection
|
448
522
|
Name: #{bold{$documents.collection}}
|
523
|
+
Embedding model: #{bold{$embedding_model}}
|
449
524
|
#Embeddings: #{$documents.size}
|
450
525
|
Tags: #{$documents.tags}
|
451
|
-
|
526
|
+
EOT
|
452
527
|
end
|
453
528
|
|
454
529
|
def configure_cache
|
455
|
-
|
530
|
+
if $opts[?M]
|
531
|
+
Ollama::Documents::MemoryCache
|
532
|
+
else
|
533
|
+
Object.const_get($config.cache)
|
534
|
+
end
|
456
535
|
rescue => e
|
457
536
|
STDERR.puts "Caught #{e.class}: #{e} => Falling back to MemoryCache."
|
458
537
|
Ollama::Documents::MemoryCache
|
459
538
|
end
|
460
539
|
|
461
|
-
def
|
462
|
-
|
540
|
+
def toggle_markdown
|
541
|
+
$markdown = !$markdown
|
542
|
+
show_markdown
|
543
|
+
end
|
544
|
+
|
545
|
+
def show_markdown
|
546
|
+
if $markdown
|
463
547
|
puts "Using ANSI markdown to output content."
|
464
|
-
true
|
465
548
|
else
|
466
549
|
puts "Using plaintext for outputting content."
|
467
|
-
false
|
468
550
|
end
|
551
|
+
$markdown
|
469
552
|
end
|
470
553
|
|
471
|
-
def
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
554
|
+
def set_embedding(embedding)
|
555
|
+
$embedding_enabled = embedding
|
556
|
+
show_embedding
|
557
|
+
end
|
558
|
+
|
559
|
+
def show_embedding
|
560
|
+
puts "Embedding is #{embedding_enabled? ? "on" : "off"}."
|
561
|
+
$embedding_enabled
|
562
|
+
end
|
563
|
+
|
564
|
+
def embedding_enabled?
|
565
|
+
$embedding_enabled && !$embedding_paused
|
566
|
+
end
|
567
|
+
|
568
|
+
def toggle_embedding_paused
|
569
|
+
$embedding_paused = !$embedding_paused
|
570
|
+
show_embedding
|
571
|
+
end
|
572
|
+
|
573
|
+
def info
|
574
|
+
puts "Current model is #{bold{$model}}."
|
575
|
+
collection_stats
|
576
|
+
if show_embedding
|
577
|
+
puts "Text splitter is #{bold{$config.embedding.splitter.name}}."
|
488
578
|
end
|
579
|
+
puts "Documents database cache is #{$documents.nil? ? 'n/a' : $documents.cache.class}"
|
580
|
+
show_markdown
|
581
|
+
end
|
582
|
+
|
583
|
+
def clear_messages(messages)
|
584
|
+
messages.delete_if { _1.role != 'system' }
|
585
|
+
end
|
586
|
+
|
587
|
+
def display_chat_help
|
588
|
+
puts <<~EOT
|
589
|
+
/paste to paste content
|
590
|
+
/markdown toggle markdown output
|
591
|
+
/list [n] list the last n / all conversation exchanges
|
592
|
+
/clear clear the whole conversation
|
593
|
+
/clobber clear the conversation and collection
|
594
|
+
/pop [n] pop the last n exchanges, defaults to 1
|
595
|
+
/model change the model
|
596
|
+
/regenerate the last answer message
|
597
|
+
/collection clear [tag]|change clear or show stats of current collection
|
598
|
+
/import source import the source's content
|
599
|
+
/summarize [n] source summarize the source's content in n words
|
600
|
+
/embedding toggle embedding paused or not
|
601
|
+
/embed source embed the source's content
|
602
|
+
/web [n] query query web search & return n or 1 results
|
603
|
+
/save filename store conversation messages
|
604
|
+
/load filename load conversation messages
|
605
|
+
/quit to quit
|
606
|
+
/help to view this help
|
607
|
+
EOT
|
489
608
|
end
|
490
609
|
|
491
610
|
def usage
|
492
|
-
puts <<~
|
611
|
+
puts <<~EOT
|
493
612
|
#{File.basename($0)} [OPTIONS]
|
494
613
|
|
495
614
|
-f CONFIG config file to read
|
@@ -498,11 +617,13 @@ def usage
|
|
498
617
|
-s SYSTEM the system prompt to use as a file, OLLAMA_CHAT_SYSTEM
|
499
618
|
-c CHAT a saved chat conversation to load
|
500
619
|
-C COLLECTION name of the collection used in this conversation
|
501
|
-
-D DOCUMENT load document and add to collection (multiple)
|
620
|
+
-D DOCUMENT load document and add to embeddings collection (multiple)
|
621
|
+
-M use (empty) MemoryCache for this chat session
|
622
|
+
-E disable embeddings for this chat session
|
502
623
|
-v use voice output
|
503
624
|
-h this help
|
504
625
|
|
505
|
-
|
626
|
+
EOT
|
506
627
|
exit 0
|
507
628
|
end
|
508
629
|
|
@@ -510,38 +631,44 @@ def ollama
|
|
510
631
|
$ollama
|
511
632
|
end
|
512
633
|
|
513
|
-
opts = go 'f:u:m:s:c:C:D:
|
634
|
+
$opts = go 'f:u:m:s:c:C:D:MEvh'
|
514
635
|
|
515
|
-
config = OllamaChatConfig.new(opts[?f])
|
636
|
+
config = OllamaChatConfig.new($opts[?f])
|
516
637
|
$config = config.config
|
517
638
|
|
518
|
-
opts[?h] and usage
|
639
|
+
$opts[?h] and usage
|
519
640
|
|
520
641
|
puts "Configuration read from #{config.filename.inspect} is:", $config
|
521
642
|
|
522
|
-
base_url = opts[?u] || $config.url
|
643
|
+
base_url = $opts[?u] || $config.url
|
523
644
|
$ollama = Client.new(base_url:, debug: $config.debug)
|
524
645
|
|
525
|
-
model
|
646
|
+
$model = choose_model($opts[?m], $config.model.name)
|
526
647
|
options = Options[$config.model.options]
|
527
|
-
model_system = pull_model_unless_present(model, options)
|
648
|
+
model_system = pull_model_unless_present($model, options)
|
528
649
|
messages = []
|
650
|
+
set_embedding($config.embedding.enabled && !$opts[?E])
|
529
651
|
|
530
|
-
if $config.
|
531
|
-
|
652
|
+
if voice = ($config.voice if $opts[?v])
|
653
|
+
puts "Using voice #{bold{voice}} to speak."
|
654
|
+
end
|
655
|
+
$markdown = $config.markdown
|
656
|
+
|
657
|
+
if embedding_enabled?
|
658
|
+
$embedding_model = $config.embedding.model.name
|
532
659
|
embedding_model_options = Options[$config.embedding.model.options]
|
533
|
-
pull_model_unless_present(embedding_model, embedding_model_options)
|
534
|
-
collection = opts[?C] || $config.embedding.collection
|
660
|
+
pull_model_unless_present($embedding_model, embedding_model_options)
|
661
|
+
collection = $opts[?C] || $config.embedding.collection
|
535
662
|
$documents = Documents.new(
|
536
663
|
ollama:,
|
537
|
-
model: $
|
664
|
+
model: $embedding_model,
|
538
665
|
model_options: $config.embedding.model.options,
|
539
666
|
collection:,
|
540
667
|
cache: configure_cache,
|
541
|
-
redis_url: $config.redis.url?,
|
668
|
+
redis_url: $config.redis.documents.url?,
|
542
669
|
)
|
543
670
|
|
544
|
-
document_list = opts[?D].to_a
|
671
|
+
document_list = $opts[?D].to_a
|
545
672
|
if document_list.any?(&:empty?)
|
546
673
|
puts "Clearing collection #{bold{collection}}."
|
547
674
|
$documents.clear
|
@@ -559,28 +686,26 @@ if $config.embedding.enabled
|
|
559
686
|
document_list.each_slice(25) do |docs|
|
560
687
|
docs.each do |doc|
|
561
688
|
fetch_source(doc) do |doc_io|
|
562
|
-
|
689
|
+
embed_source(doc_io, doc)
|
563
690
|
end
|
564
691
|
end
|
565
692
|
end
|
566
693
|
end
|
567
694
|
collection_stats
|
568
695
|
else
|
569
|
-
$documents =
|
570
|
-
end
|
571
|
-
|
572
|
-
if voice = ($config.voice if opts[?v])
|
573
|
-
puts "Using voice #{bold{voice}} to speak."
|
696
|
+
$documents = Tins::NULL
|
574
697
|
end
|
575
|
-
markdown = set_markdown($config.markdown)
|
576
698
|
|
577
|
-
if opts[?c]
|
578
|
-
messages.concat load_conversation(opts[?c])
|
699
|
+
if $opts[?c]
|
700
|
+
messages.concat load_conversation($opts[?c])
|
579
701
|
else
|
580
702
|
if system = Ollama::Utils::FileArgument.
|
581
|
-
get_file_argument(opts[?s], default: $config.prompts.system? || model_system)
|
703
|
+
get_file_argument($opts[?s], default: $config.prompts.system? || model_system)
|
582
704
|
messages << Message.new(role: 'system', content: system)
|
583
|
-
puts
|
705
|
+
puts <<~EOT
|
706
|
+
Configured system prompt is:
|
707
|
+
#{italic{Ollama::Utils::Width.wrap(system, percentage: 90)}}
|
708
|
+
EOT
|
584
709
|
end
|
585
710
|
end
|
586
711
|
|
@@ -596,25 +721,25 @@ loop do
|
|
596
721
|
when %r(^/paste$)
|
597
722
|
puts bold { "Paste your content and then press C-d!" }
|
598
723
|
content = STDIN.read
|
599
|
-
when %r(^/quit$)
|
600
|
-
puts "Goodbye."
|
601
|
-
exit 0
|
602
724
|
when %r(^/markdown$)
|
603
|
-
markdown =
|
725
|
+
$markdown = toggle_markdown
|
604
726
|
next
|
605
|
-
when %r(^/list
|
606
|
-
|
727
|
+
when %r(^/list(?:\s+(\d*))?$)
|
728
|
+
last = if $1
|
729
|
+
2 * $1.to_i
|
730
|
+
end
|
731
|
+
list_conversation(messages, last)
|
607
732
|
next
|
608
733
|
when %r(^/clear$)
|
609
|
-
messages
|
734
|
+
clear_messages(messages)
|
610
735
|
puts "Cleared messages."
|
611
736
|
next
|
612
737
|
when %r(^/clobber$)
|
613
|
-
messages
|
738
|
+
clear_messages(messages)
|
614
739
|
$documents.clear
|
615
740
|
puts "Cleared messages and collection."
|
616
741
|
next
|
617
|
-
when %r(^/collection\s+(clear|
|
742
|
+
when %r(^/collection\s+(clear|change)(?:\s+(.+))?$)
|
618
743
|
command, arg = $1, $2
|
619
744
|
case command
|
620
745
|
when 'clear'
|
@@ -626,24 +751,26 @@ loop do
|
|
626
751
|
$documents.clear
|
627
752
|
puts "Cleared collection #{bold{collection}}."
|
628
753
|
end
|
629
|
-
when 'stats'
|
630
|
-
collection_stats
|
631
754
|
when 'change'
|
632
755
|
choose_collection(collection)
|
633
|
-
when 'new'
|
634
|
-
print "Enter name of the new collection: "
|
635
|
-
$documents.collection = collection = STDIN.gets.chomp
|
636
|
-
collection_stats
|
637
756
|
end
|
638
757
|
next
|
639
|
-
when %r(
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
758
|
+
when %r(/info)
|
759
|
+
info
|
760
|
+
next
|
761
|
+
when %r(^/pop(?:\s+(\d*))?$)
|
762
|
+
if messages.size > 1
|
763
|
+
n = $1.to_i.clamp(1, Float::INFINITY)
|
764
|
+
r = messages.pop(2 * n)
|
765
|
+
m = r.size / 2
|
766
|
+
puts "Popped the last #{m} exchanges."
|
767
|
+
else
|
768
|
+
puts "No more exchanges you can pop."
|
769
|
+
end
|
770
|
+
list_conversation(messages, 2)
|
644
771
|
next
|
645
772
|
when %r(^/model$)
|
646
|
-
model = choose_model('', model)
|
773
|
+
$model = choose_model('', $model)
|
647
774
|
next
|
648
775
|
when %r(^/regenerate$)
|
649
776
|
if content = messages[-2]&.content
|
@@ -655,23 +782,29 @@ loop do
|
|
655
782
|
end
|
656
783
|
parse_content = false
|
657
784
|
content
|
658
|
-
when %r(^/
|
785
|
+
when %r(^/import\s+(.+))
|
786
|
+
parse_content = false
|
787
|
+
content = import($1) or next
|
788
|
+
when %r(^/summarize\s+(?:(\d+)\s+)?(.+))
|
659
789
|
parse_content = false
|
660
|
-
content = summarize($1) or next
|
790
|
+
content = summarize($2, words: $1) or next
|
791
|
+
when %r(^/embedding$)
|
792
|
+
toggle_embedding_paused
|
793
|
+
next
|
794
|
+
when %r(^/embed\s+(.+))
|
795
|
+
parse_content = false
|
796
|
+
content = embed($1) or next
|
661
797
|
when %r(^/web\s+(?:(\d+)\s+)?(.+))
|
662
798
|
parse_content = false
|
663
799
|
urls = search_web($2, $1.to_i)
|
664
800
|
urls.each do |url|
|
665
|
-
fetch_source(url)
|
666
|
-
import_document(url_io, url)
|
667
|
-
end
|
801
|
+
fetch_source(url) { |url_io| embed_source(url_io, url) }
|
668
802
|
end
|
669
803
|
urls_summarized = urls.map { summarize(_1) }
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
end
|
804
|
+
query = $2.inspect
|
805
|
+
results = urls.zip(urls_summarized).
|
806
|
+
map { |u, s| "%s as \n:%s" % [ u, s ] } * "\n\n"
|
807
|
+
content = $config.prompts.web % { query:, results: }
|
675
808
|
when %r(^/save\s+(.+)$)
|
676
809
|
save_conversation($1, messages)
|
677
810
|
puts "Saved conversation to #$1."
|
@@ -680,6 +813,9 @@ loop do
|
|
680
813
|
messages = load_conversation($1)
|
681
814
|
puts "Loaded conversation from #$1."
|
682
815
|
next
|
816
|
+
when %r(^/quit$)
|
817
|
+
puts "Goodbye."
|
818
|
+
exit 0
|
683
819
|
when %r(^/)
|
684
820
|
display_chat_help
|
685
821
|
next
|
@@ -697,7 +833,7 @@ loop do
|
|
697
833
|
[ content, Utils::Tags.new ]
|
698
834
|
end
|
699
835
|
|
700
|
-
if
|
836
|
+
if embedding_enabled? && content
|
701
837
|
records = $documents.find_where(
|
702
838
|
content.downcase,
|
703
839
|
tags:,
|
@@ -705,19 +841,18 @@ loop do
|
|
705
841
|
text_size: $config.embedding.found_texts_size?,
|
706
842
|
text_count: $config.embedding.found_texts_count?,
|
707
843
|
)
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
"#{found_texts.join("\n\n---\n\n")}"
|
844
|
+
unless records.empty?
|
845
|
+
content += "\nConsider these chunks for your answer:\n\n"\
|
846
|
+
"#{records.map { [ _1.text, _1.tags_set ] * ?\n }.join("\n\n---\n\n")}"
|
712
847
|
end
|
713
848
|
end
|
714
849
|
|
715
850
|
messages << Message.new(role: 'user', content:, images:)
|
716
|
-
handler = FollowChat.new(messages:, markdown
|
717
|
-
ollama.chat(model
|
851
|
+
handler = FollowChat.new(messages:, markdown: $markdown, voice:)
|
852
|
+
ollama.chat(model: $model, messages:, options:, stream: true, &handler)
|
718
853
|
|
719
|
-
if records
|
720
|
-
puts records.map { |record|
|
854
|
+
if embedding_enabled? && !records.empty?
|
855
|
+
puts "", records.map { |record|
|
721
856
|
link = if record.source =~ %r(\Ahttps?://)
|
722
857
|
record.source
|
723
858
|
else
|