ollama_chat 0.0.121 → 0.0.122

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +117 -0
  3. data/README.md +2 -2
  4. data/Rakefile +2 -1
  5. data/lib/ollama_chat/asr.rb +110 -0
  6. data/lib/ollama_chat/chat.rb +27 -4
  7. data/lib/ollama_chat/commands.rb +30 -19
  8. data/lib/ollama_chat/database/models/session.rb +1 -1
  9. data/lib/ollama_chat/dialog.rb +6 -0
  10. data/lib/ollama_chat/http_handling.rb +25 -0
  11. data/lib/ollama_chat/information.rb +7 -1
  12. data/lib/ollama_chat/invidious.rb +162 -0
  13. data/lib/ollama_chat/model_handling.rb +23 -2
  14. data/lib/ollama_chat/oc.rb +53 -0
  15. data/lib/ollama_chat/ollama_chat_config/default_config.yml +4 -1
  16. data/lib/ollama_chat/parsing.rb +54 -11
  17. data/lib/ollama_chat/personae_management.rb +1 -1
  18. data/lib/ollama_chat/prompt_management.rb +66 -2
  19. data/lib/ollama_chat/say.rb +4 -0
  20. data/lib/ollama_chat/source_fetching.rb +20 -11
  21. data/lib/ollama_chat/speaker.rb +70 -0
  22. data/lib/ollama_chat/state_selectors.rb +35 -11
  23. data/lib/ollama_chat/think_control.rb +39 -9
  24. data/lib/ollama_chat/tools/generate_image.rb +5 -5
  25. data/lib/ollama_chat/tools/get_url.rb +29 -4
  26. data/lib/ollama_chat/tools/patch_file.rb +5 -1
  27. data/lib/ollama_chat/tts.rb +24 -21
  28. data/lib/ollama_chat/utils/audio_player.rb +1 -1
  29. data/lib/ollama_chat/utils/fetcher.rb +40 -10
  30. data/lib/ollama_chat/utils/png_metadata_extractor.rb +1 -0
  31. data/lib/ollama_chat/utils.rb +7 -7
  32. data/lib/ollama_chat/version.rb +1 -1
  33. data/lib/ollama_chat.rb +2 -0
  34. data/ollama_chat.gemspec +7 -6
  35. data/spec/assets/linux.oga +0 -0
  36. data/spec/ollama_chat/asr_spec.rb +89 -0
  37. data/spec/ollama_chat/commands_spec.rb +120 -5
  38. data/spec/ollama_chat/http_handling_spec.rb +145 -0
  39. data/spec/ollama_chat/invidious_spec.rb +238 -0
  40. data/spec/ollama_chat/message_list_spec.rb +1 -2
  41. data/spec/ollama_chat/model_handling_spec.rb +42 -0
  42. data/spec/ollama_chat/parsing_spec.rb +61 -2
  43. data/spec/ollama_chat/session_management_spec.rb +5 -1
  44. data/spec/ollama_chat/source_fetching_spec.rb +32 -0
  45. data/spec/ollama_chat/tools/directory_structure_spec.rb +1 -1
  46. data/spec/ollama_chat/tools/get_url_spec.rb +58 -19
  47. data/spec/ollama_chat/tts_spec.rb +9 -11
  48. data/spec/ollama_chat/utils/audio_player_spec.rb +4 -4
  49. data/spec/spec_helper.rb +1 -1
  50. metadata +30 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e457507428f39cb5f1641783d067f1424a1dca8908cc30e8331d05e141c68f2e
4
- data.tar.gz: 9c14299158dec444c3debd7ba133678c04324a6985f26110ce779da37bdf295a
3
+ metadata.gz: d77fd2cddf7c921b77712a90d4d1d31b3daf0f80dc4170f71eb209e7ce46390b
4
+ data.tar.gz: af9f30997ca31d67d7ba1914a971b5abb165d1913b2723c0727f07929b4512e4
5
5
  SHA512:
