Current section
Files
Jump to
Current section
Files
lib/vibe/model/content.ex
defmodule Vibe.Model.Content do
@moduledoc "Provider-neutral model content parts."
alias ReqLLM.Message.ContentPart
defmodule Text do
@moduledoc "Text content part."
defstruct [:text]
@type t :: %__MODULE__{text: String.t()}
end
defmodule Image do
@moduledoc "Image content part with inline base64 data."
defstruct [:data, :mime_type, :filename, :width, :height]
@type t :: %__MODULE__{
data: String.t(),
mime_type: String.t(),
filename: String.t() | nil,
width: pos_integer() | nil,
height: pos_integer() | nil
}
end
@type t :: Text.t() | Image.t()
@spec to_req_llm_parts([t() | ContentPart.t()]) :: [ContentPart.t()]
def to_req_llm_parts(parts) when is_list(parts), do: Enum.map(parts, &to_req_llm_part/1)
@spec to_req_llm_tool_parts([t() | ContentPart.t()]) :: [ContentPart.t()]
def to_req_llm_tool_parts(parts) when is_list(parts),
do: Enum.map(parts, &to_req_llm_tool_part/1)
@spec to_req_llm_part(t() | ContentPart.t()) :: ContentPart.t()
def to_req_llm_part(%ContentPart{} = part), do: part
def to_req_llm_part(%Text{text: text}), do: ContentPart.text(text)
def to_req_llm_part(%Image{} = image) do
metadata =
%{
filename: image.filename,
width: image.width,
height: image.height
}
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
image.data
|> Base.decode64!()
|> ContentPart.image(image.mime_type, metadata)
|> Map.put(:filename, image.filename)
end
@spec to_req_llm_tool_part(t() | ContentPart.t()) :: ContentPart.t()
def to_req_llm_tool_part(%ContentPart{} = part), do: part
def to_req_llm_tool_part(%Text{} = text), do: to_req_llm_part(text)
def to_req_llm_tool_part(%Image{} = image) do
metadata =
%{filename: image.filename, width: image.width, height: image.height}
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
image
|> data_uri()
|> ContentPart.image_url(metadata)
end
@spec summarize(t() | [t()] | String.t()) :: String.t()
def summarize(text) when is_binary(text), do: text
def summarize(parts) when is_list(parts), do: Enum.map_join(parts, "\n", &summarize/1)
def summarize(%Text{text: text}), do: text
def summarize(%ContentPart{type: :text, text: text}) when is_binary(text), do: text
def summarize(%ContentPart{type: type} = part) when type in [:image, :image_url] do
["[Image", Map.get(part, :filename), Map.get(part, :media_type) || Map.get(part, :mime_type)]
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.join(" ")
|> Kernel.<>("]")
end
def summarize(%Image{} = image) do
["[Image", image.filename, image.mime_type, dimensions(image)]
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.join(" ")
|> Kernel.<>("]")
end
@spec text(String.t()) :: Text.t()
def text(value) when is_binary(value), do: %Text{text: value}
@spec image(keyword()) :: Image.t()
def image(fields) when is_list(fields) do
%Image{
data: Keyword.fetch!(fields, :data),
mime_type: Keyword.fetch!(fields, :mime_type),
filename: Keyword.get(fields, :filename),
width: Keyword.get(fields, :width),
height: Keyword.get(fields, :height)
}
end
defp data_uri(%Image{} = image), do: "data:#{image.mime_type};base64,#{image.data}"
defp dimensions(%Image{width: width, height: height})
when is_integer(width) and is_integer(height),
do: "#{width}x#{height}"
defp dimensions(_image), do: nil
end