Client Library for the Google Gemini LLM APIs
While the Google Gemini APIs offer a compelling suite of advantages for developers seeking to integrate cutting-edge, multimodal AI capabilities into their applications. A primary benefit is the large one million token context size and very fast inference speeds. Gemini is very cost effective for natural language processing tasks such as text summarization, question answering, code generation, creative content creation, and conversational AI.
Note: March 22, 2026 addition: new section on Gemini Interaction APIs to blend local tools and Google’s built in tools for search, etc. New material is at the end of this chapter.
The source code for this Gemini library is in my GitHub repository https://github.com/mark-watson/gemini. As usual you want to git clone this repository in your local directory ~/quicklisp/local-projects/ so Quicklisp can find this library with (ql:quickload :gemini). We will list the code below and then look at example use.
Relationship to the litelm library
The litelm routing library (in loving-common-lisp/src/litelm) already covers ordinary Gemini text generation and embeddings: it routes a "gemini/..." model string to Google’s OpenAI-compatible endpoints, so plain generation is just (litelm:completion "gemini/gemini-3-flash-preview" :messages prompt).
This chapter’s library is therefore not redundant with litelm. It exists for the Gemini features that are native to Google’s API, and so fall outside the OpenAI-compatible surface litelm speaks:
- Google Search grounding (
generate-with-search) and the citations returned with it (generate-with-search-and-citations) - Token counting (
count-tokens), via the native:countTokensaction - The Interactions API (
generate-with-tools,continue-with-function-responses), which mixes Google’s built-in search tool with client-side function calls across turns
Use litelm for ordinary generation and embeddings; use this library when you need search grounding, citations, token counts, or platform tools.
package.lisp
We need the function post in the external library dexador:
1 ;;;; package.lisp
2
3 (defpackage #:gemini
4 (:use #:cl)
5
6 (:export #:generate #:count-tokens #:generate-with-search #:generate-with-search-and-citations
7 #:make-function-declaration #:generate-with-tools #:continue-with-function-responses))
gemini.asd
1 ;;;; gemini.asd
2
3 (asdf:defsystem #:gemini
4 :description "Library for using the Google Gemini Interactions API"
5 :author "Mark Watson"
6 :license "Apache 2"
7 :depends-on (#:uiop #:cl-json #:alexandria #:dexador)
8 :components ((:file "package")
9 (:file "gemini")
10 (:file "gemini_interactions_api")))
gemini.lisp
This code defines functions for generating content and counting tokens. Rather than spawning an external curl process, it sends native HTTP POST requests with the Dexador client through the helper function %post-json.
The function google-api-key reads the GOOGLE_API_KEY environment variable each time it runs, unless you set *google-api-key* yourself. Reading the variable at call time, and not when the library loads, means a key that you set after loading the library (or in a saved Lisp image) still works, and a missing key signals a clear error.
The helper %generate-content calls the standard generateContent action at https://generativelanguage.googleapis.com/v1beta/models/<model>:generateContent. It sends a JSON payload whose contents array holds one content object containing a single parts entry with the prompt text, decodes the response, and returns the first candidate. The helper %candidate-text pulls the generated text out of that candidate’s first part, so generate is a one line function. Plain generation and the search grounded functions later in this chapter share these two helpers. The count-tokens function queries the companion countTokens action on the same models endpoint, using the same HTTP post helper.
1 (in-package #:gemini)
2
3 (defvar *google-api-key* nil
4 "Google API key. When NIL, the GOOGLE_API_KEY environment variable is read
5 at call time (so a key set after this library is loaded is still found).")
6
7 (defvar *model* "gemini-3-flash-preview") ;; model used in this file.
8
9 (defun google-api-key ()
10 "Return the Google API key, signaling an error if none is configured."
11 (or *google-api-key*
12 (uiop:getenv "GOOGLE_API_KEY")
13 (error "Set the GOOGLE_API_KEY environment variable (or gemini::*google-api-key*)")))
14
15 (defun %post-json (url headers payload-hash)
16 "Helper function to perform an HTTP POST request with a JSON payload using Dexador."
17 (let ((payload-json (cl-json:encode-json-to-string payload-hash)))
18 (dex:post url :headers (append headers '(("Accept-Encoding" . "identity")))
19 :content payload-json)))
20
21 ;;; ---- generateContent API ----
22
23 (defun %generate-content (prompt model-id &key google-search-p)
24 "POST PROMPT to the generateContent API for MODEL-ID, optionally enabling
25 Google Search grounding. Returns the first candidate of the decoded response."
26 (let* ((payload (make-hash-table :test 'equal))
27 (part-ht (make-hash-table :test 'equal))
28 (content-ht (make-hash-table :test 'equal)))
29 (setf (gethash "text" part-ht) prompt)
30 (setf (gethash "parts" content-ht) (list part-ht))
31 (setf (gethash "contents" payload) (list content-ht))
32 (when google-search-p
33 (let ((google-search-tool (make-hash-table :test 'equal)))
34 (setf (gethash "google_search" google-search-tool) (make-hash-table :test 'equal))
35 (setf (gethash "tools" payload) (list google-search-tool))))
36 (let* ((url (concatenate 'string
37 "https://generativelanguage.googleapis.com/v1beta/models/"
38 model-id ":generateContent"))
39 (headers (list '("Content-Type" . "application/json")
40 (cons "X-goog-api-key" (google-api-key))))
41 (response-string (%post-json url headers payload))
42 (decoded-response (cl-json:decode-json-from-string response-string)))
43 (first (cdr (assoc :CANDIDATES decoded-response))))))
44
45 (defun %candidate-text (candidate)
46 "Return the text of the first part of a generateContent CANDIDATE."
47 (let* ((content (cdr (assoc :CONTENT candidate)))
48 (parts (cdr (assoc :PARTS content))))
49 (cdr (assoc :TEXT (first parts)))))
50
51 (defun generate (prompt &optional (model-id *model*))
52 "Generates text from a given prompt using the Gemini generateContent API.
53 Uses *model* defined at the top of this file as default.
54 PROMPT: The text prompt to generate content from.
55 MODEL-ID: Optional. The ID of the model to use.
56 Returns the generated text as a string."
57 (%candidate-text (%generate-content prompt model-id)))
58
59 ;; (gemini:generate "In one sentence, explain how AI works to a child.")
60 ;; (gemini:generate "Write a short, four-line poem about coding in Python.")
61
62 (defun count-tokens (prompt &optional (model-id *model*))
63 "Counts the number of tokens for a given prompt and model.
64 Uses *model* defined at top of this file as default.
65 PROMPT: The text prompt to count tokens for.
66 MODEL-ID: Optional. The ID of the model to use.
67 Returns the total token count as an integer."
68 (let* ((api-url (concatenate 'string
69 "https://generativelanguage.googleapis.com/v1beta/models/"
70 model-id ":countTokens"))
71 (payload (make-hash-table :test 'equal)))
72 ;; Construct payload similar to generate function
73 (setf (gethash "contents" payload)
74 (list (let ((contents (make-hash-table :test 'equal)))
75 (setf (gethash "parts" contents)
76 (list (let ((part (make-hash-table :test 'equal)))
77 (setf (gethash "text" part) prompt)
78 part)))
79 contents)))
80 (let* ((headers (list '("Content-Type" . "application/json")
81 (cons "x-goog-api-key" (google-api-key))))
82 (response-string (%post-json api-url headers payload))
83 (decoded-response (cl-json:decode-json-from-string response-string))
84 (total-tokens-pair (assoc :TOTAL-TOKENS decoded-response)))
85 (if total-tokens-pair
86 (cdr total-tokens-pair)
87 (error
88 "Could not retrieve token count from API response: ~S"
89 decoded-response)))))
90
91 ;; (gemini:count-tokens "In one sentence, explain how AI works to a child.")
92
93 (defun run-tests ()
94 "Runs tests for generate and count-tokens functions."
95 (let* ((prompt "In one sentence, explain how AI works to a child.")
96 (generated-text (generate prompt))
97 (token-count (count-tokens prompt)))
98 (format t "Generated Text: ~A~%Token Count: ~A~%" generated-text token-count)))
Example Use
1 CL-USER 4 > (gemini:generate "In one sentence, explain how AI works to a child.")
2 "AI is like teaching a computer with lots and lots of examples, so it can learn to figure things out and act smart all by itself."
3
4 CL-USER 5 > (gemini:count-tokens "How many tokens is this sentence?")
5 7
Using Google’s “Grounding Search”
Google’s “Grounding with Google Search” connects the Gemini model to real-time web information. The model can then answer queries about current events, and basing answers on search results reduces “hallucinations.” The function generate-with-search asks the helper %generate-content to add a Google Search tool to the request. With this tool, the API runs a web search (for example, to look up the winner of a recent tournament like Euro 2024) and uses the results to write its answer, which the function returns as a string. The following code is near the bottom of the file gemini.lisp:
1 (defun generate-with-search (prompt &optional (model-id *model*))
2 "Generates text with Google Search grounding via the generateContent API."
3 (%candidate-text (%generate-content prompt model-id :google-search-p t)))
The work happens in the helper %generate-content that we saw in the last section. When we pass :google-search-p t, it adds a "tools" key to the payload holding a Google Search tool definition (a google_search key with an empty object as its value). This tool definition lets the model query Google’s search index before it writes an answer. This matters most for questions about current events, where the model’s static training data is out of date.
Because the request goes to the standard generateContent endpoint, the request body keeps the same contents and parts shape as plain generation, with the search tool added under "tools". The reply arrives in the usual candidates array and %candidate-text reads the text from the first candidate’s first part. Without the google_search tool, the model answers the same prompt from its static training data alone.
Here is example output:
1 $ sbcl
2 This is SBCL 2.5.10, an implementation of ANSI Common Lisp.
3 * (ql:quickload :gemini)
4 To load "gemini":
5 Load 1 ASDF system:
6 gemini
7 ; Loading "gemini"
8 nil
9 * (gemini:generate-with-search "What movies are playing in Flagstaff Arizona today at the Harkins Theater?")
10 "Today, **Thursday, December 18, 2025**, the following movies are playing at the **Harkins Flagstaff 16** theater.
11
12 Please note that many major releases, such as *Avatar: Fire and Ash*, are currently running early \"preview\" screenings ahead of their official opening tomorrow.
13
14 ### **Movies & Showtimes**
15
16 * **Avatar: Fire and Ash** (PG-13)
17 * **Ciné XL:** 2:00 PM, 6:15 PM, 10:30 PM
18 * **3D HFR:** 2:30 PM, 5:00 PM, 6:45 PM, 9:15 PM
19 * **Digital:** 3:00 PM, 4:00 PM, 7:15 PM, 8:15 PM, 9:30 PM
20 * **Five Nights at Freddy's 2** (PG-13)
21 * 11:30 AM, 12:30 PM, 2:05 PM, 4:50 PM, 7:35 PM, 10:20 PM
22 * **Zootopia 2** (PG)
23 * 1:45 PM, 5:10 PM, 7:25 PM, 8:10 PM
24 ...
25 "
Using Google’s “Grounding Search” With Citations
This example is also near the bottom of the file gemini.lisp and adds the retrieval and display of citations that consist of web sites searched. You can use the code in the web spidering chapter to fetch the text contents of these reference search results.
Here is the code:
1 (defun generate-with-search-and-citations (prompt &optional (model-id *model*))
2 "Generates text with Google Search grounding and returns citations via the generateContent API.
3 Returns two values: the response text and a list of (title . url) citation pairs."
4 (let* ((first-candidate (%generate-content prompt model-id :google-search-p t))
5 (text (%candidate-text first-candidate))
6 (grounding-metadata (cdr (assoc :GROUNDING-METADATA first-candidate)))
7 (grounding-chunks (cdr (assoc :GROUNDING-CHUNKS grounding-metadata)))
8 (citations (loop for chunk in grounding-chunks
9 for web = (cdr (assoc :WEB chunk))
10 when web
11 collect (cons (cdr (assoc :TITLE web))
12 (cdr (assoc :URI web))))))
13 (values text citations)))
The citations come from the groundingMetadata object in the first candidate. Note the keyword names in the code: when cl-json decodes a JSON object, it converts camel case keys like groundingMetadata and groundingChunks into the Lisp keywords :GROUNDING-METADATA and :GROUNDING-CHUNKS. Looking up :GROUNDINGMETADATA instead would silently return an empty list of citations.
Here is the sample output (output shortened: redirect URIs shortened for brevity):
1 * (multiple-value-bind (response sources)
2 (gemini:generate-with-search-and-citations "Who won the Super Bowl in 2024?")
3 (format t "Answer: ~a~%~%Sources:~%" response)
4 (loop for (title . url) in sources
5 do (format t "- ~a: ~a~%" title url)))
6
7 Answer: The Kansas City Chiefs won Super Bowl LVIII in 2024, defeating the San Francisco 49ers 25-22 in overtime. The game was held in Las Vegas on February 11, 2024. This victory marked the Chiefs' third Super Bowl title in five years and made them the first back-to-back NFL champions in almost 20 years. Patrick Mahomes, the Chiefs' quarterback, was named Super Bowl MVP for the third time.
8
9 Sources:
10 - olympics.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUjJ4MF1eTcc...
11 - kcur.org: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUOKyR9rZ...
12 - theguardian.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZItCsAVG...
13 - foxsports.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYrbCcX...
14 nil
15 *
Mixing Local Tools with Google Platform Tools Using the Interactions APIs.
The following diagram shows the high-level architecture of the Google Gemini API client library developed in this chapter:
The Google Interactions APIs provide a clean schema for managing multi-turn conversations and executing client-side tools. You can find more details in the official documentation here: https://ai.google.dev/gemini-api/docs/interactions.
The following Common Lisp implementation in the file gemini_interactions_api.lisp offers a robust framework for multi-turn conversations with tool orchestration. Unlike standard API generation endpoints where the developer has to manually accumulate and format the message history on the client side, the Interactions endpoint maintains the conversation state server-side.
When you call generate-with-tools on “Turn 1,” the API returns the model’s output alongside an interaction-id that references the server-side conversation state. If the model determines it needs to call a client-side function (tool), it returns a list of function calls. You execute those functions locally and pass the results to continue-with-function-responses on “Turn 2” alongside the same interaction-id. The server matches the results to the pending interaction context, allowing it to formulate the final answer.
1 (in-package #:gemini)
2
3 ;;; ====================================================================
4 ;;; Gemini Interactions API - Multi-turn conversations with tool use
5 ;;; ====================================================================
6 ;;;
7 ;;; Uses the v1beta Interactions API with the new steps schema.
8 ;;; Supports multi-turn conversations combining Google Search (built-in)
9 ;;; and custom function declarations (client-side tool use).
10 ;;;
11 ;;; Typical workflow:
12 ;;; 1. Build function declarations with MAKE-FUNCTION-DECLARATION
13 ;;; 2. Call GENERATE-WITH-TOOLS for Turn 1 -- returns TEXT, FUNCTION-CALLS, INTERACTION-ID
14 ;;; 3. Invoke the functions yourself and collect results
15 ;;; 4. Call CONTINUE-WITH-FUNCTION-RESPONSES for Turn 2 -- returns final TEXT
16 ;;; ====================================================================
17
18
19 ;;; ---- Endpoint ----
20
21 (defvar *interactions-api-url*
22 "https://generativelanguage.googleapis.com/v1beta/interactions")
23
24 ;;; ---- Internal utilities ----
25
26 (defun %make-tools-list (function-declarations &optional google-search-p)
27 "Builds the tools array for the Interactions API payload.
28 FUNCTION-DECLARATIONS: list of hash-tables from MAKE-FUNCTION-DECLARATION, or NIL.
29 GOOGLE-SEARCH-P: when T, includes the built-in Google Search tool."
30 (let ((tools '()))
31 (when function-declarations
32 (loop for fn-decl in function-declarations
33 do (let ((fn-tool (make-hash-table :test 'equal)))
34 (setf (gethash "type" fn-tool) "function"
35 (gethash "name" fn-tool) (gethash "name" fn-decl)
36 (gethash "description" fn-tool) (gethash "description" fn-decl)
37 (gethash "parameters" fn-tool) (gethash "parameters" fn-decl))
38 (push fn-tool tools))))
39 (when google-search-p
40 (let ((gs-tool (make-hash-table :test 'equal)))
41 (setf (gethash "type" gs-tool) "google_search")
42 (push gs-tool tools)))
43 (nreverse tools)))
44
45 (defun %extract-function-calls-from-steps (steps)
46 "Returns a list of plists (:NAME :ID :ARGS) for every function_call step.
47 ARGS is a cl-json decoded alist, e.g. ((:LOCATION . \"Barrow, AK\"))."
48 (loop for step in steps
49 when (string-equal (cdr (assoc :TYPE step)) "function_call")
50 collect (list :name (cdr (assoc :NAME step))
51 :id (cdr (assoc :ID step))
52 :args (cdr (assoc :ARGUMENTS step)))))
53
54 (defun %get-text-from-steps (steps)
55 "Returns the text from the last model_output step, or NIL."
56 (loop for step in (reverse steps)
57 when (string-equal (cdr (assoc :TYPE step)) "model_output")
58 return (let* ((content (cdr (assoc :CONTENT step)))
59 (first-content (first content)))
60 (cdr (assoc :TEXT first-content)))))
61
62
63 ;;; ---- Public API ----
64
65 (defun make-function-declaration (name description parameters &optional required-params)
66 "Creates a functionDeclaration hash-table suitable for GENERATE-WITH-TOOLS.
67
68 NAME: string -- the function name the model will invoke
69 DESCRIPTION: string -- natural-language description of what the function does
70 PARAMETERS: list of (param-name type description) triples, e.g.:
71 '((\"location\" \"STRING\" \"The city and state, e.g. San Francisco, CA\"))
72 REQUIRED-PARAMS: optional list of required parameter name strings, e.g.:
73 '(\"location\")
74
75 Example:
76 (make-function-declaration
77 \"getWeather\"
78 \"Get the weather in a given location\"
79 '((\"location\" \"STRING\" \"The city and state, e.g. San Francisco, CA\"))
80 '(\"location\"))"
81 (let ((decl-ht (make-hash-table :test 'equal))
82 (params-ht (make-hash-table :test 'equal))
83 (props-ht (make-hash-table :test 'equal)))
84 (dolist (param parameters)
85 (destructuring-bind (pname ptype pdesc) param
86 (let ((prop-ht (make-hash-table :test 'equal)))
87 (setf (gethash "type" prop-ht) ptype
88 (gethash "description" prop-ht) pdesc)
89 (setf (gethash pname props-ht) prop-ht))))
90 (setf (gethash "type" params-ht) "OBJECT"
91 (gethash "properties" params-ht) props-ht)
92 (when required-params
93 (setf (gethash "required" params-ht) required-params))
94 (setf (gethash "name" decl-ht) name
95 (gethash "description" decl-ht) description
96 (gethash "parameters" decl-ht) params-ht)
97 decl-ht))
98
99 (defun generate-with-tools (prompt function-declarations
100 &key (model-id *model*) google-search-p)
101 "Turn 1: Send PROMPT to the model with optional tool support via the Interactions API.
102
103 PROMPT: the user's text question.
104 FUNCTION-DECLARATIONS: list of hash-tables from MAKE-FUNCTION-DECLARATION, or NIL.
105 :GOOGLE-SEARCH-P: when T, enables the built-in Google Search tool.
106 :MODEL-ID: model to use (defaults to *model*).
107
108 Returns three values:
109 TEXT - model's text reply, or NIL when it chose to call functions.
110 FUNCTION-CALLS - list of plists (:NAME :ID :ARGS) for each function call made.
111 ARGS is a cl-json alist, e.g. ((:LOCATION . \"Barrow, AK\")).
112 INTERACTION-ID - the interaction ID for use in follow-up turns."
113 (let* ((payload (make-hash-table :test 'equal)))
114 (setf (gethash "model" payload) model-id
115 (gethash "input" payload) prompt)
116 (let ((tools (%make-tools-list function-declarations google-search-p)))
117 (when tools ; cl-json would encode an empty list as null
118 (setf (gethash "tools" payload) tools)))
119 (let* ((headers (list '("Content-Type" . "application/json")
120 (cons "x-goog-api-key" (google-api-key))
121 '("Api-Revision" . "2026-05-20")))
122 (response-string (%post-json *interactions-api-url* headers payload))
123 (decoded-response (cl-json:decode-json-from-string response-string))
124 (steps (cdr (assoc :STEPS decoded-response)))
125 (interaction-id (cdr (assoc :ID decoded-response)))
126 (text (%get-text-from-steps steps))
127 (function-calls (%extract-function-calls-from-steps steps)))
128 (values text function-calls interaction-id))))
129
130 (defun continue-with-function-responses (interaction-id
131 function-responses function-declarations
132 &key (model-id *model*) google-search-p)
133 "Turn 2+: Continue an interaction by supplying function call results.
134
135 INTERACTION-ID: the interaction ID from GENERATE-WITH-TOOLS.
136 FUNCTION-RESPONSES: list of plists with keys :NAME :ID :RESPONSE, one per function call, e.g.:
137 (list (list :name \"getWeather\" :id \"fc_123\" :response \"Very cold. 22F.\"))
138 The :ID must match the :ID from the corresponding FUNCTION-CALLS entry returned by Turn 1.
139 FUNCTION-DECLARATIONS: same list used in GENERATE-WITH-TOOLS.
140 :GOOGLE-SEARCH-P: whether to include the Google Search tool (match Turn 1 setting).
141 :MODEL-ID: model to use (defaults to *model*).
142
143 Returns three values, as GENERATE-WITH-TOOLS does: the model's text reply,
144 any further FUNCTION-CALLS, and the new INTERACTION-ID."
145 (let* ((payload (make-hash-table :test 'equal))
146 (fn-results (mapcar (lambda (fr)
147 (let ((result-ht (make-hash-table :test 'equal))
148 (text-block (make-hash-table :test 'equal)))
149 (setf (gethash "type" text-block) "text"
150 (gethash "text" text-block) (getf fr :response))
151 (setf (gethash "type" result-ht) "function_result"
152 (gethash "name" result-ht) (getf fr :name)
153 (gethash "call_id" result-ht) (getf fr :id)
154 (gethash "result" result-ht) (list text-block))
155 result-ht))
156 function-responses)))
157 (setf (gethash "model" payload) model-id
158 (gethash "previous_interaction_id" payload) interaction-id
159 (gethash "input" payload) fn-results)
160 (let ((tools (%make-tools-list function-declarations google-search-p)))
161 (when tools ; cl-json would encode an empty list as null
162 (setf (gethash "tools" payload) tools)))
163 (let* ((headers (list '("Content-Type" . "application/json")
164 (cons "x-goog-api-key" (google-api-key))
165 '("Api-Revision" . "2026-05-20")))
166 (response-string (%post-json *interactions-api-url* headers payload))
167 (decoded-response (cl-json:decode-json-from-string response-string))
168 (steps (cdr (assoc :STEPS decoded-response)))
169 (new-interaction-id (cdr (assoc :ID decoded-response)))
170 (text (%get-text-from-steps steps))
171 (function-calls (%extract-function-calls-from-steps steps)))
172 (values text function-calls new-interaction-id))))
This implementation is structured around two primary public functions: generate-with-tools and continue-with-function-responses. The first function initiates the dialogue, accepting a natural language prompt and a list of tool definitions created via make-function-declaration. If the model determines that it needs more information than it currently possesses, such as real-time weather data or a specific database query, it returns a list of function calls. The programmer is then responsible for executing these functions locally and passing the outputs to the second function, which targets the same interaction context using the interaction-id to provide the model with the context needed to conclude the interaction.
Both functions return three values: the model’s text, any further function calls, and the new interaction ID. A second turn can ask for more tools, so you can keep calling continue-with-function-responses until the function call list is empty.
Under the hood, the helper %make-tools-list builds a tool declaration matching the expected structure of the Interactions API. Both public functions add the "tools" key to the payload only when this list is not empty, because cl-json encodes an empty list as JSON null, which the API rejects as a value for "tools". The helper %extract-function-calls-from-steps extracts the model’s requested function calls and returns them as a structured Lisp property list.
Here is sample code for testing:
1 ;;; ---- Usage example ----
2
3 ;; 1. Define a custom function the model can request
4 (defparameter *get-weather-fn*
5 (gemini:make-function-declaration
6 "getWeather"
7 "Get the weather in a given location"
8 '(("location" "STRING" "The city and state, e.g. San Francisco, CA"))
9 '("location")))
10
11 ;; 2. Turn 1 -- send the question with tools enabled
12 (multiple-value-bind (text function-calls interaction-id)
13 (gemini:generate-with-tools
14 "What is the northernmost city in the United States? What's the weather like there today?"
15 (list *get-weather-fn*)
16 :google-search-p t)
17 (format t "Text: ~A~%" text)
18 (format t "Function calls: ~A~%" function-calls)
19
20 ;; 3. If the model requested function calls, handle them and continue
21 (when function-calls
22 (let* ((fc (first function-calls))
23 ;; In a real application you would call the actual weather API here.
24 ;; The :ARGS plist contains ((:LOCATION . "Barrow, AK")) or similar.
25 (weather-result "Very cold. 22 degrees Fahrenheit.")
26 (fn-responses
27 (list (list :name (getf fc :name)
28 :id (getf fc :id)
29 :response weather-result))))
30
31 ;; 4. Turn 2 -- provide the function results, get the final answer
32 (let ((final-answer
33 (gemini:continue-with-function-responses
34 interaction-id
35 fn-responses
36 (list *get-weather-fn*)
37 :google-search-p nil)))
38 (format t "~%Final answer: ~A~%" final-answer)))))
The output looks like:
1 Function calls: ((name getWeather id 04abw9d2 args
2 ((location . Utqiagvik, AK))))
3
4 Final answer: The northernmost city in the United States is **Utqiaġvik, Alaska** (formerly known as Barrow).
5
6 As of today, the weather there is very cold with a temperature of **22°F** (-5.5°C).
7
8 Located about 320 miles north of the Arctic Circle, Utqiaġvik is situated on the coast of the Arctic Ocean and experiences extreme conditions, including several weeks of total darkness in the winter and continuous daylight in the summer.
9 nil
Optional Practice Problems
Prompt Comparison: Write a function
compare-promptsthat accepts two prompt strings, callsgemini:generateon each, and prints both results side-by-side with labels. Usegemini:count-tokensto also display the token count for each prompt before generating. This exercise reinforces basic usage of thegenerateandcount-tokensAPI functions.Token Budget Helper: Write a function
fits-in-budget-pthat accepts a prompt string and a maximum token integer. It should callgemini:count-tokensand returnTif the prompt fits within the budget, orNILotherwise. Then write a wrapper aroundgemini:generatecalledgenerate-if-within-budgetthat only calls the API when the prompt is within a given token limit, printing a warning message instead of calling the API when the budget is exceeded.Search-Grounded Fact Checker: Using
gemini:generate-with-search, write a functionfact-checkthat takes a factual claim as a string, prepends the instruction “Is the following claim true or false? Provide evidence:”, and returns the grounded response. Compare the output offact-checkwith calling plaingemini:generateon the same claim to observe how search grounding improves factual accuracy.Citation Formatter: Write a function
print-sourced-answerthat wrapsgemini:generate-with-search-and-citations. It should usemultiple-value-bindto capture both the response text and the citations list, then print the answer followed by a numbered bibliography of sources. Each source line should display its index, title, and URL usingformat. This exercise practices working with Common Lisp multiple return values.Custom Tool: Calculator: Using
gemini:make-function-declaration, define acalculatetool that accepts two parameters:expression(a string like “2 + 3”) andprecision(an integer for decimal places). Write the full two-turn interaction: callgemini:generate-with-toolswith a math question, parse the returned function call from the:ARGSalist, evaluate the expression locally (do not pass model output toeval; see the safecalculatetool in the Ollama chapter for a small arithmetic parser), and pass the result back viagemini:continue-with-function-responsesto get the model’s final answer.Multi-Tool Agent: Extend the weather tool example from the chapter by registering two function declarations:
getWeather(as shown) and a newgetPopulationthat takes acityparameter. Write a single prompt that asks about both the weather and the population of a city. Observe how the model returns multiple entries in thefunction-callslist, and write code that iterates over all of them, builds simulated responses for each, and passes the full list tocontinue-with-function-responsesin a single Turn 2 call.