6
- metadata.gz: eafc7e3075b48d10dc9ad7ac1654cdb5af4e2c7b235cf2ff4e8ad4240518c77afc02167689c831987b25c9ac07d68f2a758d6d85d9bc4a1bad4537decc01ef26
7
- data.tar.gz: 9492a500da2006ef57212e0f1a68444e35d94be81ad2a5abbd041daf73d9a3087af9873d36264182c77a7b76d3825697eaea45189dd69b227178df569c558c23
6
+ metadata.gz: ba243ffe79c5062b0feb5fc2d6389efff95216961567dbe9275f020cbdf811d1f04f238e95972d007e611109c3bcc795057c8a2c4afeeaf7d1b739301b91c0f5
7
+ data.tar.gz: badb4e4ccca4ee006cdb48fd66b107367c81d7bf3ee3717482e4c61f3bc4d6fcda80d4cc54edefb9a0b5af5a6dbcf32173eeb3e0f48f545d32f5c3508d472ba1
data/CHANGES.md CHANGED
@@ -1,5 +1,122 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-25 v0.0.122
4
+
5
+ ## Features
6
+
7
+ * **Per-model thinking-state support**: Added support for per-model thinking
8
+ states via the `/api/show` endpoint.
9
+ * Replaced the `THINK_MODE_STATES` constant with a `think_mode_states`
10
+ method that reads `@model_metadata&.thinking&.full?(:values)` to return
11
+ a per-model subset, falling back to the full superset for
12
+ pre-**0.34.3** Ollama or non-thinking models.
13
+ * Added `normalize_think_state` helper to canonicalize boolean
14
+ (`true`/`false`) and string (`"low"`/`"high"`/`"max"`) thinking values.
15
+ * Added `:thinking` field to `ModelMetadata` data class.
16
+ * Implemented think-mode clamping in `use_model` to disable thinking for
17
+ non-thinking models or clamp to the model's default when the current
18
+ level is unavailable.
19
+ * Converted `DatabaseStateSelector` `states`/`off` to support `Proc` via
20
+ `ask_and_send_or_self(:call, @chat)`.
21
+ * Converted `think_mode` and `voices` selectors in
22
+ `setup_state_selectors` to lambda-based `states:`.
23
+ * Added `Thinking` display line to `info_model`.
24
+ * **Invidious YouTube captions**: Added `OllamaChat::Invidious` to fetch
25
+ video metadata and transcripts from an Invidious companion instance.
26
+ * Uses `POST /companion/youtubei/v1/player` for `videoDetails` and
27
+ `captionTracks`.
28
+ * Selects the best caption track by priority-ordered language regexes.
29
+ * Fetches timedtext XML and extracts `<text>` elements via `Nokogiri`,
30
+ decoding double-encoded HTML entities.
31
+ * Returns a formatted markdown header + transcript as a text IO.
32
+ * **Unified HTTP handling**: Added `HTTPHandling#request_url_response` to
33
+ consolidate `Excon.new` call sites, merging `http_options` (SSL, proxy)
34
+ with caller-supplied options.
35
+ * Updated `ASR.transcribe`, `TTS.voices`, `TTS#fetch_tts`,
36
+ `GenerateImage#post_url`, `GenerateImage#get_url`, `GetURL#execute`,
37
+ `Invidious.fetch_video_info`, and `SourceFetching#fetch_source` to
38
+ route through this method.
39
+ * Extracted audio/video MIME regex into `HAS_AUDIO` constant shared by
40
+ `parse_source` and the `get_url` tool.
41
+ * Split `Fetcher` `middlewares` into class-level `self.middlewares` and
42
+ instance delegate; added `RedirectFollower` to the default stack.
43
+ * Added `middlewares:` to streaming `excon` requests in `fetcher.rb`.
44
+ * **One-shot voice notification**: Added `OllamaChat::Speaker` concern
45
+ providing `speak(text, background:)` and `cancel_speaking`.
46
+ * `OllamaChat::TTS` and `OllamaChat::Say` now include
47
+ `OllamaChat::Speaker`.
48
+ * Added `Chat#speak` facade to resolve the configured voice handler.
49
+ * `Dialog#readline_prompt` and `Dialog#confirm?` now speak the prompt
50
+ with a 10-second delay.
51
+ * `Tools::PatchFile` announces "You can review the patch for the file …
52
+ now!" before opening `vimdiff`.
53
+ * `AudioPlayer#start` redirects `ffplay` stdout/stderr to `/dev/null`.
54
+ * **ASR transcription pipeline**: Added `OllamaChat::ASR` client for
55
+ audio/video transcription.
56
+ * Uploads audio/video as multipart form-data to the audio.cpp
57
+ `/v1/audio/transcriptions` endpoint.
58
+ * Converts video sources to 16 kHz mono PCM WAV via a configurable
59
+ `ffmpeg` command (`CONVERT_COMMAND`).
60
+ * Added `OC::OLLAMA::CHAT::ASR` config module with `URL`,
61
+ `CONVERT_COMMAND`, and `MODEL` settings.
62
+ * Extended `/input` command with `-l <language>` option.
63
+ * Routed `audio/*`, `video/*`, and specific `application/` MIME types
64
+ into `parse_audio`.
65
+ * Added `Fetcher::ResponseMetadata.as_text(text)` factory for synthetic
66
+ `text/plain` IO responses.
67
+ * Replaced hardcoded timeouts in `lib/ollama_chat/tts.rb` with values
68
+ from `config.timeouts`.
69
+ * **Audio/Video support in `get_url`**: Added `language` parameter to the
70
+ `get_url` tool definition for optional transcription hints.
71
+ * Pre-transcribes `audio/*` and `video/*` sources via `chat.parse_audio`
72
+ before content-type dispatch.
73
+ * Threads `language:` kwarg into `chat.import_source`.
74
+ * **Prompt management enhancements**:
75
+ * Added `reset_prompt_count` and `choose_and_reset_count` to
76
+ `lib/ollama_chat/prompt_management.rb`.
77
+ * Added `/prompt reset count` command.
78
+ * Added `count:` keyword argument to `choose_prompt` to increment
79
+ `metadata['use_count']`.
80
+ * Changed `all_prompts` sort to `use_count` descending, falling back to
81
+ name for tie-breaking.
82
+ * Added `-i` summary instruction flag to `/input` command to prompt for
83
+ an optional instruction.
84
+ * Added `instruction` parameter to `get_url` summarizing, threading it
85
+ through `summarize_source` and `summarize`.
86
+ * **Config-driven default persona**: Added `persona.default_name` to
87
+ `default_config.yml`.
88
+ * Updated `initial_persona_name` in `personae_management.rb` to fall back
89
+ to `config.persona&.default_name`.
90
+
91
+ ## Improvements
92
+
93
+ * **CI and Dependencies**:
94
+ * Added `ffmpeg` to the CI Docker image.
95
+ * Added `nokogiri ~> 1.0` dependency.
96
+ * Bumped `tins ~> 1.58`.
97
+ * Added `require 'ollama_chat/utils/utf8_converter'` in
98
+ `png_metadata_extractor.rb`.
99
+ * Reorganized `require` statements in `lib/ollama_chat/utils.rb`
100
+ alphabetically.
101
+ * **Error Handling**:
102
+ * In `lib/ollama_chat/asr.rb`, capture the return value of `system` to
103
+ detect missing executables and print a clear "not found in PATH"
104
+ message.
105
+ * Extract executable name via `Shellwords.split(cmd).first` for error
106
+ messages.
107
+ * **Documentation**:
108
+ * Updated `ollama_chat` description in README to be more concise and
109
+ user-focused.
110
+ * Updated YARD in `session.rb`, `state_selectors.rb`, and
111
+ `think_control.rb` to reference `Chat#think_mode_states`.
112
+
113
+ ## Bug Fixes
114
+
115
+ * **Pager Spec**: Changed `expect(Tins::Terminal).to receive(:lines)` to
116
+ `allow(Tins::Terminal).to receive(:lines)` in
117
+ `spec/ollama_chat/message_list_spec.rb` and removed the `skip 'no tty'`
118
+ guard.
119
+
3
120
  ## 2026-09-18 v0.0.121
