Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/controllers/api/agent_request.ex
defmodule SyntropyWeb.Api.AgentRequest do
@moduledoc false
@type parsed_request :: %{name: String.t(), perspective: String.t(), opts: keyword()}
@score_keys ~w(knowledge synthesis connections)
@spec parse(map()) :: {:ok, parsed_request()} | {:error, [map()]}
def parse(params) do
with {:ok, name} <- parse_required_string(Map.get(params, "name"), "name"),
{:ok, perspective} <-
parse_required_string(Map.get(params, "perspective"), "perspective"),
{:ok, id} <- parse_id(Map.get(params, "id")),
{:ok, knowledge_items} <- parse_knowledge_items(Map.get(params, "knowledge_items")),
{:ok, score_vector} <- parse_score_vector(Map.get(params, "score_vector")),
{:ok, position} <- parse_unit_float(Map.get(params, "position"), "position") do
{:ok,
%{
name: name,
perspective: perspective,
opts:
[knowledge_items: knowledge_items]
|> maybe_put(:id, id)
|> maybe_put(:score_vector, score_vector)
|> maybe_put(:position, position)
}}
end
end
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
defp parse_required_string(value, field) when is_binary(value) do
trimmed = String.trim(value)
if trimmed == "" do
{:error, [error(field, "must be a non-empty string")]}
else
{:ok, trimmed}
end
end
defp parse_required_string(_value, field), do: {:error, [error(field, "is required")]}
defp parse_id(nil), do: {:ok, nil}
defp parse_id(id) when is_binary(id) do
trimmed = String.trim(id)
cond do
trimmed == "" -> {:ok, nil}
Regex.match?(~r/^[a-z0-9][a-z0-9_-]*$/, trimmed) -> {:ok, trimmed}
true -> {:error, [error("id", "must contain only lowercase letters, digits, - or _")]}
end
end
defp parse_id(_id), do: {:error, [error("id", "must be a string")]}
defp parse_knowledge_items(nil), do: {:ok, []}
defp parse_knowledge_items(items) when is_list(items) do
if Enum.all?(items, &is_binary/1) do
normalized =
items
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.uniq()
{:ok, normalized}
else
{:error, [error("knowledge_items", "must be an array of strings")]}
end
end
defp parse_knowledge_items(_items),
do: {:error, [error("knowledge_items", "must be an array of strings")]}
defp parse_score_vector(nil), do: {:ok, nil}
defp parse_score_vector(score_vector) when is_map(score_vector) do
parsed =
Enum.reduce_while(@score_keys, %{}, fn key, acc ->
case parse_unit_float(Map.get(score_vector, key), "score_vector.#{key}") do
{:ok, nil} -> {:cont, Map.put(acc, String.to_existing_atom(key), 0.0)}
{:ok, value} -> {:cont, Map.put(acc, String.to_existing_atom(key), value)}
{:error, errors} -> {:halt, {:error, errors}}
end
end)
case parsed do
{:error, errors} -> {:error, errors}
vector -> {:ok, vector}
end
end
defp parse_score_vector(_score_vector),
do: {:error, [error("score_vector", "must be an object")]}
defp parse_unit_float(nil, _field), do: {:ok, nil}
defp parse_unit_float(value, field) when is_number(value) do
if value >= 0 and value <= 1 do
{:ok, value / 1}
else
{:error, [error(field, "must be a number between 0 and 1")]}
end
end
defp parse_unit_float(_value, field),
do: {:error, [error(field, "must be a number between 0 and 1")]}
defp error(field, message), do: %{field: field, message: message}
end