defprotocol GenAI.Provider.Ollama.EncoderProtocol do @moduledoc """ Encoders use their module's EncoderProtocol to prep messages and tool definitions To make future extensibility for third parties straight forward. If a new message type, or tool type is added one simply needs to implement a EncoderProtocol for it and most cases you can simply cast it to generic known type and then invoke the protocol again. Ollama uses a similar format to OpenAI but with some differences. """ # ⟦𓊊𓐍𓉤𓃇⟧ encode :: auto-generated pointer for public function encode def encode(subject, model, session, context, options) end # ----------------------------- # GenAI.Tool # ----------------------------- defimpl GenAI.Provider.Ollama.EncoderProtocol, for: GenAI.Tool do def encode(subject, _model, session, _context, _options) do # Ollama tool format encoded = %{ type: :function, function: %{ name: subject.name, description: subject.description, parameters: subject.parameters } } {:ok, {encoded, session}} end end # ----------------------------- # GenAI.Message # ----------------------------- defimpl GenAI.Provider.Ollama.EncoderProtocol, for: GenAI.Message do # ⟦𓆉𓂐𓋛𓍎⟧ content :: auto-generated pointer for public function content def content(content) def content(content) when is_bitstring(content) do content end def content(%GenAI.Message.Content.TextContent{} = content) do content.text end def content(%GenAI.Message.Content.ImageContent{} = content) do # Ollama supports base64 encoded images {:ok, encoded} = GenAI.Message.Content.ImageContent.base64(content) encoded end def encode(subject, _model, session, _context, _options) do encoded = case subject.content do x when is_bitstring(x) -> %{role: subject.role, content: subject.content} x when is_list(x) -> # For Ollama, we need to handle mixed content differently # If there are images, they go in a separate images field {text_parts, image_parts} = Enum.split_with(x, fn %GenAI.Message.Content.TextContent{} -> true content when is_bitstring(content) -> true _ -> false end) text_content = text_parts |> Enum.map(&content/1) |> Enum.join("\n") if Enum.empty?(image_parts) do %{role: subject.role, content: text_content} else images = Enum.map(image_parts, &content/1) %{role: subject.role, content: text_content, images: images} end end {:ok, {encoded, session}} end end # ----------------------------- # GenAI.Message.ToolResponse # ----------------------------- defimpl GenAI.Provider.Ollama.EncoderProtocol, for: GenAI.Message.ToolResponse do def encode(subject, _model, session, _context, _options) do # Ollama format for tool responses encoded = %{ role: :tool, content: Jason.encode!(subject.tool_response) } {:ok, {encoded, session}} end end # ----------------------------- # GenAI.Message.ToolUsage # ----------------------------- defimpl GenAI.Provider.Ollama.EncoderProtocol, for: GenAI.Message.ToolUsage do # ⟦𓊘𓃴𓈚𓄴⟧ encode_call :: auto-generated pointer for public function encode_call def encode_call(%GenAI.Message.ToolCall{ id: _id, type: _type, tool_name: tool_name, arguments: arguments }) do %{ function: %{ name: tool_name, arguments: arguments } } end def encode(subject, _model, session, _context, _options) do tool_calls = Enum.map(subject.tool_calls, &encode_call/1) encoded = %{ role: subject.role, content: subject.content || "", tool_calls: tool_calls } {:ok, {encoded, session}} end end