Skip to content

Commit

Permalink
Merge pull request #18 from imclerran/prefab-tools
Browse files Browse the repository at this point in the history
WIP: Add prefabricated tools (Toolkit)
  • Loading branch information
imclerran authored Sep 27, 2024
2 parents 436557e + 13eecee commit a43c45a
Show file tree
Hide file tree
Showing 12 changed files with 729 additions and 146 deletions.
138 changes: 17 additions & 121 deletions examples/tools.roc
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
app [main] {
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.10.2/FH4N0Sw-JSFXJfG3j54VEDPtXOoN-6I9v_IA8S18IGk.tar.br",
iso: "https://github.com/imclerran/roc-isodate/releases/download/v0.5.0/ptg0ElRLlIqsxMDZTTvQHgUSkNrUSymQaGwTfv0UEmk.tar.br",
ansi: "https://github.com/lukewilliamboswell/roc-ansi/releases/download/0.6/tzLZlg6lNmgU4uYtQ_zCmSr1AptHxZ8VBfE-O9JCudw.tar.br",
ai: "../package/main.roc",
}
Expand All @@ -10,20 +8,28 @@ import cli.Stdout
import cli.Stdin
import cli.Http
import cli.Env
import cli.Utc

import ai.Chat exposing [Client, Message]
import ai.Tools { sendHttpReq: Http.send } exposing [Tool]
import ai.Chat exposing [Message]
import ai.Tools { sendHttpReq: Http.send }
import ai.Toolkit.Serper { sendHttpReq: Http.send, getEnvVar: Env.var } exposing [serper]
import ansi.Core as Ansi
import iso.DateTime
import json.Json

main : Task {} _
main =
apiKey = getApiKey!
client = Chat.initClient { apiKey, model: "openai/gpt-4", tools: [utcNowTool, toCstTool, toCdtTool, serperTool] }
Stdout.line! ("Assistant: Ask me about the time, or anything on the web!\n" |> Ansi.color { fg: Standard Cyan })
Task.loop! { client, previousMessages: [] } loop
client = Chat.initClient { apiKey, model: "openai/gpt-4o", tools: [serper.tool] }
Stdout.line! ("Assistant: Ask me about the weather, or anything on the web!\n" |> Ansi.color { fg: Standard Cyan })
Task.loop! { previousMessages: [] } \{ previousMessages } -> ## Task.loop function must be inline due to roc issue #7116
Stdout.write! "You: "
query = Stdin.line!
when query is
"goodbye" | "quit" | "exit" -> Task.ok (Done {})
_ ->
messages = Chat.appendUserMessage previousMessages query
response = Http.send (Chat.buildHttpRequest client messages {}) |> Task.result!
updatedMessages = getMessagesFromResponse messages response |> Tools.handleToolCalls! client toolHandlerMap
printLastMessage! updatedMessages
Task.ok (Step { previousMessages: updatedMessages })

## Get the API key from the environmental variable
getApiKey : Task Str _
Expand All @@ -33,20 +39,6 @@ getApiKey =
Ok key -> Task.ok key
Err VarNotFound -> crash "OPENROUTER_API_KEY environment variable not set"

## The main loop of the program
loop : { client : Client, previousMessages : List Message } -> Task [Done {}, Step _] _
loop = \{ client, previousMessages } ->
Stdout.write! "You: "
query = Stdin.line!
when query is
"goodbye" | "quit" | "exit" -> Task.ok (Done {})
_ ->
messages = Chat.appendUserMessage previousMessages query
response = Http.send (Chat.buildHttpRequest client messages {}) |> Task.result!
updatedMessages = getMessagesFromResponse messages response |> Tools.handleToolCalls! client toolHandlerMap
printLastMessage! updatedMessages
Task.ok (Step { client, previousMessages: updatedMessages })

# Print the last message in the list of messages. Will only print assistant and system messages.
printLastMessage : List Message -> Task {} _
printLastMessage = \messages ->
Expand Down Expand Up @@ -74,102 +66,6 @@ getMessagesFromResponse = \messages, responseRes ->
Err (HttpErr err) ->
Chat.appendSystemMessage messages (Http.errorToString err)

## tool for the utcNow function
utcNowTool : Tool
utcNowTool = Tools.buildTool "utcNow" "Get the current UTC time as an ISO 8601 string" []

## Handler for the utcNow tool
utcNow : Str -> Task Str _
utcNow = \_args ->
Utc.now! {}
|> Utc.toNanosSinceEpoch
|> DateTime.fromNanosSinceEpoch
|> DateTime.toIsoStr
|> Task.ok

## tool for the toCdt function
toCdtTool : Tool
toCdtTool =
utcTimeParam = {
name: "utcTime",
type: "string",
description: "An ISO 8601 formatted time to convert from UTC to CDT",
required: Bool.true,
}
Tools.buildTool "toCdt" "Convert a UTC time to a CDT time" [utcTimeParam]

## Handler for the toCdt tool
toCdt : Str -> Task Str _
toCdt = \args ->
{ utcTime } =
args
|> Str.toUtf8
|> Decode.fromBytes Json.utf8
|> Task.fromResult!
utcTime
|> DateTime.fromIsoStr
|> Task.fromResult!
|> DateTime.addHours -5
|> DateTime.toIsoStr
|> Task.ok

