ollama-ruby 0.0.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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/LICENSE +19 -0
  4. data/README.md +430 -0
  5. data/Rakefile +35 -0
  6. data/bin/ollama_chat +258 -0
  7. data/bin/ollama_console +20 -0
  8. data/lib/ollama/client/command.rb +25 -0
  9. data/lib/ollama/client/doc.rb +26 -0
  10. data/lib/ollama/client.rb +137 -0
  11. data/lib/ollama/commands/chat.rb +21 -0
  12. data/lib/ollama/commands/copy.rb +19 -0
  13. data/lib/ollama/commands/create.rb +20 -0
  14. data/lib/ollama/commands/delete.rb +19 -0
  15. data/lib/ollama/commands/embed.rb +21 -0
  16. data/lib/ollama/commands/embeddings.rb +20 -0
  17. data/lib/ollama/commands/generate.rb +21 -0
  18. data/lib/ollama/commands/ps.rb +19 -0
  19. data/lib/ollama/commands/pull.rb +19 -0
  20. data/lib/ollama/commands/push.rb +19 -0
  21. data/lib/ollama/commands/show.rb +20 -0
  22. data/lib/ollama/commands/tags.rb +19 -0
  23. data/lib/ollama/dto.rb +42 -0
  24. data/lib/ollama/errors.rb +15 -0
  25. data/lib/ollama/handlers/collector.rb +17 -0
  26. data/lib/ollama/handlers/concern.rb +31 -0
  27. data/lib/ollama/handlers/dump_json.rb +8 -0
  28. data/lib/ollama/handlers/dump_yaml.rb +8 -0
  29. data/lib/ollama/handlers/markdown.rb +22 -0
  30. data/lib/ollama/handlers/nop.rb +7 -0
  31. data/lib/ollama/handlers/print.rb +16 -0
  32. data/lib/ollama/handlers/progress.rb +36 -0
  33. data/lib/ollama/handlers/say.rb +19 -0
  34. data/lib/ollama/handlers/single.rb +17 -0
  35. data/lib/ollama/handlers.rb +13 -0
  36. data/lib/ollama/image.rb +31 -0
  37. data/lib/ollama/message.rb +9 -0
  38. data/lib/ollama/options.rb +68 -0
  39. data/lib/ollama/response.rb +5 -0
  40. data/lib/ollama/tool/function/parameters/property.rb +9 -0
  41. data/lib/ollama/tool/function/parameters.rb +10 -0
  42. data/lib/ollama/tool/function.rb +11 -0
  43. data/lib/ollama/tool.rb +9 -0
  44. data/lib/ollama/utils/ansi_markdown.rb +217 -0
  45. data/lib/ollama/utils/width.rb +22 -0
  46. data/lib/ollama/version.rb +8 -0
  47. data/lib/ollama.rb +43 -0
  48. data/ollama-ruby.gemspec +36 -0
  49. data/spec/assets/kitten.jpg +0 -0
  50. data/spec/ollama/client/doc_spec.rb +11 -0
  51. data/spec/ollama/client_spec.rb +144 -0
  52. data/spec/ollama/commands/chat_spec.rb +52 -0
  53. data/spec/ollama/commands/copy_spec.rb +28 -0
  54. data/spec/ollama/commands/create_spec.rb +37 -0
  55. data/spec/ollama/commands/delete_spec.rb +28 -0
  56. data/spec/ollama/commands/embed_spec.rb +52 -0
  57. data/spec/ollama/commands/embeddings_spec.rb +38 -0
  58. data/spec/ollama/commands/generate_spec.rb +29 -0
  59. data/spec/ollama/commands/ps_spec.rb +25 -0
  60. data/spec/ollama/commands/pull_spec.rb +28 -0
  61. data/spec/ollama/commands/push_spec.rb +28 -0
  62. data/spec/ollama/commands/show_spec.rb +28 -0
  63. data/spec/ollama/commands/tags_spec.rb +22 -0
  64. data/spec/ollama/handlers/collector_spec.rb +15 -0
  65. data/spec/ollama/handlers/dump_json_spec.rb +16 -0
  66. data/spec/ollama/handlers/dump_yaml_spec.rb +18 -0
  67. data/spec/ollama/handlers/markdown_spec.rb +46 -0
  68. data/spec/ollama/handlers/nop_spec.rb +15 -0
  69. data/spec/ollama/handlers/print_spec.rb +30 -0
  70. data/spec/ollama/handlers/progress_spec.rb +22 -0
  71. data/spec/ollama/handlers/say_spec.rb +30 -0
  72. data/spec/ollama/handlers/single_spec.rb +24 -0
  73. data/spec/ollama/image_spec.rb +23 -0
  74. data/spec/ollama/message_spec.rb +37 -0
  75. data/spec/ollama/options_spec.rb +25 -0
  76. data/spec/ollama/tool_spec.rb +78 -0
  77. data/spec/ollama/utils/ansi_markdown_spec.rb +15 -0
  78. data/spec/spec_helper.rb +16 -0
  79. metadata +321 -0