4
121
 
5
122
  * Added `**_kwargs` splat to `Say.voices` to prevent `ArgumentError` when the
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Description
4
4
 
5
- **ollama_chat** is a chat client, that can be used to connect to an ollama
6
- server and enter chat conversations with the LLMs provided by it.
5
+ **ollama_chat** is a terminal chat client that connects to an Ollama
6
+ server and lets you converse with the LLMs it provides.
7
7
 
8
8
  ## Documentation
9
9
 
data/Rakefile CHANGED
@@ -49,7 +49,7 @@ GemHadar do
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
50
  dependency 'kramdown-ansi', '~> 0.5'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
- dependency 'tins', '~> 1.57'
52
+ dependency 'tins', '~> 1.58'
53
53
  dependency 'search_ui', '>= 0.2'
54
54
  dependency 'amatch', '~> 0.4'
55
55
  dependency 'pdf-reader', '~> 2.0'
@@ -61,6 +61,7 @@ GemHadar do
61
61
  dependency 'rubyzip', '~> 3.0'
62
62
  dependency 'sequel', '~> 5.0'
63
63
  dependency 'file-tail', '~> 1.4'
64
+ dependency 'nokogiri', '~> 1.0'
64
65
  development_dependency 'all_images', '~> 0.12'
65
66
  development_dependency 'rspec', '~> 3.2'
