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.
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
- %s
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.fetch('OLLAMA_CHAT_COLLECTION', 'ollama_chat') %>
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::RedisCache
60
+ cache: Ollama::Documents::Cache::RedisBackedMemoryCache
51
61
  redis:
52
- url: <%= ENV.fetch('REDIS_URL', 'null') %>
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.present?
115
- markdown_content = Utils::ANSIMarkdown.parse(@messages.last.content)
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
- if images.present?
220
- ?📸
221
- else
222
- ?📨
223
- end
233
+ images.present? ? ?📸 : ?📨
224
234
  end
225
235
 
226
- def list_conversation(messages, markdown)
227
- messages.each do |m|
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 = if markdown && m.content.present?
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
- if m.images.present?
243
- message_text += "\nImages: " + italic { m.images.map(&:path) * ', ' }
244
- end
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 = <<~end
267
+ title = <<~EOT
261
268
  # #{feed&.channel&.title}
262
269
 
263
- end
270
+ EOT
264
271
  feed.items.inject(title) do |text, item|
265
- text << <<~end
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
- end
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 = <<~end
285
+ title = <<~EOT
279
286
  # #{feed.title.content}
280
287
 
281
- end
288
+ EOT
282
289
  feed.items.inject(title) do |text, item|
283
- text << <<~end
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
- end
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 import #{source_io&.content_type} document."
334
+ STDERR.puts "Cannot embed #{source_io&.content_type} document."
322
335
  return
323
336
  end
324
337
  end
325
338
 
326
- def import_document(source_io, source)
327
- unless $config.embedding.enabled
328
- STDOUT.puts "Embedding disabled, I won't import any documents, try: /summarize"
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 = case splitter_config.name
336
- when 'Character'
337
- Ollama::Documents::Splitters::Character.new(
338
- chunk_size: splitter_config.chunk_size,
339
- ).split(text)
340
- when 'RecursiveCharacter'
341
- Ollama::Documents::Splitters::RecursiveCharacter.new(
342
- chunk_size: splitter_config.chunk_size,
343
- ).split(text)
344
- when 'Semantic'
345
- Ollama::Documents::Splitters::Semantic.new(
346
- ollama:, model: $config.embedding.model.name,
347
- chunk_size: splitter_config.chunk_size,
348
- ).split(
349
- text,
350
- breakpoint: splitter_config.breakpoint.to_sym,
351
- percentage: splitter_config.percentage?,
352
- percentile: splitter_config.percentile?,
353
- )
354
- end
355
- $documents.add(inputs, source: source.to_s)
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 summarize(source)
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) or return
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
- import_document(source_io, source)
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
- $documents.collection = collection =
439
- Ollama::Utils::Chooser.choose(collections) || default_collection
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 <<~end
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
- end
526
+ EOT
452
527
  end
453
528
 
454
529
  def configure_cache
455
- Object.const_get($config.cache)
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 set_markdown(value)
462
- if value
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 display_chat_help
472
- puts <<~end
473
- /paste to paste content
474
- /markdown toggle markdown output
475
- /list list the messages of the conversation
476
- /clear clear the conversation messages
477
- /clobber clear conversation messages and collection
478
- /pop [n] pop the last n exchanges, defaults to 1
479
- /model change the model
480
- /regenerate the last answer message
481
- /collection clear [tag]|stats|change|new clear or show stats of current collection
482
- /summarize source summarize the URL/file source's content
483
- /web [n] query query web search & return n or 1 results
484
- /save filename store conversation messages
485
- /load filename load conversation messages
486
- /quit to quit
487
- /help to view this help
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 <<~end
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
- end
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:vh'
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 = choose_model(opts[?m], $config.model.name)
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.embedding.enabled
531
- embedding_model = $config.embedding.model.name
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: $config.embedding.model.name,
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
- import_document(doc_io, doc)
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 = Documents.new(ollama:, model:)
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 "Configured system prompt is:\n#{italic { system }}"
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 = set_markdown(!markdown)
725
+ $markdown = toggle_markdown
604
726
  next
605
- when %r(^/list$)
606
- list_conversation(messages, markdown)
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.clear
734
+ clear_messages(messages)
610
735
  puts "Cleared messages."
611
736
  next
612
737
  when %r(^/clobber$)
613
- messages.clear
738
+ clear_messages(messages)
614
739
  $documents.clear
615
740
  puts "Cleared messages and collection."
616
741
  next
617
- when %r(^/collection\s+(clear|stats|change|new)(?:\s+(.+))?$)
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(^/pop?(?:\s+(\d*))?$)
640
- n = $1.to_i.clamp(1, Float::INFINITY)
641
- r = messages.pop(2 * n)
642
- m = r.size / 2
643
- puts "Popped the last #{m} exchanges."
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(^/summarize\s+(.+))
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) do |url_io|
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
- content = <<~end
671
- Answer the the query #{$2.inspect} using these sources and summaries:
672
-
673
- #{urls.zip(urls_summarized).map { |u, s| "%s as \n:%s" % [ u, s ] } * "\n\n"}
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 $config.embedding.enabled && content
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
- found_texts = records.map(&:text)
709
- unless found_texts.empty?
710
- content += "\nConsider these chunks for your answer:\n"\
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:, voice:)
717
- ollama.chat(model:, messages:, options:, stream: true, &handler)
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