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