66
67
  development_dependency 'kramdown', '~> 2.0'
@@ -0,0 +1,110 @@
1
+ require 'securerandom'
2
+
3
+ # Standalone ASR (Speech-to-Text) transcription client for audio.cpp.
4
+ #
5
+ # Transcribes audio or video sources by uploading them as multipart
6
+ # form-data to the audio.cpp `/v1/audio/transcriptions` endpoint.
7
+ # Video sources are first converted to 16 kHz mono WAV via ffmpeg.
8
+ #
9
+ # @example Transcribing a WAV source
10
+ # text = OllamaChat::ASR.transcribe(source_io, chat:)
11
+ #
12
+ # @example Transcribing an MP4 video (ffmpeg extraction)
13
+ # text = OllamaChat::ASR.transcribe(source_io, chat:)
14
+ module OllamaChat
15
+ class ASR
16
+ class << self
17
+ # Transcribes audio/video content via the audio.cpp ASR endpoint.
18
+ #
19
+ # @param source_io [IO] the audio/video source stream
20
+ # (must respond to #content_type and be seekable)
21
+ # @param language [String, nil] optional language hint
22
+ # (e.g. "en", "de") passed as the `language` form field
23
+ # @param chat [OllamaChat::Chat] the chat instance (for HTTP middleware)
24
+ #
25
+ # @return [String, nil] the transcribed text, or nil on failure
26
+ def transcribe(source_io, chat:, language: nil)
27
+ asr_url = OC::OLLAMA::CHAT::ASR::URL
28
+ asr_model = OC::OLLAMA::CHAT::ASR::MODEL
29
+ input = Tempfile.new(['asr_in'])
30
+ wav = Tempfile.new(['asr', '.wav'])
31
+ begin
32
+ IO.copy_stream(source_io, input)
33
+ input.flush
34
+
35
+ cmd = OC::OLLAMA::CHAT::ASR::CONVERT_COMMAND % {
36
+ input: input.path, output: wav.path
37
+ }
38
+ executable = Shellwords.split(cmd).first
39
+ result = system(cmd, out: File::NULL, err: File::NULL)
40
+ if result.nil?
41
+ STDERR.puts "ASR: #{executable} not found in PATH."
42
+ return
43
+ end
44
+ unless File.size(wav.path) > 0
45
+ STDERR.puts "ASR: #{executable} produced no output."
46
+ return
47
+ end
48
+
49
+ url = "#{asr_url}/v1/audio/transcriptions"
50
+ form_data = multipart_form_data(wav.path, model: asr_model, language:)
51
+ chat.request_url_response(:post, url, **form_data) do |response|
52
+ return JSON.parse(response.body)['text']
53
+ end
54
+ ensure
55
+ input.close!
56
+ wav&.close!
57
+ end
58
+ rescue JSON::ParserError, Excon::Error => e
59
+ if response = e.ask_and_send(:response)
60
+ status = response.status
61
+ result = JSON.parse(response.body) rescue nil
62
+ end
63
+ chat.log(:error, e, data: { status:, result: })
64
+ STDERR.puts "ASR transcription failed: #{e.message}"
65
+ nil
66
+ end
67
+
68
+ private
69
+
70
+ # Builds a multipart/form-data request body for the ASR endpoint.
71
+ #
72
+ # Constructs the wire format manually (boundary, text field parts,
73
+ # file part) so the result can be splatted directly into
74
+ # {HTTPHandling#request_url_response}.
75
+ #
76
+ # @param file_path [String] path to the WAV file to upload
77
+ # @param rest [Hash] additional form fields (e.g. `model:`,
78
+ # `language:`); `nil` values are omitted via `compact`
79
+ #
80
+ # @return [Hash] Excon-compatible options with `:headers`
81
+ # (Content-Type including the generated boundary), `:expects`
82
+ # (200), and `:body` (the assembled multipart string)
83
+ def multipart_form_data(file_path, **rest)
84
+ path = Pathname.new(file_path)
85
+ body = ''
86
+ boundary = SecureRandom.hex(8)
87
+
88
+ rest.compact.each do |field_name, field_value|
89
+ body << "--#{boundary}\r\n"
90
+ body << "Content-Disposition: form-data; name=\"#{field_name}\"\r\n\r\n"
91
+ body << "#{field_value}\r\n"
92
+ end
93
+
94
+ body << "--#{boundary}" << Excon::CR_NL
95
+ body << %{Content-Disposition: form-data; name="file"; filename="#{path.basename}"} << Excon::CR_NL
96
+ body << 'Content-Type: audio/wav' << Excon::CR_NL
97
+ body << Excon::CR_NL
98
+ body << path.binread
99
+ body << Excon::CR_NL
100
+ body << "--#{boundary}--" << Excon::CR_NL
101
+
102
+ rest.symbolize_keys_recursive.compact | {
103
+ headers: { 'Content-Type' => %{multipart/form-data; boundary="#{boundary}"} },
104
+ expects: 200,
105
+ body:,
106
+ }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -162,15 +162,14 @@ class OllamaChat::Chat
162
162
  # @return [OllamaChat::RedisCache, nil] the cache instance
