Class: Spectre::Ollama::Completions

Inherits:
Object
  • Object
show all
Defined in:
lib/spectre/ollama/completions.rb

Constant Summary collapse

API_PATH =
'api/chat'
DEFAULT_MODEL =
'llama3.1:8b'
DEFAULT_TIMEOUT =
60

Class Method Summary collapse

Class Method Details

.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash

Class method to generate a completion based on user messages and optional tools

Parameters:

  • messages (Array<Hash>)

    The conversation messages, each with a role and content

  • model (String) (defaults to: DEFAULT_MODEL)

    The model to be used for generating completions, defaults to DEFAULT_MODEL

  • json_schema (Hash, nil) (defaults to: nil)

    An optional JSON schema to enforce structured output

  • tools (Array<Hash>, nil) (defaults to: nil)

    An optional array of tool definitions for function calling

  • args (Hash, nil)

    optional arguments like read_timeout and open_timeout. Any additional top-level kwargs (e.g., temperature:, max_tokens:) will be forwarded into body, same as other providers forward into body.

  • path (String, nil)

    Top-level path override for the Ollama API endpoint, defaults to API_PATH

Returns:

  • (Hash)

    The parsed response including any function calls or content

Raises:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/spectre/ollama/completions.rb', line 27

def self.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args)
  api_host = Spectre.ollama_configuration.host
  api_key = Spectre.ollama_configuration.api_key
  raise HostNotConfiguredError, "Host is not configured" unless api_host
  raise APIKeyNotConfiguredError, "API key is not configured" unless api_key

  validate_messages!(messages)

  path = args[:path] || API_PATH
  uri = URI.join(api_host, path)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true if uri.scheme == 'https'
  http.read_timeout = args.fetch(:read_timeout, DEFAULT_TIMEOUT)
  http.open_timeout = args.fetch(:open_timeout, DEFAULT_TIMEOUT)

  request = Net::HTTP::Post.new(uri.path, {
    'Content-Type' => 'application/json',
    'Authorization' => "Bearer #{api_key}"
  })

  # Forward extra top-level args (like temperature, max_tokens) into body[:options],
  # excluding control/network keys and the request path override.
  forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :path].include?(k) }
  options = nil
  if forwarded && !forwarded.empty?
    options = forwarded.transform_keys(&:to_sym)
  end
  request.body = generate_body(messages, model, json_schema, tools, options).to_json
  response = http.request(request)

  unless response.is_a?(Net::HTTPSuccess)
    raise "Ollama API Error: #{response.code} - #{response.message}: #{response.body}"
  end

  parsed_response = JSON.parse(response.body)

  handle_response(parsed_response)
rescue JSON::ParserError => e
  raise "JSON Parse Error: #{e.message}"
end

.generate_body(messages, model, json_schema, tools, options) ⇒ Hash

Helper method to generate the request body

Parameters:

  • messages (Array<Hash>)

    The conversation messages, each with a role and content

  • model (String)

    The model to be used for generating completions

  • json_schema (Hash, nil)

    An optional JSON schema to enforce structured output

  • tools (Array<Hash>, nil)

    An optional array of tool definitions for function calling

  • options (Hash, nil)

    Additional model parameters listed in the documentation for the github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values such as temperature

Returns:

  • (Hash)

    The body for the API request



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/spectre/ollama/completions.rb', line 97

def self.generate_body(messages, model, json_schema, tools, options)
  body = {
    model: model,
    stream: false,
    messages: messages
  }

  # Extract schema if json_schema follows OpenAI's structure
  if json_schema.is_a?(Hash) && json_schema.key?(:schema)
    body[:format] = json_schema[:schema] # Use only the "schema" key
  elsif json_schema.is_a?(Hash)
    body[:format] = json_schema # Use the schema as-is if it doesn't follow OpenAI's structure
  end

  body[:tools] = tools if tools # Add the tools to the request body if provided
  body[:options] = options if options

  body
end

.handle_response(response) ⇒ Hash

Handles the API response, raising errors for specific cases and returning structured content otherwise

Parameters:

  • response (Hash)

    The parsed API response

Returns:

  • (Hash)

    The relevant data based on the finish reason



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/spectre/ollama/completions.rb', line 121

def self.handle_response(response)
  message = response.dig('message')
  finish_reason = response.dig('done_reason')
  done = response.dig('done')

  # Check if the model made a function call
  if message['tool_calls'] && !message['tool_calls'].empty?
    return { tool_calls: message['tool_calls'], content: message['content'] }
  end

  # If the response finished normally, return the content
  if done
    return { content: message['content'] }
  end

  # Handle unexpected finish reasons
  raise "Unexpected finish_reason: #{finish_reason}, done: #{done}, message: #{message}"
end

.validate_messages!(messages) ⇒ Object

Validate the structure and content of the messages array.

Parameters:

  • messages (Array<Hash>)

    The array of message hashes to validate.

Raises:

  • (ArgumentError)

    if the messages array is not in the expected format or contains invalid data.



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/spectre/ollama/completions.rb', line 75

def self.validate_messages!(messages)
  # Check if messages is an array of hashes.
  # This ensures that the input is in the correct format for message processing.
  unless messages.is_a?(Array) && messages.all? { |msg| msg.is_a?(Hash) }
    raise ArgumentError, "Messages must be an array of message hashes."
  end

  # Check if the array is empty.
  # This prevents requests with no messages, which would be invalid.
  if messages.empty?
    raise ArgumentError, "Messages cannot be empty."
  end
end