Skip to content

Commit 6275900

Browse files
committed
Add tests about tool calling
1 parent 141cf68 commit 6275900

File tree

3 files changed

+105
-32
lines changed

3 files changed

+105
-32
lines changed

src/voice_fn/processors/llm_context_aggregator.clj

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,38 @@ S: Start, E: End, T: Transcription, I: Interim, X: Text
272272
[frame]
273273
(-> frame :frame/data :messages last :tool_calls first))
274274

275+
(defn assistant-aggregator-init
276+
[{:llm/keys [registered-tools] :as args}]
277+
(let [tool-read (a/chan 100)
278+
tool-write (a/chan 100)
279+
tool-call-loop #(loop []
280+
(when-let [frame (a/<!! tool-write)]
281+
(assert (frame/llm-context? frame) "Tool caller accepts only llm-context frames")
282+
(let [tool-call (context->tool-call frame)
283+
tool-id (:id tool-call)
284+
fname (get-in tool-call [:function :name])
285+
args (u/parse-if-json (get-in tool-call [:function :arguments]))
286+
rt (get registered-tools fname)
287+
f (:tool rt)
288+
async? (:async? rt)]
289+
(t/log! {:id :tool-caller :level :debug} ["Got tool-call-request" tool-call])
290+
(if (fn? f)
291+
(let [tool-result (if async? (a/<!! (f args)) (f args))]
292+
(a/>!! tool-read (frame/llm-tool-call-result
293+
{:role :tool
294+
:content [{:type :text
295+
:text (u/json-str tool-result)}]
296+
:tool_call_id tool-id})))
297+
(a/>!! tool-read (frame/llm-tool-call-result
298+
{:role :tool
299+
:content [{:type :text
300+
:text "Tool not found"}]
301+
:tool_call_id tool-id}))))
302+
(recur)))]
303+
((flow/futurize tool-call-loop :exec :io))
304+
(merge args {::flow/in-ports {:tool-read tool-read}
305+
::flow/out-ports {:tool-write tool-write}})))
306+
275307
(def assistant-context-aggregator
276308
"Takes streaming tool-call request tokens and returns a new context with the
277309
tool call result if a tool registered is available."
@@ -290,34 +322,5 @@ S: Start, E: End, T: Transcription, I: Interim, X: Text
290322
should return a channel on which the invocation result will
291323
be put."
292324
:flow/handles-interrupt? "Wether the flow handles user interruptions. Default false"}})
293-
:init (fn [{:llm/keys [registered-tools] :as args}]
294-
(let [tool-read (a/chan 100)
295-
tool-write (a/chan 100)
296-
tool-call-loop #(loop []
297-
(when-let [frame (a/<!! tool-write)]
298-
(assert (frame/llm-context? frame) "Tool caller accepts only llm-context frames")
299-
(let [tool-call (context->tool-call frame)
300-
tool-id (:id tool-call)
301-
fname (get-in tool-call [:function :name])
302-
args (u/parse-if-json (get-in tool-call [:function :arguments]))
303-
rt (get registered-tools fname)
304-
f (:tool rt)
305-
async? (:async? rt)]
306-
(t/log! {:id :tool-caller :level :debug} ["Got tool-call-request" tool-call])
307-
(if (fn? f)
308-
(let [tool-result (if async? (a/<!! (f args)) (f args))]
309-
(a/>!! tool-read (frame/llm-tool-call-result
310-
{:role :tool
311-
:content [{:type :text
312-
:text (u/json-str tool-result)}]
313-
:tool_call_id tool-id})))
314-
(a/>!! tool-read (frame/llm-tool-call-result
315-
{:role :tool
316-
:content [{:type :text
317-
:text "Tool not found"}]
318-
:tool_call_id tool-id}))))
319-
(recur)))]
320-
((flow/futurize tool-call-loop :exec :io))
321-
(merge args {::flow/in-ports {:tool-read tool-read}
322-
::flow/out-ports {:tool-write tool-write}})))
325+
:init assistant-aggregator-init
323326
:transform assistant-aggregator-transform}))

