defmodule AgentSea.Capability do
@moduledoc """
A named capability an agent has, with a proficiency level. Capability matching
is pure: given an agent's capabilities and the names a task requires, compute
which are matched/missing, an aggregate score in `[0, 1]`, and whether the
agent can execute the task at all.
"""
@enforce_keys [:name]
defstruct [:name, :description, proficiency: :intermediate, keywords: []]
@type proficiency :: :novice | :intermediate | :expert | :master
@type t :: %__MODULE__{
name: String.t(),
description: String.t() | nil,
proficiency: proficiency(),
keywords: [String.t()]
}
@type match :: %{
matched: [String.t()],
missing: [String.t()],
score: float(),
can_execute: boolean()
}
@scores %{novice: 0.25, intermediate: 0.5, expert: 0.75, master: 1.0}
@doc "Numeric weight for a capability's proficiency."
@spec proficiency_score(t()) :: float()
def proficiency_score(%__MODULE__{proficiency: p}), do: Map.get(@scores, p, 0.5)
@doc """
Match agent capabilities against the capability names a task requires.
With no required capabilities, the score reflects the agent's overall
proficiency and `can_execute` is true. Otherwise the score is coverage
(fraction of required capabilities present) times the average proficiency of
the matched ones, and `can_execute` is true only when nothing is missing.
"""
@spec match([t()], [String.t()]) :: match()
def match(agent_capabilities, [] = _required) do
%{matched: [], missing: [], score: avg_proficiency(agent_capabilities), can_execute: true}
end
def match(agent_capabilities, required_names) do
have = Map.new(agent_capabilities, &{&1.name, &1})
required = Enum.uniq(required_names)
{matched, missing} = Enum.split_with(required, &Map.has_key?(have, &1))
coverage = length(matched) / length(required)
avg = matched |> Enum.map(&proficiency_score(have[&1])) |> average(0.0)
%{
matched: matched,
missing: missing,
score: coverage * avg,
can_execute: missing == []
}
end
defp avg_proficiency([]), do: 0.0
defp avg_proficiency(caps), do: caps |> Enum.map(&proficiency_score/1) |> average(0.0)
defp average([], default), do: default
defp average(list, _default), do: Enum.sum(list) / length(list)
end