snapkitty
developer-tools
c++
hyperkitty-chromium / src /hk_api_agents_h.erl
SNAPKITTYWEST's picture
Add covenant SPDX headers to every remaining unmarked source file
0196580 verified
Raw History Blame Contribute Delete
6.32 kB
% SPDX-License-Identifier: LicenseRef-Sovereign-Leviathan-SL-AGPL3-001
% Licensing: see LICENSE in the repository root.
%%%-------------------------------------------------------------------
%%% @doc `/agents' API domain.
%%%
%%% POST /api/agents create an agent
%%% GET /api/agents list agents
%%% GET /api/agents/:id describe one agent
%%% POST /api/agents/:id/objective assign its objective
%%% POST /api/agents/:id/actions run one loop iteration (a tool call)
%%% POST /api/agents/:id/complete mark completed
%%% POST /api/agents/:id/terminate terminate
%%%
%%% Every POST here goes through `hk_api_util:run_operation/4', so
%%% every mutating call returns an `operation_id' alongside its
%%% result -- including a failed one, per the "every mutating
%%% operation should return an operation identifier" requirement
%%% (an operation identifier is returned whether the operation
%%% succeeded or not; only its `status' differs).
%%% @end
%%%-------------------------------------------------------------------
-module(hk_api_agents_h).
-export([init/2]).
init(Req0 = #{method := <<"POST">>}, collection) ->
RequestId = hk_api_util:request_id(Req0),
{ok, Body, Req1} = hk_api_util:read_json_body(Req0),
Capabilities = parse_capabilities(maps:get(<<"capabilities">>, Body, [])),
Limits = maps:get(<<"limits">>, Body, #{}),
Req = hk_api_util:run_operation(agent_create, RequestId, fun() ->
AgentId = hk_id:new(<<"agent">>),
case hk_agent_fsm_sup:start_agent(#{agent_id => AgentId, capabilities => Capabilities,
limits => atomize_limit_keys(Limits)}) of
{ok, _Pid} -> {ok, {agent, AgentId}, #{agent_id => AgentId}};
{error, Reason} -> {error, Reason}
end
end, Req1),
{ok, Req, collection};
init(Req0 = #{method := <<"GET">>}, collection) ->
Described = [describe_or_skip(Id) || {Id, _Pid} <- hk_agent_registry:list_agents()],
Agents = [M || {true, M} <- Described],
Req = hk_api_util:reply_json(200, #{agents => Agents}, Req0),
{ok, Req, collection};
init(Req0 = #{method := <<"GET">>}, item) ->
AgentId = cowboy_req:binding(id, Req0),
Req = case lookup_agent(AgentId) of
{ok, Pid} ->
{ok, Agent} = hk_agent_fsm:describe(Pid),
hk_api_util:reply_json(200, hk_schema:to_map(Agent), Req0);
{error, not_found} ->
hk_api_util:reply_error(404, agent_not_found, Req0)
end,
{ok, Req, item};
init(Req0 = #{method := <<"POST">>}, objective) ->
AgentId = cowboy_req:binding(id, Req0),
RequestId = hk_api_util:request_id(Req0),
{ok, Body, Req1} = hk_api_util:read_json_body(Req0),
Description = maps:get(<<"description">>, Body, <<>>),
Req = hk_api_util:run_operation(agent_assign_objective, RequestId, fun() ->
case lookup_agent(AgentId) of
{ok, Pid} ->
case hk_agent_fsm:assign_objective(Pid, Description) of
ok -> {ok, {agent, AgentId}, #{agent_id => AgentId}};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end
end, Req1),
{ok, Req, objective};
init(Req0 = #{method := <<"POST">>}, actions) ->
AgentId = cowboy_req:binding(id, Req0),
RequestId = hk_api_util:request_id(Req0),
{ok, Body, Req1} = hk_api_util:read_json_body(Req0),
Tool = binary_to_atom(maps:get(<<"tool">>, Body, <<"browser">>), utf8),
Op = binary_to_atom(maps:get(<<"op">>, Body, <<"read_page">>), utf8),
Args = atomize_arg_keys(maps:get(<<"args">>, Body, #{})),
Req = hk_api_util:run_operation(agent_action, RequestId, fun() ->
case lookup_agent(AgentId) of
{ok, Pid} ->
case hk_agent_fsm:next_action(Pid, Tool, Op, Args) of
{ok, Result} -> {ok, {agent, AgentId}, #{observation => Result}};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end
end, Req1),
{ok, Req, actions};
init(Req0 = #{method := <<"POST">>}, complete) ->
AgentId = cowboy_req:binding(id, Req0),
RequestId = hk_api_util:request_id(Req0),
{ok, Body, Req1} = hk_api_util:read_json_body(Req0),
Result = maps:get(<<"result">>, Body, null),
Req = hk_api_util:run_operation(agent_complete, RequestId, fun() ->
case lookup_agent(AgentId) of
{ok, Pid} -> ok = hk_agent_fsm:complete(Pid, Result), {ok, {agent, AgentId}, #{}};
{error, Reason} -> {error, Reason}
end
end, Req1),
{ok, Req, complete};
init(Req0 = #{method := <<"POST">>}, terminate) ->
AgentId = cowboy_req:binding(id, Req0),
RequestId = hk_api_util:request_id(Req0),
{ok, Body, Req1} = hk_api_util:read_json_body(Req0),
Reason0 = maps:get(<<"reason">>, Body, <<"operator_requested">>),
Req = hk_api_util:run_operation(agent_terminate, RequestId, fun() ->
case lookup_agent(AgentId) of
{ok, Pid} -> ok = hk_agent_fsm:terminate_agent(Pid, Reason0), {ok, {agent, AgentId}, #{}};
{error, Reason} -> {error, Reason}
end
end, Req1),
{ok, Req, terminate};
init(Req0, State) ->
Req = hk_api_util:reply_error(405, method_not_allowed, Req0),
{ok, Req, State}.
%%% internal
lookup_agent(undefined) -> {error, missing_agent_id};
lookup_agent(AgentId) -> hk_agent_registry:lookup(AgentId).
describe_or_skip(AgentId) ->
case lookup_agent(AgentId) of
{ok, Pid} ->
case catch hk_agent_fsm:describe(Pid) of
{ok, Agent} -> {true, hk_schema:to_map(Agent)};
_ -> false
end;
_ -> false
end.
parse_capabilities(List) when is_list(List) ->
[binary_to_atom(C, utf8) || C <- List, is_binary(C)];
parse_capabilities(_) ->
[].
atomize_limit_keys(Map) when is_map(Map) ->
maps:fold(fun(K, V, Acc) -> Acc#{binary_to_atom(K, utf8) => V} end, #{}, Map);
atomize_limit_keys(_) -> #{}.
atomize_arg_keys(Map) when is_map(Map) ->
maps:fold(fun(K, V, Acc) -> Acc#{binary_to_atom(K, utf8) => V} end, #{}, Map);
atomize_arg_keys(_) -> #{}.