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.
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