lib/genai_providers/deep_seek/encoder_protocol.ex

defprotocol GenAI.Provider.DeepSeek.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.
  """
  def encode(subject, model, session, context, options)
end

# -----------------------------
# GenAI.Tool
# -----------------------------
defimpl GenAI.Provider.DeepSeek.EncoderProtocol, for: GenAI.Tool do
  def encode(subject, _model, session, _context, _options) do
    encoded = %{
      type: :function,
      function: %{
        name: subject.name,
        description: subject.description,
        parameters: subject.parameters
      }
    }

    {:ok, {encoded, session}}
  end
end

# -----------------------------
# GenAI.Message
# -----------------------------
defimpl GenAI.Provider.DeepSeek.EncoderProtocol, for: GenAI.Message do
  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
    "AN IMAGE WAS INCLUDED IN THE MESSAGE: (TODO: IMAGE TO TEXT CAPTIONS)"
  end

  def content(%GenAI.Message.Content.AudioContent{} = content) do
    content.transcript
  end

  def content(%GenAI.Message.Content.ToolUseContent{} = content) do
    %{
      id: content.id,
      type: :function,
      function: %{
        name: content.tool_name,
        arguments: Jason.encode!(content.arguments)
      }
    }
  end

  def content(%GenAI.Message.ToolCall{} = content) do
    %{
      id: content.id,
      type: :function,
      function: %{
        name: content.tool_name,
        arguments: Jason.encode!(content.arguments)
      }
    }
  end

  def content(content) do
    """
    UNSUPPORTED MESSAGE PART:
    #{inspect(content, limit: :infinity, pretty: true)}
    """
  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) ->
          reasoning =
            x
            |> Enum.filter(fn
              %GenAI.Message.Content.ThinkingContent{} -> true
              _ -> false
            end)
            |> Enum.map(&content/1)
            |> Enum.join("\n ------------------------ \n")

          tool_calls =
            x
            |> Enum.filter(fn
              %GenAI.Message.Content.ToolUseContent{} -> true
              %GenAI.Message.ToolCall{} -> true
              _ -> false
            end)
            |> Enum.map(&content/1)
            |> Enum.reject(&is_nil/1)

          content =
            x
            |> Enum.reject(fn
              %GenAI.Message.Content.ThinkingContent{} -> true
              %GenAI.Message.Content.ToolUseContent{} -> true
              %GenAI.Message.ToolCall{} -> true
              _ -> false
            end)
            |> Enum.map(&content/1)
            |> Enum.reject(&is_nil/1)
            |> Enum.join("\n ------------------------ \n")

          %{role: subject.role, content: content}
          |> then(
            &if tool_calls != [],
              do: put_in(&1, [Access.key(:tool_calls)], tool_calls),
              else: &1
          )
          |> then(
            &if reasoning != "",
              do: put_in(&1, [Access.key(:reasoning_content)], reasoning),
              else: &1
          )
      end
      |> then(
        &if subject.user,
          do: put_in(&1, [Access.key(:name)], subject.user),
          else: &1
      )

    {:ok, {encoded, session}}
  end
end

# -----------------------------
# GenAI.Message.ToolResponse
# -----------------------------
defimpl GenAI.Provider.DeepSeek.EncoderProtocol, for: GenAI.Message.ToolResponse do
  def encode(subject, _model, session, _context, _options) do
    encoded = %{
      role: :tool,
      tool_call_id: subject.tool_call_id,
      content: Jason.encode!(subject.tool_response)
    }

    {:ok, {encoded, session}}
  end
end

# -----------------------------
# GenAI.Message.ToolUsage
# -----------------------------
defimpl GenAI.Provider.DeepSeek.EncoderProtocol, for: GenAI.Message.ToolUsage do
  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
    "AN IMAGE WAS INCLUDED IN THE MESSAGE: (TODO: IMAGE TO TEXT CAPTIONS)"
  end

  def content(%GenAI.Message.Content.AudioContent{} = content) do
    content.transcript
  end

  def content(%GenAI.Message.Content.ToolUseContent{} = content) do
    %{
      id: content.id,
      type: :function,
      function: %{
        name: content.tool_name,
        arguments: Jason.encode!(content.arguments)
      }
    }
  end

  def content(%GenAI.Message.ToolCall{} = content) do
    %{
      id: content.id,
      type: :function,
      function: %{
        name: content.tool_name,
        arguments: Jason.encode!(content.arguments)
      }
    }
  end

  def content(content) do
    """
    UNSUPPORTED MESSAGE PART:
    #{inspect(content, limit: :infinity, pretty: true)}
    """
  end

  def encode(subject, _model, session, _context, _options) do
    content =
      cond do
        is_list(subject.content) -> subject.content
        is_bitstring(subject.content) -> [subject.content]
      end

    reasoning =
      content
      |> Enum.filter(fn
        %GenAI.Message.Content.ThinkingContent{} -> true
        _ -> false
      end)
      |> Enum.map(&content/1)
      |> Enum.join("\n ------------------------ \n")

    tool_calls =
      content
      |> Enum.filter(fn
        %GenAI.Message.Content.ToolUseContent{} -> true
        %GenAI.Message.ToolCall{} -> true
        _ -> false
      end)

    tool_calls =
      (tool_calls ++ (subject.tool_calls || []))
      |> Enum.map(&content/1)
      |> Enum.reject(&is_nil/1)

    content =
      content
      |> Enum.reject(fn
        %GenAI.Message.Content.ThinkingContent{} -> true
        %GenAI.Message.Content.ToolUseContent{} -> true
        %GenAI.Message.ToolCall{} -> true
        _ -> false
      end)
      |> Enum.map(&content/1)
      |> Enum.reject(&is_nil/1)
      |> Enum.join("\n ------------------------ \n")

    encoded =
      %{
        role: subject.role,
        content: content,
        tool_calls: tool_calls
      }
      |> then(
        &if reasoning != "",
          do: put_in(&1, [Access.key(:reasoning_content)], reasoning),
          else: &1
      )
      |> then(
        &if subject.user,
          do: put_in(&1, [Access.key(:name)], subject.user),
          else: &1
      )

    {:ok, {encoded, session}}
  end
end