Commit bae8637
Changed files (1)
lib
elelem
lib/elelem/ollama.rb
@@ -3,169 +3,46 @@
module Net
module Llm
class Ollama
- attr_reader :host, :model, :http
-
- def initialize(host: ENV.fetch("OLLAMA_HOST", "localhost:11434"), model: "gpt-oss", http: Net::Llm.http)
- @host = host
- @model = model
- @http = http
- end
-
- def chat(messages, tools = [], &block)
- payload = { model: model, messages: messages, stream: block_given? }
- payload[:tools] = tools unless tools.empty?
-
- execute(build_url("/api/chat"), payload, &block)
+ def initialize(model:, host: ENV.fetch("OLLAMA_HOST", "localhost:11434"), http: Net::Llm.http)
+ @url = "#{host.start_with?('http') ? host : "http://#{host}"}/api/chat"
+ @model, @http = model, http
end
def fetch(messages, tools = [], &block)
- content = ""
- thinking = ""
- tool_calls = []
-
- if block_given?
- chat(messages, tools) do |chunk|
- msg = chunk["message"] || {}
- delta_content = msg["content"]
- delta_thinking = msg["thinking"]
+ content, thinking, tool_calls = "", "", []
- content += delta_content if delta_content
- thinking += delta_thinking if delta_thinking
- tool_calls += normalize_tool_calls(msg["tool_calls"]) if msg["tool_calls"]
+ stream({ model: @model, messages:, tools:, stream: true }) do |json|
+ msg = json["message"] || {}
+ content += msg["content"].to_s
+ thinking += msg["thinking"].to_s
+ tool_calls.concat(parse_tools(msg["tool_calls"])) if msg["tool_calls"]
- if chunk["done"]
- block.call({
- type: :complete,
- content: content,
- thinking: thinking.empty? ? nil : thinking,
- tool_calls: tool_calls,
- stop_reason: map_stop_reason(chunk["done_reason"])
- })
- else
- block.call({
- type: :delta,
- content: delta_content,
- thinking: delta_thinking,
- tool_calls: nil
- })
- end
- end
- else
- result = chat(messages, tools)
- msg = result["message"] || {}
- {
- type: :complete,
- content: msg["content"],
- thinking: msg["thinking"],
- tool_calls: normalize_tool_calls(msg["tool_calls"]),
- stop_reason: map_stop_reason(result["done_reason"])
- }
+ block.call(json["done"] ?
+ { type: :complete, content:, thinking: nilify(thinking), tool_calls: } :
+ { type: :delta, content: msg["content"], thinking: msg["thinking"], tool_calls: nil })
end
end
- def generate(prompt, &block)
- execute(build_url("/api/generate"), {
- model: model,
- prompt: prompt,
- stream: block_given?
- }, &block)
- end
-
- def embeddings(input)
- post_request(build_url("/api/embed"), { model: model, input: input })
- end
-
- def tags
- get_request(build_url("/api/tags"))
- end
-
- def show(name)
- post_request(build_url("/api/show"), { name: name })
- end
-
private
- def execute(url, payload, &block)
- if block_given?
- stream_request(url, payload, &block)
- else
- post_request(url, payload)
- end
- end
-
- def build_url(path)
- base = host.start_with?("http://", "https://") ? host : "http://#{host}"
- "#{base}#{path}"
- end
-
- def get_request(url)
- handle_response(http.get(url))
- end
-
- def post_request(url, payload)
- handle_response(http.post(url, body: payload))
- end
-
- def handle_response(response)
- if response.is_a?(Net::HTTPSuccess)
- JSON.parse(response.body)
- else
- {
- "code" => response.code,
- "body" => response.body
- }
- end
- end
-
- def stream_request(url, payload, &block)
- http.post(url, body: payload) do |response|
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-
- buffer = ""
- response.read_body do |chunk|
- buffer += chunk
-
- while (message = extract_message(buffer))
- next if message.empty?
-
- json = JSON.parse(message)
- block.call(json)
-
- break if json["done"]
+ def stream(body, &block)
+ @http.post(@url, body:) do |res|
+ raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
+ buf = ""
+ res.read_body do |chunk|
+ buf += chunk
+ while (i = buf.index("\n"))
+ block.call(JSON.parse(buf.slice!(0, i + 1)))
end
end
end
end
- def extract_message(buffer)
- message_end = buffer.index("\n")
- return nil unless message_end
-
- message = buffer[0...message_end]
- buffer.replace(buffer[(message_end + 1)..-1] || "")
- message
+ def parse_tools(tcs)
+ tcs.map { |tc| { id: tc["id"], name: tc.dig("function", "name"), arguments: tc.dig("function", "arguments") || {} } }
end
- def normalize_tool_calls(tool_calls)
- return [] if tool_calls.nil? || tool_calls.empty?
-
- tool_calls.map do |tc|
- {
- id: tc["id"] || tc.dig("function", "id"),
- name: tc.dig("function", "name"),
- arguments: tc.dig("function", "arguments") || {}
- }
- end
- end
-
- def map_stop_reason(reason)
- case reason
- when "stop" then :end_turn
- when "tool_calls", "tool_use" then :tool_use
- when "length" then :max_tokens
- else :end_turn
- end
- end
+ def nilify(s) = s.empty? ? nil : s
end
end
end