Current section
Files
Jump to
Current section
Files
src/starlet@tool.erl
-module(starlet@tool).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/starlet/tool.gleam").
-export([function/3, success/2, error/2, dispatch/1, dynamic_handler/2, handler/3, dynamic_to_json/1, to_string/1]).
-export_type([definition/0, call/0, tool_result/0, tool_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Tool definitions and dispatch helpers for function calling.\n"
"\n"
" ## Defining Tools\n"
"\n"
" ```gleam\n"
" import gleam/json\n"
" import starlet/tool\n"
"\n"
" let weather_tool =\n"
" tool.function(\n"
" name: \"get_weather\",\n"
" description: \"Get current weather for a city\",\n"
" parameters: json.object([\n"
" #(\"type\", json.string(\"object\")),\n"
" #(\"properties\", json.object([\n"
" #(\"city\", json.object([#(\"type\", json.string(\"string\"))])),\n"
" ])),\n"
" ]),\n"
" )\n"
" ```\n"
"\n"
" ## Handling Tool Calls\n"
"\n"
" ```gleam\n"
" let city_decoder = {\n"
" use city <- decode.field(\"city\", decode.string)\n"
" decode.success(city)\n"
" }\n"
"\n"
" let dispatcher = tool.dispatch([\n"
" tool.handler(\"get_weather\", city_decoder, fn(city) {\n"
" let temp = case city {\n"
" \"Tokyo\" -> 18\n"
" _ -> 22\n"
" }\n"
" Ok(json.object([#(\"temp\", json.int(temp))]))\n"
" }),\n"
" ])\n"
" ```\n"
).
-type definition() :: {function, binary(), binary(), gleam@json:json()}.
-type call() :: {call, binary(), binary(), gleam@dynamic:dynamic_()}.
-type tool_result() :: {tool_result, binary(), binary(), gleam@json:json()}.
-type tool_error() :: {not_found, binary()} |
{invalid_arguments, binary()} |
{execution_failed, binary()}.
-file("src/starlet/tool.gleam", 77).
?DOC(" Create a function tool definition.\n").
-spec function(binary(), binary(), gleam@json:json()) -> definition().
function(Name, Description, Parameters) ->
{function, Name, Description, Parameters}.
-file("src/starlet/tool.gleam", 86).
?DOC(" Create a successful tool result from a call.\n").
-spec success(call(), gleam@json:json()) -> tool_result().
success(Call, Output) ->
{tool_result, erlang:element(2, Call), erlang:element(3, Call), Output}.
-file("src/starlet/tool.gleam", 91).
?DOC(" Create an error result (encoded as JSON with error message).\n").
-spec error(call(), binary()) -> tool_result().
error(Call, Message) ->
{tool_result,
erlang:element(2, Call),
erlang:element(3, Call),
gleam@json:object([{<<"error"/utf8>>, gleam@json:string(Message)}])}.
-file("src/starlet/tool.gleam", 100).
?DOC(" Create a dispatcher that routes calls to the right handler by name.\n").
-spec dispatch(
list({binary(),
fun((call()) -> {ok, tool_result()} | {error, tool_error()})})
) -> fun((call()) -> {ok, tool_result()} | {error, tool_error()}).
dispatch(Handlers) ->
fun(Call) ->
Maybe_tool_call = gleam@list:find(
Handlers,
fun(H) ->
{Name, _} = H,
Name =:= erlang:element(3, Call)
end
),
case Maybe_tool_call of
{ok, {_, Handle}} ->
Handle(Call);
{error, _} ->
{error, {not_found, erlang:element(3, Call)}}
end
end.
-file("src/starlet/tool.gleam", 123).
?DOC(
" Create a handler tuple from name and a function that receives Dynamic arguments.\n"
" For most cases, prefer `handler` which provides automatic argument decoding.\n"
).
-spec dynamic_handler(
binary(),
fun((gleam@dynamic:dynamic_()) -> {ok, gleam@json:json()} |
{error, tool_error()})
) -> {binary(), fun((call()) -> {ok, tool_result()} | {error, tool_error()})}.
dynamic_handler(Name, Run) ->
{Name, fun(Call) -> case Run(erlang:element(4, Call)) of
{ok, Output} ->
{ok, success(Call, Output)};
{error, E} ->
{error, E}
end end}.
-file("src/starlet/tool.gleam", 149).
?DOC(
" Create a handler tuple that automatically decodes arguments to a typed value.\n"
"\n"
" Example:\n"
" ```gleam\n"
" let decoder = {\n"
" use city <- decode.field(\"city\", decode.string)\n"
" decode.success(city)\n"
" }\n"
"\n"
" let #(name, run) =\n"
" tool.handler(\"get_weather\", decoder, fn(city) {\n"
" Ok(json.string(\"Weather in \" <> city))\n"
" })\n"
" ```\n"
).
-spec handler(
binary(),
gleam@dynamic@decode:decoder(OOA),
fun((OOA) -> {ok, gleam@json:json()} | {error, tool_error()})
) -> {binary(), fun((call()) -> {ok, tool_result()} | {error, tool_error()})}.
handler(Name, Decoder, Run) ->
{Name,
fun(Call) ->
gleam@result:'try'(
begin
_pipe = gleam@dynamic@decode:run(
erlang:element(4, Call),
Decoder
),
gleam@result:map_error(
_pipe,
fun(Errors) ->
{invalid_arguments,
<<"Failed to decode: "/utf8,
(gleam@string:inspect(Errors))/binary>>}
end
)
end,
fun(Args) -> case Run(Args) of
{ok, Output} ->
{ok, success(Call, Output)};
{error, E} ->
{error, E}
end end
)
end}.
-file("src/starlet/tool.gleam", 189).
-spec decode_null() -> gleam@dynamic@decode:decoder(gleam@json:json()).
decode_null() ->
gleam@dynamic@decode:new_primitive_decoder(
<<"null"/utf8>>,
fun(Dyn) -> case gleam_stdlib:classify_dynamic(Dyn) of
<<"Nil"/utf8>> ->
{ok, gleam@json:null()};
_ ->
{error, gleam@json:null()}
end end
).
-file("src/starlet/tool.gleam", 198).
-spec decode_list() -> gleam@dynamic@decode:decoder(gleam@json:json()).
decode_list() ->
gleam@dynamic@decode:then(
gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(Items) ->
gleam@dynamic@decode:success(
gleam@json:array(
gleam@list:map(Items, fun dynamic_to_json/1),
fun(X) -> X end
)
)
end
).
-file("src/starlet/tool.gleam", 172).
?DOC(false).
-spec dynamic_to_json(gleam@dynamic:dynamic_()) -> gleam@json:json().
dynamic_to_json(Dyn) ->
Decoder = gleam@dynamic@decode:one_of(
begin
_pipe = {decoder, fun gleam@dynamic@decode:decode_string/1},
gleam@dynamic@decode:map(_pipe, fun gleam@json:string/1)
end,
[begin
_pipe@1 = {decoder, fun gleam@dynamic@decode:decode_int/1},
gleam@dynamic@decode:map(_pipe@1, fun gleam@json:int/1)
end,
begin
_pipe@2 = {decoder, fun gleam@dynamic@decode:decode_float/1},
gleam@dynamic@decode:map(_pipe@2, fun gleam@json:float/1)
end,
begin
_pipe@3 = {decoder, fun gleam@dynamic@decode:decode_bool/1},
gleam@dynamic@decode:map(_pipe@3, fun gleam@json:bool/1)
end,
decode_null(),
decode_list(),
decode_object()]
),
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, J} ->
J;
{error, _} ->
gleam@json:null()
end.
-file("src/starlet/tool.gleam", 116).
?DOC(
" Formats a tool call for display, e.g. `get_weather({\"city\":\"Paris\"})`.\n"
" Useful for logging and debugging.\n"
).
-spec to_string(call()) -> binary().
to_string(Call) ->
Args = gleam@json:to_string(dynamic_to_json(erlang:element(4, Call))),
<<<<<<(erlang:element(3, Call))/binary, "("/utf8>>/binary, Args/binary>>/binary,
")"/utf8>>.
-file("src/starlet/tool.gleam", 203).
-spec decode_object() -> gleam@dynamic@decode:decoder(gleam@json:json()).
decode_object() ->
gleam@dynamic@decode:then(
gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(D) ->
Pairs = begin
_pipe = maps:to_list(D),
gleam@list:map(
_pipe,
fun(P) ->
{K, V} = P,
{K, dynamic_to_json(V)}
end
)
end,
gleam@dynamic@decode:success(gleam@json:object(Pairs))
end
).