163
163
  attr_reader :cache
164
164
 
165
-
166
165
  # Returns the voice handler class based on the configuration.
167
166
  #
168
167
  # This method resolves the voice handler class specified in the config.
169
- # If running within RSpec, it returns an empty array to avoid side effects.
168
+ # If running within RSpec, it returns nil to avoid side effects.
170
169
  #
171
- # @return [Class, Array] the voice handler class or an empty array if in test mode
170
+ # @return [Class, NilClass] the voice handler class or nil if in test mode
172
171
  def voice_handler
173
- defined?(RSpec) and return []
172
+ defined?(RSpec) and return nil
174
173
  case config.voice.handler
175
174
  when 'OllamaChat::TTS'
176
175
  OllamaChat::TTS
@@ -295,6 +294,30 @@ class OllamaChat::Chat
295
294
  content
296
295
  end
297
296
 
297
+ # Synthesizes and plays a short voice notification using the session's
298
+ # current voice.
299
+ #
300
+ # This is the chat-level facade for one-shot speech: it resolves the
301
+ # active voice, instantiates the configured voice handler, and delegates
302
+ # to its `#speak` method. It is a no-op when the voice switch is off,
303
+ # no voice is selected, or the handler lacks `#speak`.
304
+ #
305
+ # @param text [String] the text to speak
306
+ # @param background [Boolean, Integer] pass `true` for an immediate
307
+ # background thread, `false` for inline, or a numeric delay in
308
+ # seconds (e.g. `10`) to spawn a background thread that sleeps
309
+ # before speaking
310
+ #
311
+ # @return [OllamaChat::TTS, OllamaChat::Say, nil] the voice handler
312
+ # instance when a background thread was spawned (so callers can call
313
+ # `cancel_speaking`), or `nil` for inline playback or when voice is off
314
+ def speak(text, background: true)
315
+ voice.on? or return
316
+ voice = session.current_voice.full? or return
317
+ voice_handler.method_defined?(:speak) or return
318
+ voice_handler.new(chat: self, voice:).speak(text, background:)
319
+ end
320
+
298
321
  # Returns a human-readable string representation of the Chat object,
299
322
  # including the class name and the base URL.
300
323
  #
@@ -433,27 +433,30 @@ module OllamaChat::Commands
433
433
 
434
434
  command(
435
435
  name: :prompt,
436
- regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|rename|sync|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
437
- complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset rename sync ] ],
436
+ regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset count|reset|rename|sync|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
437
+ complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset\ count reset rename sync ] ],
438
438
  optional: true,
439
439
  options: '[-c CONTEXT|-e|-f]',
440
440
  help: <<~EOT,
441
441
  📝 Manage prompt templates:
442
442
  Subcommands: edit, info, add, delete, list,
443
- duplicate, import, export, reset, rename,
444
- sync.
443
+ duplicate, import, export, reset count, reset,
444
+ rename, sync.
445
445
  Options: -c [context]
446
446
  (? for interactive in /prompt),
447
447
  -e (edit next)
448
448
  EOT
