Current section
Files
Jump to
Current section
Files
src/adk_graph_node.erl
%% @doc adk_graph_node - Helper functions for creating common graph nodes.
%%
%% ADK 2.0 graphs can contain various node types. This module provides builders
%% for common node types to simplify graph construction.
-module(adk_graph_node).
-export([agent_node/3, function_node/1, tool_node/1]).
%% @doc Create a node that executes an agent.
%% The agent will generate a response based on the current state (memory/events).
-spec agent_node(Name :: binary(), Config :: map(), Tools :: [module()]) -> fun((map()) -> map()).
agent_node(Name, Config, Tools) ->
fun(State) ->
%% Extract history from state or use empty
History = maps:get(<<"events">>, State, []),
%% Invoke LLM
case adk_llm:generate(Config, History, Tools) of
{ok, Text} ->
ResponseText = unicode:characters_to_list(Text),
FinalEvent = adk_event:new(Name, ResponseText, #{is_final => true}),
%% Append new event to events list
NewHistory = [FinalEvent | History],
#{<<"events">> => NewHistory, <<"last_agent">> => Name};
{tool_calls, Calls} ->
AgentEvent = adk_event:new(Name, {tool_calls, Calls}, #{}),
NewHistory = [AgentEvent | History],
#{<<"events">> => NewHistory, <<"pending_tools">> => Calls, <<"last_agent">> => Name};
{error, Reason} ->
ErrorEvent = adk_event:new(Name, list_to_binary(io_lib:format("Error: ~p", [Reason])), #{}),
NewHistory = [ErrorEvent | History],
#{<<"events">> => NewHistory, <<"error">> => Reason}
end
end.
%% @doc Create a node that executes a pure Erlang function on the state.
%% Function should take a map (State) and return a map (StateDelta).
-spec function_node(Fun :: fun((map()) -> map())) -> fun((map()) -> map()).
function_node(Fun) when is_function(Fun, 1) ->
fun(State) ->
Fun(State)
end.
%% @doc Create a node that executes pending tool calls.
-spec tool_node(ToolsList :: [module()]) -> fun((map()) -> map()).
tool_node(ToolsList) ->
fun(State) ->
case maps:get(<<"pending_tools">>, State, []) of
[] -> #{}; %% No tools to execute
Calls ->
History = maps:get(<<"events">>, State, []),
NewEvents = execute_tools(Calls, ToolsList, []),
NewHistory = lists:reverse(NewEvents) ++ History,
#{<<"events">> => NewHistory, <<"pending_tools">> => []}
end
end.
%% Internal Tool Execution
execute_tools([], _ToolsList, Acc) ->
Acc;
execute_tools([{NameBin, ArgsMap} | Rest], ToolsList, Acc) ->
execute_tools_inner(NameBin, ArgsMap, undefined, Rest, ToolsList, Acc);
execute_tools([{NameBin, ArgsMap, Sig} | Rest], ToolsList, Acc) ->
execute_tools_inner(NameBin, ArgsMap, Sig, Rest, ToolsList, Acc).
execute_tools_inner(NameBin, ArgsMap, Sig, Rest, ToolsList, Acc) ->
FoundTool = lists:search(
fun(Mod) ->
Schema = Mod:schema(),
maps:get(<<"name">>, Schema, atom_to_binary(Mod, utf8)) == NameBin
end, ToolsList),
Result = case FoundTool of
{value, Mod} ->
case Mod:execute(ArgsMap) of
{ok, Res} -> #{<<"success">> => true, <<"result">> => format_result(Res)};
{error, Reason} -> #{<<"success">> => false, <<"error">> => format_result(Reason)}
end;
false ->
#{<<"success">> => false, <<"error">> => <<"Tool not found">>}
end,
ToolEvent = adk_event:new(<<"tool">>, {tool_response, NameBin, Result, Sig}),
execute_tools(Rest, ToolsList, [ToolEvent | Acc]).
format_result(Res) when is_map(Res) -> Res;
format_result(Res) when is_binary(Res) -> #{<<"result">> => Res};
format_result(Res) -> #{<<"result">> => list_to_binary(io_lib:format("~p", [Res]))}.