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 executes native HTTP POST requests using the Dexador client via the helper function %post-json.
To generate text, the generate function 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, then decodes the response and pulls the generated text out of the first candidate’s first part. The count-tokens function queries the companion countTokens action on the same models endpoint, using the same native HTTP post helper.
1 (in-package #:gemini)
2
3 (defvar *google-api-key* (uiop:getenv "GOOGLE_API_KEY"))
4
5 (defvar *model* "gemini-3-flash-preview") ;; model used in this file.
6
7 (defun %post-json (url headers payload-hash)
8 "Helper function to perform an HTTP POST request with a JSON payload using Dexador."
9 (let ((payload-json (cl-json:encode-json-to-string payload-hash)))
10 (dex:post url :headers (append headers '(("Accept-Encoding" . "identity")))
11 :content payload-json)))
12
13 ;;; ---- generateContent API ----
14
15 (defun generate (prompt &optional (model-id *model*))
16 "Generates text from a given prompt using the Gemini generateContent API.
17 Uses *model* defined at the top of this file as default.
18 PROMPT: The text prompt to generate content from.
19 MODEL-ID: Optional. The ID of the model to use.
20 Returns the generated text as a string."
21 (let* ((payload (make-hash-table :test 'equal))
22 (part-ht (make-hash-table :test 'equal))
23 (content-ht (make-hash-table :test 'equal)))
24 (setf (gethash "text" part-ht) prompt)
25 (setf (gethash "parts" content-ht) (list part-ht))
26 (setf (gethash "contents" payload) (list content-ht))
27 (let* ((url (concatenate 'string
28 "https://generativelanguage.googleapis.com/v1beta/models/"
29 model-id ":generateContent"))
30 (headers (list '("Content-Type" . "application/json")
31 (cons "X-goog-api-key" *google-api-key*)))
32 (response-string (%post-json url headers payload))
33 (decoded-response (cl-json:decode-json-from-string response-string))
34 (candidates (cdr (assoc :CANDIDATES decoded-response)))
35 (first-candidate (first candidates))
36 (content (cdr (assoc :CONTENT first-candidate)))
37 (parts (cdr (assoc :PARTS content)))
38 (first-part (first parts)))
39 (cdr (assoc :TEXT first-part)))))
40
41 ;; (gemini:generate "In one sentence, explain how AI works to a child.")
42 ;; (gemini:generate "Write a short, four-line poem about coding in Python.")
43
44 (defun count-tokens (prompt &optional (model-id *model*))
45 "Counts the number of tokens for a given prompt and model.
46 Uses *model* defined at top of this file as default.
47 PROMPT: The text prompt to count tokens for.
48 MODEL-ID: Optional. The ID of the model to use.
49 Returns the total token count as an integer."
50 (let* ((api-url (concatenate 'string
51 "https://generativelanguage.googleapis.com/v1beta/models/"
52 model-id ":countTokens"))
53 (payload (make-hash-table :test 'equal)))
54 ;; Construct payload similar to generate function
55 (setf (gethash "contents" payload)
56 (list (let ((contents (make-hash-table :test 'equal)))
57 (setf (gethash "parts" contents)
58 (list (let ((part (make-hash-table :test 'equal)))
59 (setf (gethash "text" part) prompt)
60 part)))
61 contents)))
62 (let* ((headers (list '("Content-Type" . "application/json")
63 (cons "x-goog-api-key" *google-api-key*)))
64 (response-string (%post-json api-url headers payload))
65 (decoded-response (cl-json:decode-json-from-string response-string))
66 (total-tokens-pair (assoc :TOTAL-TOKENS decoded-response)))
67 (if total-tokens-pair
68 (cdr total-tokens-pair)
69 (error
70 "Could not retrieve token count from API response: ~S"
71 decoded-response)))))
72
73 ;; (gemini:count-tokens "In one sentence, explain how AI works to a child.")
74
75 (defun run-tests ()
76 "Runs tests for generate and count-tokens functions."
77 (let* ((prompt "In one sentence, explain how AI works to a child.")
78 (generated-text (generate prompt))
79 (token-count (count-tokens prompt)))
80 (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” is a powerful feature that connects the Gemini model to real-time web information, allowing it to answer queries about current events and reducing the likelihood of “hallucinations” by anchoring responses in verifiable external data. The following Common Lisp program utilizes this feature by defining a function, generate-with-search, which builds a JSON payload that specifically includes a tools configuration. By inserting an empty Google Search object into this configuration, the code explicitly instructs the API to perform a web search—such as looking up the winner of a recent tournament like Euro 2024—and synthesize those findings into the final response, which is then parsed and returned as a string. The following code snippet 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 (let* ((payload (make-hash-table :test 'equal))
4 (part-ht (make-hash-table :test 'equal))
5 (content-ht (make-hash-table :test 'equal))
6 (google-search-tool (make-hash-table :test 'equal)))
7 (setf (gethash "text" part-ht) prompt)
8 (setf (gethash "parts" content-ht) (list part-ht))
9 (setf (gethash "contents" payload) (list content-ht))
10 (setf (gethash "google_search" google-search-tool) (make-hash-table :test 'equal))
11 (setf (gethash "tools" payload) (list google-search-tool))
12 (let* ((url (concatenate 'string
13 "https://generativelanguage.googleapis.com/v1beta/models/"
14 model-id ":generateContent"))
15 (headers (list '("Content-Type" . "application/json")
16 (cons "X-goog-api-key" *google-api-key*)))
17 (response-string (%post-json url headers payload))
18 (decoded-response (cl-json:decode-json-from-string response-string))
19 (candidates (cdr (assoc :CANDIDATES decoded-response)))
20 (first-candidate (first candidates))
21 (content (cdr (assoc :CONTENT first-candidate)))
22 (parts (cdr (assoc :PARTS content)))
23 (first-part (first parts)))
24 (cdr (assoc :TEXT first-part)))))
The core mechanism of this implementation relies on the payload hash table construction, specifically where the "tools" key is populated. Unlike a standard generation request, this payload includes a list containing a Google Search tool definition; the presence of this specific configuration acts as a switch, granting the model permission to query Google’s search index before formulating its answer. This is particularly critical for questions regarding current events, as the model’s static training data would otherwise be outdated.
Because this goes to the standard generateContent endpoint, the request body keeps the same contents → parts shape as plain generation, with the search tool added under "tools", and the reply arrives in the usual candidates array — the text is read from the first candidate’s first part. Declaring the google_search tool is what authorizes the model to consult Google’s search index before answering; without it, the same prompt would be answered from 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* ((payload (make-hash-table :test 'equal))
5 (part-ht (make-hash-table :test 'equal))
6 (content-ht (make-hash-table :test 'equal))
7 (google-search-tool (make-hash-table :test 'equal)))
8 (setf (gethash "text" part-ht) prompt)
9 (setf (gethash "parts" content-ht) (list part-ht))
10 (setf (gethash "contents" payload) (list content-ht))
11 (setf (gethash "google_search" google-search-tool) (make-hash-table :test 'equal))
12 (setf (gethash "tools" payload) (list google-search-tool))
13 (let* ((url (concatenate 'string
14 "https://generativelanguage.googleapis.com/v1beta/models/"
15 model-id ":generateContent"))
16 (headers (list '("Content-Type" . "application/json")
17 (cons "X-goog-api-key" *google-api-key*)))
18 (response-string (%post-json url headers payload))
19 (decoded-response (cl-json:decode-json-from-string response-string))
20 (candidates (cdr (assoc :CANDIDATES decoded-response)))
21 (first-candidate (first candidates))
22 (content (cdr (assoc :CONTENT first-candidate)))
23 (parts (cdr (assoc :PARTS content)))
24 (first-part (first parts))
25 (text (cdr (assoc :TEXT first-part)))
26 (grounding-metadata (cdr (assoc :GROUNDINGMETADATA first-candidate)))
27 (grounding-chunks (cdr (assoc :GROUNDINGCHUNKS grounding-metadata)))
28 (citations (loop for chunk in grounding-chunks
29 for web = (cdr (assoc :WEB chunk))
30 when web
31 collect (cons (cdr (assoc :TITLE web))
32 (cdr (assoc :URI web))))))
33 (values text 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 (gethash "tools" payload) (%make-tools-list function-declarations google-search-p))
117 (let* ((headers (list '("Content-Type" . "application/json")
118 (cons "x-goog-api-key" *google-api-key*)
119 '("Api-Revision" . "2026-05-20")))
120 (response-string (%post-json *interactions-api-url* headers payload))
121 (decoded-response (cl-json:decode-json-from-string response-string))
122 (steps (cdr (assoc :STEPS decoded-response)))
123 (interaction-id (cdr (assoc :ID decoded-response)))
124 (text (%get-text-from-steps steps))
125 (function-calls (%extract-function-calls-from-steps steps)))
126 (values text function-calls interaction-id))))
127
128 (defun continue-with-function-responses (interaction-id
129 function-responses function-declarations
130 &key (model-id *model*) google-search-p)
131 "Turn 2+: Continue an interaction by supplying function call results.
132
133 INTERACTION-ID: the interaction ID from GENERATE-WITH-TOOLS.
134 FUNCTION-RESPONSES: list of plists with keys :NAME :ID :RESPONSE, one per function call, e.g.:
135 (list (list :name \"getWeather\" :id \"fc_123\" :response \"Very cold. 22F.\"))
136 The :ID must match the :ID from the corresponding FUNCTION-CALLS entry returned by Turn 1.
137 FUNCTION-DECLARATIONS: same list used in GENERATE-WITH-TOOLS.
138 :GOOGLE-SEARCH-P: whether to include the Google Search tool (match Turn 1 setting).
139 :MODEL-ID: model to use (defaults to *model*).
140
141 Returns the model's final text response string."
142 (let* ((payload (make-hash-table :test 'equal))
143 (fn-results (mapcar (lambda (fr)
144 (let ((result-ht (make-hash-table :test 'equal))
145 (text-block (make-hash-table :test 'equal)))
146 (setf (gethash "type" text-block) "text"
147 (gethash "text" text-block) (getf fr :response))
148 (setf (gethash "type" result-ht) "function_result"
149 (gethash "name" result-ht) (getf fr :name)
150 (gethash "call_id" result-ht) (getf fr :id)
151 (gethash "result" result-ht) (list text-block))
152 result-ht))
153 function-responses)))
154 (setf (gethash "model" payload) model-id
155 (gethash "previous_interaction_id" payload) interaction-id
156 (gethash "input" payload) fn-results
157 (gethash "tools" payload) (%make-tools-list function-declarations google-search-p))
158 (let* ((headers (list '("Content-Type" . "application/json")
159 (cons "x-goog-api-key" *google-api-key*)
160 '("Api-Revision" . "2026-05-20")))
161 (response-string (%post-json *interactions-api-url* headers payload))
162 (decoded-response (cl-json:decode-json-from-string response-string))
163 (steps (cdr (assoc :STEPS decoded-response)))
164 (new-interaction-id (cdr (assoc :ID decoded-response)))
165 (text (%get-text-from-steps steps))
166 (function-calls (%extract-function-calls-from-steps steps)))
167 (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.
Under the hood, the helper %make-tools-list builds a tool declaration matching the expected structure of the Interactions API. 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 (you can use a simpleread-from-stringandevalapproach), 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.