449
449
  ) do |subcommand, opts, filename|
450
450
  opts = go_command('fc:', opts)
451
- context = if subcommand.nil? || subcommand == '-e'
451
+ context = case subcommand
452
+ when nil, '-e'
452
453
  if opts[?c] == ??
453
454
  choose_prompt_context
454
455
  else
455
456
  opts[?c] || 'prompt'
456
457
  end
458
+ when 'reset count'
459
+ 'prompt'
457
460
  else
458
461
  opts[?c] || choose_prompt_context
459
462
  end
@@ -481,6 +484,8 @@ module OllamaChat::Commands
481
484
  export_prompt(context:)
482
485
  when 'info'
483
486
  info_prompt(context:)
487
+ when 'reset count'
488
+ choose_and_reset_count(context:)
484
489
  when 'reset'
485
490
  if prompt = choose_prompt(
486
491
  default: true,
@@ -498,8 +503,9 @@ module OllamaChat::Commands
498
503
  prompt_sync(context:)
499
504
  when nil, '-e'
500
505
  if prompt = choose_prompt(
501
- prompt: 'Which template shall guide the next response? %s',
502
- context:
506
+ prompt: 'Which template shall guide the next response? %s',
507
+ context: ,
508
+ count: true
503
509
  ).full?(&:to_s)
504
510
  if subcommand
505
511
  prompt = edit_text(prompt)
@@ -820,38 +826,43 @@ module OllamaChat::Commands
820
826
 
821
827
  command(
822
828
  name: :input,
823
- regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[aprem]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
829
+ regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[apreim]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*|l\s*[-\w]+))*)(?:\s+([^-].*))?$),
824
830
  optional: true,
825
831
  complete: [ 'input', %w[ path context embedding summary ] ],
826
832
  options: <<~EOT,
827
833
  [
828
- -w|-a|-p|-e|-m|
834
+ -w|-a|-p|-e|-m|-i|
829
835
  -c <collection>|
830
- -t <tags>
836
+ -t <tags>|
837
+ -l <language>
831
838
  ]
832
839
  [arg…]
833
840
  EOT
834
841
  help: <<~EOT
835
842
  📥 Import content (read, summarize, embed, context)
836
843
  Subcommands: path, context, embedding, summary
837
- Options: -p (pattern), -w [words], -a (all),
838
- -c [collection], -t [tags], -e (edit),
839
- -m (monochrome, strip ANSI from file content)
844
+ Options: -p (pattern), -w [words], -a (all),
845
+ -c [collection], -t [tags], -e (edit),
846
+ -m (monochrome), -i (summary instruction),
847
+ -l [language]
840
848
  EOT
841
849
  ) do |input_mode,opts,arg|
842
850
  disable_content_parsing
843
851
  case input_mode
844
852
  when 'summary'
845
- opts = go_command('paw:', opts)
853
+ opts = go_command('paw:i', opts)
854
+ instruction = if opts[?i]
855
+ ask?(prompt: 'Summary instruction (Enter to skip): ').full?(:strip)
856
+ end
846
857
  if opts[?p]
847
858
  words = opts.fetch(?w, 100)
848
859
  all = opts.fetch(?a, false)
849
860
  patterns = extract_patterns(arg)
850
- next provide_file_set_content(patterns, all:, skip_blank: true) { summarize(_1, words:) } || :next
861
+ next provide_file_set_content(patterns, all:, skip_blank: true) { summarize(_1, words:, instruction:) } || :next
851
862
  elsif arg
852
863
  words = opts.fetch(?w, 100)
853
864
  source = arg
854
- next summarize(source, words:) || :next
865
+ next summarize(source, words:, instruction:) || :next
855
866
  else
856
867
  STDERR.puts "Need a source to summarize for input!"
857
868
  next :next
@@ -915,17 +926,17 @@ module OllamaChat::Commands
915
926
  next :next
916
927
  end
917
928
  else
918
- opts = go_command('paem', opts)
929
+ opts = go_command('paeml:', opts)
919
930
  if opts[?p]
920
931
  all = opts.fetch(?a, false)
921
932
  patterns = extract_patterns(arg)
922
933
  next provide_file_set_content(patterns, all:, skip_blank: true) do |src|
923
- content = import(src)
934
+ content = import(src, language: opts[?l])
924
935
  opts[?m] ? OllamaChat::Utils::StripANSI.strip_ansi(content) : content
