-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 ).