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