Current section

Files

Jump to
pi_bridge lib pi agent.ex
Raw

lib/pi/agent.ex

defmodule Pi.Agent do
@moduledoc "Unified BEAM abstraction for top-level agents and child agents."
alias Pi.Agent.Registry
alias Pi.Agent.Result
alias Pi.Agent.Run
alias Pi.Agent.Session
alias Pi.Agent.Step
alias Pi.Protocol.LLM.Message
alias Pi.Session, as: RuntimeSession
def run(prompt_or_opts, opts \\ []) do
session = prompt_or_opts |> session(opts) |> Registry.put()
with {:ok, runtime} <- start_runtime_session(session, opts) do
complete_runtime(runtime, session, opts)
end
end
def run!(prompt_or_opts, opts \\ []) do
case run(prompt_or_opts, opts) do
{:ok, result} -> result
{:error, %Result{error: reason}} -> raise RuntimeError, message: inspect(reason)
end
end
def async(prompt_or_opts, opts \\ []) do
Task.async(fn -> run(prompt_or_opts, opts) end)
end
def await(task, timeout \\ 60_000), do: Task.await(task, timeout)
def await_many(tasks, timeout \\ 60_000) do
Enum.map(tasks, &await(&1, timeout))
end
def parallel(runs, opts \\ []) when is_list(runs) do
{:ok, parent} = RuntimeSession.start(name: Keyword.get(opts, :name, :parallel))
timeout = Keyword.get(opts, :timeout, 60_000)
results =
runs
|> Enum.with_index(1)
|> Enum.map(fn {run, index} ->
Task.async(fn -> run_child(parent, run, child_opts(run, opts, index)) end)
end)
|> await_many(timeout + 1_000)
if Enum.all?(results, &match?({:ok, %Result{}}, &1)) do
{:ok, Run.ok(:parallel, Enum.map(results, fn {:ok, result} -> result end))}
else
{:error, Run.error(:parallel, results, :one_or_more_failed)}
end
end
def fanout(inputs, opts \\ []) when is_list(inputs), do: parallel(inputs, opts)
def chain(steps, opts \\ []) when is_list(steps) do
case reduce_chain(steps, opts, nil, []) do
{:ok, results} -> {:ok, Run.ok(:chain, Enum.reverse(results))}
{:error, results, reason} -> {:error, Run.error(:chain, Enum.reverse(results), reason)}
end
end
def child(%Session{} = parent, opts \\ []) do
parent
|> Session.child(opts)
|> Registry.put()
end
def sessions, do: Registry.sessions()
def children(parent), do: Registry.children(parent)
def history(agent), do: Registry.history(agent)
def session(prompt_or_opts, opts \\ [])
def session(%Step{} = step, opts), do: Step.to_session(step, opts)
def session(prompt, opts) when is_binary(prompt) do
opts
|> Keyword.put(:messages, [%Message{role: :user, content: prompt}])
|> Session.new()
end
def session(opts, extra_opts) when is_list(opts) do
opts
|> Keyword.merge(extra_opts)
|> Session.new()
end
def session(%Session{} = session, _opts), do: session
defp child_opts(run, opts, index) do
opts = Keyword.delete(opts, :name)
if child_has_name?(run) do
opts
else
Keyword.put(opts, :name, child_name(run, index))
end
end
defp child_has_name?(run) when is_list(run), do: Keyword.has_key?(run, :name)
defp child_has_name?(_run), do: false
defp child_name(prompt, _index) when is_binary(prompt) do
prompt
|> String.replace(~r/\s+/u, " ")
|> String.trim()
|> String.slice(0, 40)
end
defp child_name(_run, index), do: "child #{index}"
defp run_child(parent, prompt_or_opts, opts) do
session = prompt_or_opts |> session(opts) |> Registry.put()
with {:ok, runtime} <- start_runtime_child(parent, session, opts) do
complete_runtime(runtime, session, opts)
end
end
defp complete_runtime(runtime, %Session{} = session, opts) do
case RuntimeSession.complete(runtime, Keyword.put(opts, :agent, session.id)) do
{:ok, result} ->
Registry.append(session.id, %Message{role: :assistant, content: result})
{:ok, Result.ok(session, result)}
{:error, reason} ->
{:error, Result.error(session, reason)}
end
end
defp start_runtime_session(%Session{} = session, opts) do
RuntimeSession.start(
id: session.id,
parent_id: session.parent_id,
name: session.name,
system: session.system,
messages: session.messages,
metadata: Map.merge(session.metadata, Map.new(Keyword.get(opts, :metadata, %{})))
)
end
defp start_runtime_child(parent, %Session{} = session, opts) do
RuntimeSession.child(parent,
id: session.id,
name: session.name,
system: session.system,
messages: session.messages,
metadata: Map.merge(session.metadata, Map.new(Keyword.get(opts, :metadata, %{})))
)
end
defp reduce_chain([], _opts, _previous, results), do: {:ok, results}
defp reduce_chain([step | steps], opts, previous, results) do
input = chain_input(step, previous)
case run(input, opts) do
{:ok, %Result{result: result} = agent_result} ->
reduce_chain(steps, opts, result, [agent_result | results])
{:error, reason} ->
{:error, results, reason}
end
end
defp chain_input(step, nil), do: step
defp chain_input(step, previous) when is_binary(step),
do: step <> "\n\nPrevious result:\n" <> inspect(previous)
defp chain_input(step, previous) when is_list(step) do
Keyword.update(
step,
:messages,
[%Message{role: :user, content: inspect(previous)}],
fn messages ->
messages ++ [%Message{role: :user, content: inspect(previous)}]
end
)
end
end