## tool for the toCst function
toCstTool : Tool
toCstTool =
utcTimeParam = {
name: "utcTime",
type: "string",
description: "An ISO 8601 formatted time to convert from UTC to CST",
required: Bool.true,
}
Tools.buildTool "toCst" "Convert a UTC time to a CST time" [utcTimeParam]

## Handler for the toCst tool
toCst : Str -> Task Str _
toCst = \args ->
{ utcTime } =
args
|> Str.toUtf8
|> Decode.fromBytes Json.utf8
|> Task.fromResult!
utcTime
|> DateTime.fromIsoStr
|> Task.fromResult!
|> DateTime.addHours -6
|> DateTime.toIsoStr
|> Task.ok

serperTool =
queryParam = {
name: "q",
type: "string",
description: "The search query to send to the serper.dev API",
required: Bool.true,
}
Tools.buildTool "serper" "Access to the serper.dev google search API" [queryParam]

serper : Str -> Task Str _
serper = \args ->
apiKey = Env.var! "SERPER_API_KEY"
request = {
method: Post,
headers: [{ key: "X-API-KEY", value: apiKey }],
url: "https://google.serper.dev/search",
mimeType: "application/json",
body: args |> Str.toUtf8,
timeout: NoTimeout,
}
when Http.send request |> Task.result! is
Ok response ->
response.body
|> Str.fromUtf8
|> Result.withDefault "Failed to decode API response"
|> Task.ok
Err _ ->
"Failed to get response from serper.dev"
|> Task.ok

## Map of tool names to tool handlers
toolHandlerMap : Dict Str (Str -> Task Str _)
toolHandlerMap =
Dict.fromList [("utcNow", utcNow), ("toCdt", toCdt), ("toCst", toCst), ("serper", serper)]
toolHandlerMap = Dict.fromList [(serper.name, serper.handler)]
22 changes: 22 additions & 0 deletions package/InternalTools.roc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module [
ToolChoice,
injectTools,
injectToolChoice,
buildTool,
]

import json.Json
Expand Down Expand Up @@ -91,3 +92,24 @@ propertiesToJson = \properties ->
"""
|> Str.joinWith ", "
|> \dictContent -> "{$(dictContent)}"

buildTool : Str, Str, List { name : Str, type : Str, description : Str, required : Bool } -> Tool
buildTool = \name, description, parameters ->
properties =
parameters
|> List.map \{ name: n, type: t, description: d } -> (n, { type: t, description: d })
|> Dict.fromList
{
type: "function",
function: {
name,
description,
parameters: {
type: "object",
properties,
},
required: parameters
|> List.dropIf \param -> !param.required
|> List.map \param -> param.name,
},
}
60 changes: 59 additions & 1 deletion package/Shared.roc
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
module [ApiError, TimeoutConfig, ErrorResponse, RequestObject, ResponseFormat, dropLeadingGarbage, decodeErrorResponse, optionToStr, optionToList]
module [
ApiError,
TimeoutConfig,
ErrorResponse,
RequestObject,
ResponseFormat,
dropLeadingGarbage,
decodeErrorResponse,
optionToStr,
optionToList,
urlEncode,
]

import json.Json
import json.Option exposing [Option]
Expand Down Expand Up @@ -59,3 +70,50 @@ optionToList = \opt ->
when Option.get opt is
Some list -> list
None -> []

urlEncode : Str -> Str
urlEncode = \str ->
str
|> Str.toUtf8
|> List.map \char ->
Dict.get urlEncodeDict char
|> Result.withDefault
([char] |> Str.fromUtf8 |> Result.withDefault "")
|> Str.joinWith ""

urlEncodeDict : Dict U8 Str
urlEncodeDict =
Dict.empty {}
|> Dict.insert ' ' "%20"
|> Dict.insert '!' "%21"
|> Dict.insert '"' "%22"
|> Dict.insert '#' "%23"
|> Dict.insert '$' "%24"
|> Dict.insert '%' "%25"
|> Dict.insert '&' "%26"
|> Dict.insert '\'' "%27"
|> Dict.insert '(' "%28"
|> Dict.insert ')' "%29"
|> Dict.insert '*' "%2A"
|> Dict.insert '+' "%2B"
|> Dict.insert ',' "%2C"
|> Dict.insert '-' "%2D"
|> Dict.insert '.' "%2E"
|> Dict.insert '/' "%2F"
|> Dict.insert ':' "%3A"
|> Dict.insert ';' "%3B"
|> Dict.insert '<' "%3C"
|> Dict.insert '=' "%3D"
|> Dict.insert '>' "%3E"
|> Dict.insert '?' "%3F"
|> Dict.insert '@' "%40"
|> Dict.insert '[' "%5B"
|> Dict.insert '\\' "%5C"
|> Dict.insert ']' "%5D"
|> Dict.insert '^' "%5E"
|> Dict.insert '_' "%5F"
|> Dict.insert '`' "%60"
|> Dict.insert '{' "%7B"
|> Dict.insert '|' "%7C"
|> Dict.insert '}' "%7D"
|> Dict.insert '~' "%7E"
Loading

0 comments on commit a43c45a

Please sign in to comment.