defmodule CouncilEx.Councils.Voting do
@moduledoc """
Topology #5 — independent_analysis → vote (with aggregator) → chair.
All members analyse independently, then cast structured votes. The vote
round aggregates results; the chair synthesizes from the vote outcome.
## Usage
council =
CouncilEx.Councils.Voting.new(
as: MyApp.VoteCouncil,
members: [
{:m1, MyApp.Members.Voter, [provider: :openai, model: "gpt-4o-mini"]},
{:m2, MyApp.Members.Voter, [provider: :openai, model: "gpt-4o-mini"]}
],
chair: {MyApp.Members.Synth, [provider: :openai, model: "gpt-4o"]},
aggregator: CouncilEx.Aggregators.WeightedMean
)
"""
@new_opts_schema NimbleOptions.new!(
as: [type: :atom, required: false],
members: [type: {:list, :any}, required: true],
chair: [type: :any, required: true],
aggregator: [type: :atom, default: CouncilEx.Aggregators.Plurality]
)
@spec new(keyword()) :: module()
def new(opts) do
NimbleOptions.validate!(opts, @new_opts_schema)
module_name = Keyword.get_lazy(opts, :as, &generate_name/0)
members = Keyword.fetch!(opts, :members)
{chair_mod, chair_opts} = Keyword.fetch!(opts, :chair)
aggregator = Keyword.get(opts, :aggregator, CouncilEx.Aggregators.Plurality)
member_calls =
Enum.map(members, fn {id, mod, mem_opts} ->
quote do
member(unquote(id), unquote(mod), unquote(mem_opts))
end
end)
body =
quote do
use CouncilEx
unquote_splicing(member_calls)
CouncilEx.round(:independent_analysis)
CouncilEx.round(:vote, aggregator: unquote(aggregator))
CouncilEx.chair(unquote(chair_mod), unquote(chair_opts))
end
Module.create(module_name, body, Macro.Env.location(__ENV__))
module_name
end
defp generate_name do
Module.concat([__MODULE__, "Generated", "#{:erlang.unique_integer([:positive, :monotonic])}"])
end
end