@@ -0,0 +1,20 @@
1
+ class Ollama::Commands::Show
2
+ include Ollama::DTO
3
+
4
+ def self.path
5
+ '/api/show'
6
+ end
7
+
8
+ def initialize(name:, verbose: nil)
9
+ @name, @verbose = name, verbose
10
+ @stream = false
11
+ end
12
+
13
+ attr_reader :name, :verbose, :stream
14
+
15
+ attr_writer :client
16
+
17
+ def perform(handler)
18
+ @client.request(method: :post, path: self.class.path, body: to_json, stream:, handler:)
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ class Ollama::Commands::Tags
2
+ def self.path
3
+ '/api/tags'
4
+ end
5
+
6
+ def initialize(**parameters)
7
+ parameters.empty? or raise ArgumentError,
8
+ "Invalid parameters: #{parameters.keys * ' '}"
9
+ @stream = false
10
+ end
11
+
12
+ attr_reader :stream
13
+
14
+ attr_writer :client
15
+
16
+ def perform(handler)
17
+ @client.request(method: :get, path: self.class.path, stream:, handler:)
18
+ end
19
+ end
data/lib/ollama/dto.rb ADDED
@@ -0,0 +1,42 @@
1
+ module Ollama::DTO
2
+ extend Tins::Concern
3
+
4
+ included do
5
+ self.attributes = Set.new
6
+ end
7
+
8
+ module ClassMethods
9
+ attr_accessor :attributes
10
+
11
+ def json_create(object)
12
+ new(**object.transform_keys(&:to_sym))
13
+ end
14
+
15
+ def attr_reader(*names)
16
+ super
17
+ attributes.merge(names.map(&:to_sym))
18
+ end
19
+ end
20
+
21
+ def as_array_of_hashes(obj)
22
+ if obj.respond_to?(:to_hash)
23
+ [ obj.to_hash ]
24
+ elsif obj.respond_to?(:to_ary)
25
+ obj.to_ary.map(&:to_hash)
26
+ end
27
+ end
28
+
29
+ def as_json(*)
30
+ {
31
+ json_class: self.class.name
32
+ }.merge(
33
+ self.class.attributes.each_with_object({}) { |a, h| h[a] = send(a) }
34
+ ).reject { _2.nil? || _2.ask_and_send(:size) == 0 }
35
+ end
36
+
37
+ alias to_hash as_json
38
+
39
+ def to_json(*)
40
+ as_json.to_json(*)
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ module Ollama
2
+ module Errors
3
+ class Error < StandardError
4
+ end
5
+
6
+ class NotFoundError < Error
7
+ end
8
+
9
+ class TimeoutError < Error
10
+ end
11
+
12
+ class SocketError < Error
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ class Ollama::Handlers::Collector
2
+ include Ollama::Handlers::Concern
3
+
4
+ def initialize(output: $stdout)
5
+ super
6
+ @array = []
7
+ end
8
+
9
+ def call(response)
10
+ @array << response
11
+ self
12
+ end
13
+
14
+ def result
15
+ @array
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ require 'tins/concern'
2
+ require 'tins/implement'
3
+
4
+ module Ollama::Handlers::Concern
5
+ extend Tins::Concern
6
+ extend Tins::Implement
7
+
8
+ def initialize(output: $stdout)
9
+ @output = output
10
+ end
11
+
12
+ attr_reader :output
13
+
14
+ attr_reader :result
15
+
16
+ implement :call
17
+
18
+ def to_proc
19
+ -> response { call(response) }
20
+ end
21
+
22
+ module ClassMethods
23
+ def call(response)
24
+ new.call(response)
25
+ end
26
+
27
+ def to_proc
28
+ new.to_proc
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ class Ollama::Handlers::DumpJSON
2
+ include Ollama::Handlers::Concern
3
+
4
+ def call(response)
5
+ @output.puts JSON::pretty_generate(response, allow_nan: true, max_nesting: false)
6
+ self
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class Ollama::Handlers::DumpYAML
2
+ include Ollama::Handlers::Concern
3
+
4
+ def call(response)
5
+ @output.puts Psych.dump(response)
6
+ self
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ require 'term/ansicolor'
2
+
3
+ class Ollama::Handlers::Markdown
4
+ include Ollama::Handlers::Concern
5
+ include Term::ANSIColor
6
+
7
+ def initialize(output: $stdout)
8
+ super
9
+ @output.sync = true
10
+ @content = ''
11
+ end
12
+
13
+ def call(response)
14
+ if content = response.response || response.message&.content
15
+ @content << content
16
+ markdown_content = Ollama::Utils::ANSIMarkdown.parse(@content)
17
+ @output.print clear_screen, move_home, markdown_content
18
+ end
19
+ response.done and @output.puts
20
+ self
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ class Ollama::Handlers::NOP
2
+ include Ollama::Handlers::Concern
3
+
4
+ def call(response)
5
+ self
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ class Ollama::Handlers::Print
2
+ include Ollama::Handlers::Concern
3
+
4
+ def initialize(output: $stdout)
5
+ super
6
+ @output.sync = true
7
+ end
8
+
9
+ def call(response)
10
+ if content = response.response || response.message&.content
11
+ @output.print content
12
+ end
13
+ response.done and @output.puts
14
+ self
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ require 'infobar'
2
+
3
+ class Ollama::Handlers::Progress
4
+ include Ollama::Handlers::Concern
5
+ include Term::ANSIColor
6
+
7
+ def initialize(output: $stdout)
8
+ super
9
+ @current = 0
10
+ @total = nil
11
+ @last_status = nil
12
+ end
13
+
14
+ def call(response)
15
+ infobar.display.output = @output
16
+ status = response.status
17
+ if response.total && response.completed
18
+ if !@last_status or @last_status != status
19
+ @last_status and infobar.newline
20
+ @last_status = status
21
+ @current = 0
22
+ @total = response.total
23
+ infobar.counter.reset(total: @total, current: @current)
24
+ end
25
+ infobar.counter.progress(by: response.completed - @current)
26
+ @current = response.completed
27
+ end
28
+ if status
29
+ infobar.label = status
30
+ infobar.update(message: '%l %c/%t in %te, ETA %e @%E', force: true)
31
+ elsif error = response.error
32
+ infobar.puts bold { "Error: " } + red { error }
33
+ end
34
+ self
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ require 'shellwords'
2
+
3
+ class Ollama::Handlers::Say
4
+ include Ollama::Handlers::Concern
5
+
6
+ def initialize(output: nil, voice: 'Samantha')
7
+ output ||= IO.popen(Shellwords.join([ 'say', '-v', voice ]), 'w')
8
+ super(output:)
9
+ @output.sync = true
10
+ end
11
+
12
+ def call(response)
13
+ if content = response.response || response.message&.content
14
+ @output.print content
15
+ end
16
+ response.done and @output.close
17
+ self
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ class Ollama::Handlers::Single
2
+ include Ollama::Handlers::Concern
3
+
4
+ def initialize(output: $stdout)
5
+ super
6
+ @array = []
7
+ end
8
+
9
+ def call(response)
10
+ @array << response
11
+ self
12
+ end
13
+
14
+ def result
15
+ @array.size <= 1 ? @array.first : @array
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Ollama::Handlers
2
+ end
3
+
4
+ require 'ollama/handlers/concern'
5
+ require 'ollama/handlers/collector'
6
+ require 'ollama/handlers/nop'
7
+ require 'ollama/handlers/single'
8
+ require 'ollama/handlers/markdown'
9
+ require 'ollama/handlers/progress'
10
+ require 'ollama/handlers/print'
11
+ require 'ollama/handlers/dump_json'
12
+ require 'ollama/handlers/dump_yaml'
13
+ require 'ollama/handlers/say'
@@ -0,0 +1,31 @@
1
+ require 'base64'
2
+
3
+ class Ollama::Image
4
+ def initialize(data)
5
+ @data = data
6
+ end
7
+
8
+ class << self
9
+ def for_base64(data)
10
+ new(data)
11
+ end
12
+
13
+ def for_string(string)
14
+ for_base64(Base64.encode64(string))
15
+ end
16
+
17
+ def for_io(io)
18
+ for_string(io.read)
19
+ end
20
+
21
+ def for_filename(path)
22
+ File.open(path, 'rb') { |io| for_io(io) }
23
+ end
24
+
25
+ private :new
26
+ end
27
+
28
+ def to_s
29
+ @data
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ class Ollama::Message
2
+ include Ollama::DTO
3
+
4
+ attr_reader :role, :content, :images
5
+
6
+ def initialize(role:, content:, images: nil, **)
7
+ @role, @content, @images = role, content, (Array(images) if images)
8
+ end
9
+ end
@@ -0,0 +1,68 @@
1
+ # Options are explained in the parameters for the modelfile:
2
+ # https://github.com/ollama/ollama/blob/main/docs/modelfile.md#parameter
3
+ class Ollama::Options
4
+ include Ollama::DTO
5
+
6
+ @@types = {
7
+ numa: [ false, true ],
8
+ num_ctx: Integer,
9
+ num_batch: Integer,
10
+ num_gpu: Integer,
11
+ main_gpu: Integer,
12
+ low_vram: [ false, true ],
13
+ f16_kv: [ false, true ],
14
+ logits_all: [ false, true ],
15
+ vocab_only: [ false, true ],
16
+ use_mmap: [ false, true ],
17
+ use_mlock: [ false, true ],
18
+ num_thread: Integer,
19
+ num_keep: Integer,
20
+ seed: Integer,
21
+ num_predict: Integer,
22
+ top_k: Integer,
23
+ top_p: Float,
24
+ min_p: Float,
25
+ tfs_z: Float,
26
+ typical_p: Float,
27
+ repeat_last_n: Integer,
28
+ temperature: Float,
29
+ repeat_penalty: Float,
30
+ presence_penalty: Float,
31
+ frequency_penalty: Float,
32
+ mirostat: Integer,
33
+ mirostat_tau: Float,
34
+ mirostat_eta: Float,
35
+ penalize_newline: [ false, true ],
36
+ stop: Array,
37
+ }
38
+
39
+ @@types.each do |name, type|
40
+ attr_reader name
41
+
42
+ define_method("#{name}=") do |value|
43
+ instance_variable_set(
44
+ "@#{name}",
45
+ if value.nil?
46
+ nil
47
+ else
48
+ case type
49
+ when Class
50
+ send(type.name, value)
51
+ when Array
52
+ if type.include?(value)
53
+ value
54
+ else
55
+ raise TypeError, "#{value} not in #{type * ?|}"
56
+ end
57
+ end
58
+ end
59
+ )
60
+ end
61
+ end
62
+
63
+ class_eval %{
64
+ def initialize(#{@@types.keys.map { "#{_1}: nil" }.join(', ') + ', **'})
65
+ #{@@types.keys.map { "self.#{_1} = #{_1}" }.join(?\n)}
66
+ end
67
+ }
68
+ end
@@ -0,0 +1,5 @@
1
+ class Ollama::Response < JSON::GenericObject
2
+ def as_json(*)
3
+ to_hash
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Ollama::Tool::Function::Parameters::Property
2
+ include Ollama::DTO
3
+
4
+ attr_reader :type, :description, :enum
5
+
6
+ def initialize(type:, description:, enum: nil)
7
+ @type, @description, @enum = type, description, Array(enum)
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class Ollama::Tool::Function::Parameters
2
+ include Ollama::DTO
3
+
4
+ attr_reader :type, :properties, :required
5
+
6
+ def initialize(type:, properties:, required:)
7
+ @type, @properties, @required =
8
+ type, Hash(properties).transform_values(&:to_hash), Array(required)
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class Ollama::Tool::Function
2
+ include Ollama::DTO
3
+
4
+ attr_reader :name, :description, :parameters, :required
5
+
6
+ def initialize(name:, description:, parameters: nil, required: nil)
7
+ @name, @description, @parameters, @required =
8
+ name, description, (Hash(parameters) if parameters),
9
+ (Array(required) if required)
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class Ollama::Tool
2
+ include Ollama::DTO
3
+
4
+ attr_reader :type, :function
5
+
6
+ def initialize(type:, function:)
7
+ @type, @function = type, function.to_hash
8
+ end
9
+ end
@@ -0,0 +1,217 @@
1
+ require 'kramdown'
2
+ require 'kramdown-parser-gfm'
3
+ require 'terminal-table'
4
+
5
+ class Ollama::Utils::ANSIMarkdown < Kramdown::Converter::Base
6
+ include Term::ANSIColor
7
+ include Ollama::Utils::Width
8
+
9
+ class ::Kramdown::Parser::Mygfm < ::Kramdown::Parser::GFM
10
+ def initialize(source, options)
11
+ options[:gfm_quirks] << :no_auto_typographic
12
+ super
13
+ @block_parsers -= %i[
14
+ definition_list block_html block_math
15
+ footnote_definition abbrev_definition
16
+ ]
17
+ @span_parsers -= %i[ footnote_marker inline_math ]
18
+ end
19
+ end
20
+
21
+ def self.parse(source)
22
+ @doc = Kramdown::Document.new(
23
+ source, input: :mygfm, auto_ids: false, entity_output: :as_char
24
+ ).to_ansi
25
+ end
26
+
27
+ def initialize(root, options)
28
+ super
29
+ end
30
+
31
+ def convert(el, opts = {})
32
+ send("convert_#{el.type}", el, opts)
33
+ end
34
+
35
+ def inner(el, opts, &block)
36
+ result = +''
37
+ options = opts.dup.merge(parent: el)
38
+ el.children.each_with_index do |inner_el, index|
39
+ options[:index] = index
40
+ options[:result] = result
41
+ begin
42
+ content = send("convert_#{inner_el.type}", inner_el, options)
43
+ result << (block&.(inner_el, index, content) || content)
44
+ rescue NameError => e
45
+ warning "Caught #{e.class} for #{inner_el.type}"
46
+ end
47
+ end
48
+ result
49
+ end
50
+
51
+ def convert_root(el, opts)
52
+ inner(el, opts)
53
+ end
54
+
55
+ def convert_blank(_el, opts)
56
+ opts[:result] =~ /\n\n\Z|\A\Z/ ? "" : "\n"
57
+ end
58
+
59
+ def convert_text(el, _opts)
60
+ el.value
61
+ end
62
+
63
+ def convert_header(el, opts)
64
+ newline bold { underline { inner(el, opts) } }
65
+ end
66
+
67
+ def convert_p(el, opts)
68
+ length = width(percentage: 90) - opts[:list_indent].to_i
69
+ length < 0 and return ''
70
+ newline wrap(inner(el, opts), length:)
71
+ end
72
+
73
+ def convert_strong(el, opts)
74
+ bold { inner(el, opts) }
75
+ end
76
+
77
+ def convert_em(el, opts)
78
+ italic { inner(el, opts) }
79
+ end
80
+
81
+ def convert_a(el, opts)
82
+ url = el.attr['href']
83
+ hyperlink(url) { inner(el, opts) }
84
+ end
85
+
86
+ def convert_codespan(el, _opts)
87
+ blue { el.value }
88
+ end
89
+
90
+ def convert_codeblock(el, _opts)
91
+ blue { el.value }
92
+ end
93
+
94
+ def convert_blockquote(el, opts)
95
+ newline ?“ + inner(el, opts).sub(/\n+\z/, '') + ?”
96
+ end
97
+
98
+ def convert_hr(_el, _opts)
99
+ newline ?─ * width(percentage: 100)
100
+ end
101
+
102
+ def convert_img(el, _opts)
103
+ url = el.attr['src']
104
+ alt = el.attr['alt']
105
+ alt.strip.size == 0 and alt = url
106
+ alt = '🖼 ' + alt
107
+ hyperlink(url) { alt }
108
+ end
109
+
110
+ def convert_ul(el, opts)
111
+ list_indent = opts[:list_indent].to_i
112
+ inner(el, opts) { |_inner_el, index, content|
113
+ result = '· %s' % content
114
+ result = newline(result, count: index <= el.children.size - 1 ? 1 : 2)
115
+ result.gsub(/^/, ' ' * list_indent)
116
+ }
117
+ end
118
+
119
+ def convert_ol(el, opts)
120
+ list_indent = opts[:list_indent].to_i
121
+ inner(el, opts) { |_inner_el, index, content|
122
+ result = '%u. %s' % [ index + 1, content ]
123
+ result = newline(result, count: index <= el.children.size - 1 ? 1 : 2)
124
+ result.gsub(/^/, ' ' * list_indent)
125
+ }
126
+ end
127
+
128
+ def convert_li(el, opts)
129
+ opts = opts.dup
130
+ opts[:list_indent] = 2 + opts[:list_indent].to_i
131
+ newline inner(el, opts).sub(/\n+\Z/, '')
132
+ end
133
+
134
+ def convert_html_element(el, opts)
135
+ if el.value == 'i' || el.value == 'em'
136
+ italic { inner(el, opts) }
137
+ elsif el.value == 'b' || el.value == 'strong'
138
+ bold { inner(el, opts) }
139
+ else
140
+ ''
141
+ end
142
+ end
143
+
144
+ def convert_table(el, opts)
145
+ table = Terminal::Table.new
146
+ table.style = {
147
+ all_separators: true,
148
+ border: :unicode_round,
149
+ }
150
+ opts[:table] = table
151
+ inner(el, opts)
152
+ el.options[:alignment].each_with_index do |a, i|
153
+ a == :default and next
154
+ opts[:table].align_column(i, a)
155
+ end
156
+ newline table.to_s
157
+ end
158
+
159
+ def convert_thead(el, opts)
160
+ rows = inner(el, opts)
161
+ rows = rows.split(/\s*\|\s*/)[1..].map(&:strip)
162
+ opts[:table].headings = rows
163
+ ''
164
+ end
165
+
166
+ def convert_tbody(el, opts)
167
+ res = +''
168
+ res << inner(el, opts)
169
+ end
170
+
171
+ def convert_tfoot(el, opts)
172
+ ''
173
+ end
174
+
175
+ def convert_tr(el, opts)
176
+ return '' if el.children.empty?
177
+ full_width = width(percentage: 90)
178
+ cols = el.children.map { |c| convert(c, opts).strip }
179
+ row_size = cols.sum(&:size)
180
+ return '' if row_size.zero?
181
+ opts[:table] << cols.map { |c|
182
+ length = (full_width * (c.size / row_size.to_f)).floor
183
+ wrap(c, length:)
184
+ }
185
+ ''
186
+ end
187
+
188
+ def convert_td(el, opts)
189
+ inner(el, opts)
190
+ end
191
+
192
+ def convert_entity(el, _opts)
193
+ el.value.char
194
+ end
195
+
196
+ def convert_xml_comment(*)
197
+ ''
198
+ end
199
+
200
+ def convert_xml_pi(*)
201
+ ''
202
+ end
203
+
204
+ def convert_br(_el, opts)
205
+ ''
206
+ end
207
+
208
+ def convert_smart_quote(el, _opts)
209
+ el.value.to_s =~ /[rl]dquo/ ? "\"" : "'"
210
+ end
211
+
212
+ def newline(text, count: 1)
213
+ text.gsub(/\n*\z/, ?\n * count)
214
+ end
215
+ end
216
+
217
+ Kramdown::Converter.const_set(:Ansi, Ollama::Utils::ANSIMarkdown)