test/voice_fn/mock_data.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
:streamSid "MZ48f5da1eae19d83dcc3782e4ed89a45d"
123123
:start
124124
{:streamSid "MZ48f5da1eae19d83dcc3782e4ed89a45d"
125+
:callSid "CAe692ad978bea6b203c130f0071931234"
125126
:customParameters
126127
{:last-bill "100"
127128
:address "Hello World"
@@ -131,6 +132,5 @@
131132
:tracks ["inbound"]
132133
:accountSid "hello123"
133134
:mediaFormat
134-
{:encoding "audio/x-mulaw", :channels 1, :sampleRate 8000}
135-
:callSid "CAe692ad978bea6b203c130f0071931234"}
135+
{:encoding "audio/x-mulaw", :channels 1, :sampleRate 8000}}
136136
:event "start"})

test/voice_fn/processors/llm_context_aggregator_test.clj

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
(ns voice-fn.processors.llm-context-aggregator-test
22
(:require
3+
[clojure.core.async :as a]
4+
[clojure.core.async.flow :as flow]
35
[midje.sweet :refer [fact facts]]
46
[voice-fn.frame :as frame]
57
[voice-fn.mock-data :as mock]
@@ -324,3 +326,71 @@
324326
sstate :sys-in
325327
(frame/system-config-change
326328
{:llm/context nc})) => [(assoc sstate :llm/context nc)]))))
329+
330+
(facts
331+
"About the tool calling loop"
332+
(let [call-id "test-call-id"
333+
llm-context {:messages [{:role "system"
334+
:content "You are a voice agent operating via phone. Be concise. The input you receive comes from a speech-to-text (transcription) system that isn't always efficient and may send unclear text. Ask for clarification when you're unsure what the person said."}]
335+
:tools [{:type :function
336+
:function
337+
{:name "get_weather"
338+
:description "Get the current weather of a location"
339+
:parameters {:type :object
340+
:required [:town]
341+
:properties {:town {:type :string
342+
:description "Town for which to retrieve the current weather"}}
343+
:additionalProperties false}
344+
:strict true}}
345+
{:type "function"
346+
:function
347+
{:name "end_call"
348+
:description "End the current call"
349+
:parameters {:type "object"
350+
:required []
351+
:properties {}
352+
:additionalProperties false}}}]}
353+
registered-tools {"get_weather" {:async false
354+
:tool (fn [{:keys [town]}] (str "The weather in " town " is 17 degrees celsius"))}
355+
"end_call" {:async true
356+
:tool (fn [_]
357+
(a/<!! (a/timeout 300))
358+
(str "Call with id " call-id " has ended"))}}
359+
{::flow/keys [in-ports out-ports]} (sut/assistant-aggregator-init {:llm/registered-tools registered-tools})
360+
tool-read (:tool-read in-ports)
361+
tool-write (:tool-write out-ports)
362+
async-context (frame/llm-context {:messages [{:content "You are a helpful assistant"
363+
:role "assistant"}
364+
{:content "Hello there" :role "user"}
365+
{:role :assistant
366+
:tool_calls [{:function {:arguments "{}"
367+
:name "end_call"}
368+
:id "call_J9MSffmnxdPj8r28tNzCO8qj"
369+
:type :function}]}]})
370+
sync-context (frame/llm-context {:messages [{:content "You are a helpful assistant"
371+
:role "assistant"}
372+
{:content "Hello there" :role "user"}
373+
{:role :assistant
374+
:tool_calls [{:id "call_LCEOwyJ6wsqC5rzJRH0uMnR8"
375+
:type :function
376+
:function {:name "get_weather", :arguments "{\"town\":\"New York\"}"}}]}]})]
377+
(fact
378+
"Handles async calls correctly"
379+
(a/>!! tool-write async-context)
380+
(let [res (a/<!! tool-read)]
381+
(frame/llm-tool-call-result? res) => true
382+
(:frame/data res) => {:content [{:text "Call with id test-call-id has ended"
383+
:type :text}]
384+
:role :tool
385+
:tool_call_id "call_J9MSffmnxdPj8r28tNzCO8qj"}))
386+
387+
(fact
388+
"Handles sync calls correctly"
389+
(a/>!! tool-write sync-context)
390+
(let [res (a/<!! tool-read)]
391+
(frame/llm-tool-call-result? res) => true
392+
(:frame/data res) => {:content [{:text "The weather in New York is 17 degrees celsius" :type :text}]
393+
:role :tool
394+
:tool_call_id "call_LCEOwyJ6wsqC5rzJRH0uMnR8"}))
395+
(a/close! tool-read)
396+
(a/close! tool-write)))

0 commit comments

Comments
 (0)