Packages

fnord

0.9.21

AI code archaeology

Current section

Files

Jump to
fnord lib skills toml.ex
Raw

lib/skills/toml.ex

defmodule Skills.Toml do
@moduledoc """
Encode a skill definition as TOML.
Skills are stored as TOML files so they are easy to create and edit by hand.
This module intentionally implements only the subset of TOML we need for the
skill schema:
- strings
- string arrays
- a shallow `response_format` table (string/number/bool values)
If we need richer encoding later, we can expand this module or swap it for a
dedicated encoder.
"""
@type encode_error ::
{:invalid_response_format_value, key :: String.t(), term()}
| {:invalid_response_format_key, term()}
@doc """
Encode a `%Skills.Skill{}` to a TOML string.
The output is stable (fixed key order) to minimize noisy diffs.
"""
@spec encode_skill(Skills.Skill.t()) :: {:ok, String.t()} | {:error, encode_error}
def encode_skill(%Skills.Skill{} = skill) do
with {:ok, rf_section} <- response_format_section(skill.response_format) do
toml =
[
kv_string("name", skill.name),
kv_string("description", skill.description),
kv_string("model", skill.model),
kv_string_array("tools", skill.tools),
kv_multiline_string("system_prompt", skill.system_prompt),
rf_section
]
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")
|> Kernel.<>("\n")
{:ok, toml}
end
end
defp response_format_section(nil), do: {:ok, ""}
defp response_format_section(%{} = map) do
map
|> normalize_keys()
|> Enum.sort_by(fn {k, _v} -> k end)
|> Enum.reduce_while({:ok, []}, fn {k, v}, {:ok, acc} ->
case {k, v} do
{key, _} when not is_binary(key) ->
{:halt, {:error, {:invalid_response_format_key, key}}}
{key, value} ->
case encode_value(value) do
{:ok, encoded} -> {:cont, {:ok, ["#{key} = #{encoded}" | acc]}}
{:error, _} -> {:halt, {:error, {:invalid_response_format_value, key, value}}}
end
end
end)
|> case do
{:ok, lines} ->
lines = Enum.reverse(lines)
{:ok,
[
"",
"[response_format]",
Enum.join(lines, "\n")
]
|> Enum.join("\n")}
{:error, _} = err ->
err
end
end
defp response_format_section(other),
do: {:error, {:invalid_response_format_value, "response_format", other}}
defp normalize_keys(map) do
Map.new(map, fn
{k, v} when is_atom(k) -> {Atom.to_string(k), v}
{k, v} -> {k, v}
end)
end
defp encode_value(value) when is_binary(value), do: {:ok, encode_string(value)}
defp encode_value(value) when is_boolean(value), do: {:ok, if(value, do: "true", else: "false")}
defp encode_value(value) when is_integer(value), do: {:ok, Integer.to_string(value)}
defp encode_value(value) when is_float(value), do: {:ok, Float.to_string(value)}
defp encode_value(_), do: {:error, :unsupported}
defp kv_string(key, value) do
"#{key} = #{encode_string(value)}"
end
defp kv_string_array(key, values) when is_list(values) do
encoded =
values
|> Enum.filter(&is_binary/1)
|> Enum.map(&encode_string/1)
|> Enum.join(", ")
"#{key} = [#{encoded}]"
end
defp kv_multiline_string(key, value) do
"#{key} = \"\"\"\n#{escape_triple_quotes(value)}\n\"\"\""
end
defp encode_string(value) do
escaped =
value
|> String.replace("\\", "\\\\")
|> String.replace("\"", "\\\"")
|> String.replace("\n", "\\n")
"\"#{escaped}\""
end
defp escape_triple_quotes(value) do
String.replace(value, "\"\"\"", "\\\"\\\"\\\"")
end
end