925
936
  end || :next
926
937
  elsif arg
927
938
  source = arg
928
- content = import(source) or next :next
939
+ content = import(source, language: opts[?l]) or next :next
929
940
  content = OllamaChat::Utils::StripANSI.strip_ansi(content) if opts[?m]
930
941
  content = edit_text(content) if opts[?e]
931
942
  content
@@ -89,7 +89,7 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
89
89
  # @return [String, nil] A JSON-serialized string containing default settings for tools.
90
90
  #
91
91
  # @!attribute [v] think_mode
92
- # @return [String] The current thinking mode (must be one of `THINK_MODE_STATES`).
92
+ # @return [String] The current thinking mode (must be one of `Chat#think_mode_states`).
93
93
  #
94
94
  # @!attribute [v] think_loud_enabled
95
95
  # @return [Boolean] Whether thinking annotations are displayed in the output.
@@ -19,10 +19,13 @@ module OllamaChat::Dialog
19
19
  old_pre_input_hook = Reline.pre_input_hook
20
20
  Reline.pre_input_hook = -> { Reline.insert_text prefill.to_s }
21
21
  end
22
+ speaker = speak(prompt, background: 10)
22
23
  Reline.readline(prompt, true)&.chomp
24
+
23
25
  rescue Interrupt
24
26
  return nil
25
27
  ensure
28
+ speaker&.cancel_speaking
26
29
  prefill and Reline.pre_input_hook = old_pre_input_hook
27
30
  end
28
31
 
@@ -46,6 +49,7 @@ module OllamaChat::Dialog
46
49
  if prompt.include?('%s')
47
50
  prompt = prompt % (timeout ? ('timeout in %us' % timeout) : 'no timeout')
48
51
  end
52
+ speaker = speak(prompt, background: 10) if timeout.nil?
49
53
  output.print prompt
50
54
  min = 1
51
55
  time = 0
@@ -86,6 +90,8 @@ module OllamaChat::Dialog
86
90
  end
87
91
  nil
88
92
  end
93
+ ensure
94
+ speaker&.cancel_speaking
89
95
  end
90
96
 
91
97
  private
@@ -72,4 +72,29 @@ module OllamaChat::HTTPHandling
72
72
  &block
73
73
  )
74
74
  end
75
+
76
+ # Performs an HTTP request with configuration-aware options.
77
+ #
78
+ # Merges the caller-supplied options (body, headers, etc.) with the
79
+ # HTTP options generated by `http_options` (SSL verification, proxy)
80
+ # and sends the request via Excon.
81
+ #
82
+ # @param method [Symbol] the HTTP method (`:get` or `:post`).
83
+ # @param url [String] the target URL.
84
+ # @param options [Hash] request options (e.g. `:body`, `:headers`).
85
+ # @yield [response] yields the Excon response object when a block is
86
+ # given.
87
+ # @return [Excon::Response, Object] the response object, or the block's
88
+ # return value if a block was provided.
89
+ def request_url_response(method, url, **options, &block)
90
+ normalized_url = OllamaChat::Utils::Fetcher.normalize_url(url)
91
+ options = http_options(normalized_url) | options
92
+ excon = Excon.new(url, options)
93
+ response = excon.request(method:)
94
+ if block
95
+ block.(response)
96
+ else
97
+ response
98
+ end
99
+ end
75
100
  end
@@ -97,8 +97,14 @@ module OllamaChat::Information
97
97
  # (defaults to STDOUT).
98
98
  def info_model(output: STDOUT)
99
99
  output.puts "🧠 Current chat model is #{bold{@model}}."
100
- output.puts " Capabilities: #{Array(@model_metadata&.capabilities) * ', '}"
101
100
  output.puts " Families: #{Array(@model_metadata&.families) * ', '}"
101
+ output.puts " Capabilities: #{Array(@model_metadata&.capabilities) * ', '}"
102
+ if @model_metadata&.thinking
103
+ thinking = @model_metadata.thinking
104
+ levels = Array(thinking.values).map { normalize_think_state(_1) }
105
+ default = normalize_think_state(thinking.default)
106
+ output.puts " Thinking: #{levels * ', '} (default: #{bold{default}})"
107
+ end
102
108
 
103
109
  profiles = models::ModelOptions.where(model_name: @model).order(:profile).all
104
110
  if profiles.full?