CouncilEx.Councils.Tournament (CouncilEx v0.1.0)

Copy Markdown View Source

Topology #7 — independent_analysis → iterate(pairwise_elimination) → synthesis.

Members analyse the input in parallel, then a tournament bracket runs pairwise elimination rounds (judged by the chair, or by a custom 2-arity function) until one member remains. The chair then synthesizes a final answer from the winner's output.

Usage

council =
  CouncilEx.Councils.Tournament.new(
    as: MyApp.Tournament,
    members: [
      {:m1, MyApp.Members.M1, [provider: :openai, model: "gpt-4o-mini"]},
      {:m2, MyApp.Members.M2, [provider: :openai, model: "gpt-4o-mini"]},
      {:m3, MyApp.Members.M3, [provider: :openai, model: "gpt-4o-mini"]},
      {:m4, MyApp.Members.M4, [provider: :openai, model: "gpt-4o-mini"]}
    ],
    chair: {MyApp.Members.Synth, [provider: :openai, model: "gpt-4o"]}
  )

{:ok, result} = CouncilEx.run(council, %{topic: "..."})

Options

  • :as — generated module name (default: anonymous unique name).
  • :members (required) — [{id, module, opts}].
  • :chair (required) — {module, opts} tuple. Used both as bracket judge (default) and final synthesizer.
  • :judge — optional custom judge: a 2-arity function fn output_a, output_b -> :a | :b end. Skips the chair for bracket judgments. Useful for deterministic / non-LLM brackets.
  • :max_iterations — bracket depth cap (default 16, supports up to 65536-member brackets).

Internal — judge dispatch

When a 2-arity :judge fn is provided, it is stored in :persistent_term keyed by the generated council module. The generated module exposes a __pairwise_judge__/2 function that looks it up; this captured function reference is what flows through the wrap: :pairwise_elimination round's wrapped_opts[:judge] (anonymous function values cannot be embedded as AST literals, so the :persistent_term indirection is required).

Dynamic form

new_dynamic/1 returns a %CouncilEx.DynamicCouncil{} with the same topology. The optional :judge value MUST be a remote function capture — &MyMod.my_judge/2 — not an inline fn, because dynamic councils are designed to be JSON-serializable (closures aren't). Inline fn works at runtime but won't survive to_json/from_json round-trips.

Summary

Functions

Build the Tournament topology as a generated council module.

Build the same Tournament topology as new/1 but as a data-only %CouncilEx.DynamicCouncil{}.

Functions

new(opts)

@spec new(keyword()) :: module()

Build the Tournament topology as a generated council module.

Options

  • :as — module name for the generated council (default: anonymous unique).
  • :members (required) — [{id, module, opts}] member tuples.
  • :chair (required) — {module, opts} chair tuple. Used both as bracket judge (default) and final synthesizer.
  • :judge — optional 2-arity (output_a, output_b -> :a | :b) function that overrides the chair for bracket judgments. Useful for deterministic / non-LLM brackets.

  • :max_iterations — bracket depth cap (default 16).

See the moduledoc for a complete example. Use new_dynamic/1 for the data-only %CouncilEx.DynamicCouncil{} form.

new_dynamic(opts)

@spec new_dynamic(keyword()) :: CouncilEx.DynamicCouncil.t()

Build the same Tournament topology as new/1 but as a data-only %CouncilEx.DynamicCouncil{}.

Members and chair accept the same shapes as Specialist.new_dynamic/1.

The optional :judge MUST be a remote function capture (&MyMod.fun/2); inline fn literals will work at runtime but break JSON round-trips. Omit :judge to let the chair perform bracket judgments (the standard Vote-style chair-as-judge path).