Class: Ask::Providers::Ollama

Inherits:
Ask::Provider
  • Object
show all
Includes:
LLM::ProviderConfig
Defined in:
lib/ask/provider/ollama.rb

Overview

Ollama provider for local LLM inference. Connects to a local Ollama server (default: http://localhost:11434).

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ Ollama

Returns a new instance of Ollama.



10
11
12
13
14
# File 'lib/ask/provider/ollama.rb', line 10

def initialize(config = {})
  config = normalize_config(config)
  super(config)
  @http = build_http
end

Class Method Details

.assume_models_exist?Boolean

Returns:

  • (Boolean)


59
# File 'lib/ask/provider/ollama.rb', line 59

def assume_models_exist?; true; end

.capabilitiesObject



52
53
54
# File 'lib/ask/provider/ollama.rb', line 52

def capabilities
  { chat: true, streaming: true, tool_calls: true, embed: true, local: true }
end

.configuration_optionsObject



56
# File 'lib/ask/provider/ollama.rb', line 56

def configuration_options; %i[api_base]; end

.configuration_requirementsObject



57
# File 'lib/ask/provider/ollama.rb', line 57

def configuration_requirements; %i[]; end

.local?Boolean

Returns:

  • (Boolean)


58
# File 'lib/ask/provider/ollama.rb', line 58

def local?; true; end

.slugObject



50
# File 'lib/ask/provider/ollama.rb', line 50

def slug; "ollama"; end

Instance Method Details

#api_baseObject



16
17
18
# File 'lib/ask/provider/ollama.rb', line 16

def api_base
  @config.api_base || "http://localhost:11434"
end

#build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params) ⇒ Object

--- Config transformation contract ---



64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ask/provider/ollama.rb', line 64

def build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params)
  payload = {
    model:,
    messages: messages.map { |m| format_message(m) },
    stream: stream || false,
    options: {}
  }
  payload[:options][:temperature] = temperature if temperature
  tool_defs = format_tools(tools) if tools&.any?
  payload[:tools] = tool_defs if tool_defs
  payload.merge(params)
end

#chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block) ⇒ Object



24
25
26
27
28
29
30
31
32
# File 'lib/ask/provider/ollama.rb', line 24

def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
  msgs = messages.is_a?(Ask::Conversation) ? messages.to_a : messages
  payload = build_request(msgs, model:, tools:, temperature:, stream:, schema:, **params)
  if stream
    chat_stream(payload, model, &block)
  else
    chat_nonstream(payload, model)
  end
end

#embed(texts, model:) ⇒ Object



34
35
36
37
38
39
40
# File 'lib/ask/provider/ollama.rb', line 34

def embed(texts, model:)
  texts = Array(texts)
  response = @http.post("api/embeddings") { |r| r.body = { model:, prompt: texts.first } }
  raise LLM::HTTP.map_error(response.status, response.body, provider: "Ollama") unless response.success?

  Ask::Result.success(response.body["embedding"])
end

#format_message(msg) ⇒ Object



112
113
114
# File 'lib/ask/provider/ollama.rb', line 112

def format_message(msg)
  { role: (msg[:role] || msg["role"]).to_s, content: msg[:content] || msg["content"] }
end

#format_tools(tools) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/ask/provider/ollama.rb', line 116

def format_tools(tools)
  tools.map do |t|
    {
      type: "function",
      function: {
        name: t.respond_to?(:name) ? t.name : t[:name],
        description: t.respond_to?(:description) ? t.description : t[:description],
        parameters: t.respond_to?(:parameters) ? t.parameters : (t[:parameters] || {})
      }
    }
  end
end

#headersObject



20
21
22
# File 'lib/ask/provider/ollama.rb', line 20

def headers
  { "Content-Type" => "application/json" }
end

#list_modelsObject



42
43
44
45
46
47
# File 'lib/ask/provider/ollama.rb', line 42

def list_models
  response = @http.get("api/tags")
  return [] unless response.success?

  response.body["models"].map { |m| Ask::ModelInfo.new(id: m["name"], provider: slug) }
end

#parse_response(body, model) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ask/provider/ollama.rb', line 77

def parse_response(body, model)
  msg = body["message"] || {}
  Ask::Message.new(
    role: :assistant,
    content: msg["content"],
    metadata: {
      model: body["model"] || model,
      done: body["done"],
      total_duration: body["total_duration"],
      raw: body
    }
  )
end

#parse_stream(raw, stream, model, &block) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ask/provider/ollama.rb', line 91

def parse_stream(raw, stream, model, &block)
  @_sse_buffer ||= +""
  @_sse_buffer << raw

  while (line_end = @_sse_buffer.index("\n"))
    line = @_sse_buffer.slice!(0, line_end + 1).strip
    next if line.empty?

    parsed = JSON.parse(line) rescue next
    msg = parsed["message"] || {}
    chunk = Ask::Chunk.new(content: msg["content"])
    stream.add(chunk)
    yield chunk if block_given?
    if parsed["done"]
      chunk = Ask::Chunk.new(finish_reason: "stop", usage: { total_duration: parsed["total_duration"] })
      stream.add(chunk)
      yield chunk if block_given?
    end
  end
end