ollama-ruby 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)