Current section

Files

Jump to
erlang_adk src adk_graph_node.erl
Raw

src/adk_graph_node.erl

%% @doc adk_graph_node - Helper functions for creating common graph nodes.
%%
%% Erlang ADK 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
HistoryEvents = maps:get(<<"events">>, State, []),
Memory0 = adk_memory:from_events(HistoryEvents),
Memory = ensure_instructions(Memory0, Config),
History = adk_memory:get_history(Memory),
%% Invoke LLM
case generate_with_callbacks(Config, Memory, History, Tools) of
{ok, Text} ->
ResponseText = unicode:characters_to_binary(Text),
FinalEvent = adk_event:new(Name, ResponseText, #{is_final => true}),
%% Append new event to events list
NewHistory = HistoryEvents ++ [FinalEvent],
#{<<"events">> => NewHistory, <<"last_agent">> => Name};
{tool_calls, Calls} ->
AgentEvent = adk_event:new(Name, {tool_calls, Calls}, #{}),
NewHistory = HistoryEvents ++ [AgentEvent],
#{<<"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 = HistoryEvents ++ [ErrorEvent],
#{<<"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 = History ++ lists:reverse(NewEvents),
#{<<"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, undefined, Rest,
ToolsList, Acc);
execute_tools([{NameBin, ArgsMap, Sig} | Rest], ToolsList, Acc) ->
execute_tools_inner(NameBin, ArgsMap, Sig, undefined, Rest,
ToolsList, Acc);
execute_tools([{NameBin, ArgsMap, Sig, CallId} | Rest], ToolsList, Acc) ->
execute_tools_inner(NameBin, ArgsMap, Sig, CallId, Rest, ToolsList, Acc).
execute_tools_inner(NameBin, ArgsMap, Sig, CallId, 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} ->
try Mod:execute(ArgsMap, #{}) of
{ok, Res} -> #{<<"success">> => true, <<"result">> => format_result(Res)};
{error, Reason} -> #{<<"success">> => false, <<"error">> => format_result(Reason)};
Other -> #{<<"success">> => false,
<<"error">> => format_result({invalid_tool_result, Other})}
catch
Class:Failure ->
#{<<"success">> => false,
<<"error">> => format_result({Class, Failure})}
end;
false ->
#{<<"success">> => false, <<"error">> => <<"Tool not found">>}
end,
ToolResponse = case CallId of
undefined -> {tool_response, NameBin, Result, Sig};
_ -> {tool_response, NameBin, Result, Sig, CallId}
end,
ToolEvent = adk_event:new(<<"tool">>, ToolResponse),
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">> => unicode:characters_to_binary(io_lib:format("~p", [Res]))}.
ensure_instructions(Memory, Config) ->
case lists:any(fun(#{role := system}) -> true; (_) -> false end, Memory) of
true -> Memory;
false ->
Instructions = maps:get(instructions, Config,
<<"You are a helpful assistant.">>),
Memory ++ [#{role => system, content => Instructions,
timestamp => erlang:system_time(millisecond)}]
end.
generate_with_callbacks(Config, Memory, History, Tools) ->
Handlers = maps:get(callbacks, Config, []),
RawResult = case adk_callbacks:run(Handlers, before_model,
[Config, Memory, Tools]) of
{halt, Replacement} -> Replacement;
{replace, Replacement} -> Replacement;
_ ->
try adk_llm:generate(Config, History, Tools) of
Result -> Result
catch
Class:Reason -> {error, {Class, Reason}}
end
end,
case adk_callbacks:run(Handlers, after_model, [Config, RawResult]) of
{halt, ReplacementResult} -> ReplacementResult;
{replace, ReplacementResult} -> ReplacementResult;
_ -> RawResult
end.