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.
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 #:send-chat-message
7 #:generate-streaming #:generate-with-search
8 #:generate-with-search-and-citations
9 #:make-function-declaration #:generate-with-tools
10 #:continue-with-function-responses))
gemini.asd
1 ;;;; gemini.asd
2
3 (asdf:defsystem #:gemini
4 :description "Library for using the perplexity search+LLM APIs"
5 :author "Mark Watson"
6 :license "Apache 2"
7 :depends-on (#:uiop #:cl-json #:alexandria)
8 :components (
9 (:file "package")
10 (:file "gemini")
11 (:file "gemini_interactions_api")))
gemini.lisp
This code defines a function that sends a user-provided text prompt to an external API for generative language processing. It first retrieves a Google API key from the environment and sets the API endpoint URL, then constructs a nested JSON payload embedding the prompt within a specific structure. Using a POST request with appropriate headers - including the API key - the function submits the payload to the API. It then decodes the JSON response, traverses the nested data to extract the generated text, and finally returns plain text as the result.
Note: later we will look at the last part of the file gemini.lisp for code to use Google’s “grounding search”.
1 (in-package #:gemini)
2
3 (defvar *google-api-key* (uiop:getenv "GOOGLE_API_KEY"))
4 (defvar
5 *gemini-api-base-url*
6 "https://generativelanguage.googleapis.com/v1beta/models/")
7
8 (defvar *model* "gemini-3-flash-preview") ;; model used in this file.
9
10 (defun escape-json (json-string)
11 (with-output-to-string (s)
12 (loop for char across json-string
13 do (case char
14 (#\" (write-string "\\\"" s))
15 (#\\ (write-string "\\\\" s))
16 (t (write-char char s))))))
17
18 (defun run-curl-command (curl-command)
19 (multiple-value-bind (output error-output exit-code)
20 (uiop:run-program curl-command
21 :output :string
22 :error-output :string
23 :ignore-error-status t)
24 (if (zerop exit-code)
25 output
26 (error "Curl command failed: ~A~%Error: ~A" curl-command error-output))))
27
28 (defun generate (prompt &optional (model-id *model*))
29 "Generates text from a given prompt using the specified model.
30 Uses *model* defined at the top of this file as default.
31 PROMPT: The text prompt to generate content from.
32 MODEL-ID: Optional. The ID of the model to use.
33 Returns the generated text as a string."
34 (let* ((payload (make-hash-table :test 'equal)))
35 (setf (gethash "contents" payload)
36 (list (let ((contents (make-hash-table :test 'equal)))
37 (setf (gethash "parts" contents)
38 (list (let ((part (make-hash-table :test 'equal)))
39 (setf (gethash "text" part) prompt)
40 part)))
41 contents)))
42 (let* ((api-url (concatenate 'string *gemini-api-base-url*
43 model-id ":generateContent"))
44 (data (cl-json:encode-json-to-string payload))
45 (escaped-json (escape-json data))
46 (curl-cmd
47 (format
48 nil
49 "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
50 api-url *google-api-key* escaped-json))
51 (response-string (run-curl-command curl-cmd))
52 (decoded-response (cl-json:decode-json-from-string response-string))
53 (candidates-pair (assoc :CANDIDATES decoded-response))
54 (candidates (cdr candidates-pair))
55 (candidate (first candidates))
56 (content-pair (assoc :CONTENT candidate))
57 (content (cdr content-pair))
58 (parts-pair (assoc :PARTS content))
59 (parts (cdr parts-pair))
60 (first-part (first parts))
61 (text-pair (assoc :TEXT first-part))
62 (text (cdr text-pair)))
63 text)))
64
65 ;; (gemini:generate "In one sentence, explain how AI works to a child.")
66 ;; (gemini:generate "Write a short, four-line poem about coding in Python.")
67
68 (defun count-tokens (prompt &optional (model-id *model*))
69 "Counts the number of tokens for a given prompt and model.
70 Uses *model* defined at top of this file as default.
71 PROMPT: The text prompt to count tokens for.
72 MODEL-ID: Optional. The ID of the model to use.
73 Returns the total token count as an integer."
74 (let* ((api-url (concatenate 'string
75 *gemini-api-base-url* model-id ":countTokens"))
76 (payload (make-hash-table :test 'equal)))
77 ;; Construct payload similar to generate function
78 (setf (gethash "contents" payload)
79 (list (let ((contents (make-hash-table :test 'equal)))
80 (setf (gethash "parts" contents)
81 (list (let ((part (make-hash-table :test 'equal)))
82 (setf (gethash "text" part) prompt)
83 part)))
84 contents)))
85 (let* ((data (cl-json:encode-json-to-string payload))
86 (escaped-json (escape-json data))
87 (curl-cmd
88 (format
89 nil
90 "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
91 api-url *google-api-key* escaped-json))
92 (response-string (run-curl-command curl-cmd))
93 (decoded-response (cl-json:decode-json-from-string response-string))
94 ;; cl-json by default uses :UPCASE for keys,
95 ;; so :TOTAL-TOKENS should be correct
96 (total-tokens-pair (assoc :TOTAL-TOKENS decoded-response)))
97 (if total-tokens-pair
98 (cdr total-tokens-pair)
99 (error "Could not retrieve token count from API response: ~S"
100 decoded-response)))))
101
102 ;; (gemini:count-tokens "In one sentence, explain how AI works to a child.")
103
104 (defun run-tests ()
105 "Runs tests for generate and count-tokens functions."
106 (let* ((prompt "In one sentence, explain how AI works to a child.")
107 (generated-text (generate prompt))
108 (token-count (count-tokens prompt)))
109 (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 (let* ((payload (make-hash-table :test 'equal)))
3 (setf (gethash "contents" payload)
4 (list (let ((contents (make-hash-table :test 'equal)))
5 (setf (gethash "parts" contents)
6 (list (let ((part (make-hash-table :test 'equal)))
7 (setf (gethash "text" part) prompt)
8 part)))
9 contents)))
10 (setf (gethash "tools" payload)
11 (list (let ((tool (make-hash-table :test 'equal)))
12 (setf (gethash "google_search" tool)
13 (make-hash-table :test 'equal))
14 tool)))
15 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
16 (data (cl-json:encode-json-to-string payload))
17 (escaped-json (escape-json data))
18 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
19 api-url *google-api-key* escaped-json))
20 (response-string (run-curl-command curl-cmd))
21 (decoded-response (cl-json:decode-json-from-string response-string))
22 (candidates-pair (assoc :CANDIDATES decoded-response))
23 (candidates (cdr candidates-pair))
24 (candidate (first candidates))
25 (content-pair (assoc :CONTENT candidate))
26 (content (cdr content-pair))
27 (parts-pair (assoc :PARTS content))
28 (parts (cdr parts-pair))
29 (first-part (first parts))
30 (text-pair (assoc :TEXT first-part))
31 (text (cdr text-pair)))
32 text)))
33
34 ;; (gemini:generate-with-search "Consultant Mark Watson has written Common Lisp, semantic web, Clojure, Java, and AI books. What musical instruments does he play?")
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 object; the presence of this specific key acts as a switch, granting the model permission to query Google’s search index before formulating its answer. This is particularly critical for the example query regarding “Euro 2024,” as the model’s static training data would likely cut off before the event occurred, making the search tool indispensable for factual accuracy.
Once the API processes the request, the function handles the JSON response by traversing the nested structure of candidates and content parts. While the API response for a grounded query actually includes rich metadata—such as groundingMetadata with search queries, source titles, and URLs—this specific function is designed to filter that out, drilling down solely to extract the synthesized text string from the first part of the first candidate. This provides a clean, human-readable answer while abstracting away the complexity of the underlying search-and-retrieve operations that generated it.
Here is example output:
1 $ sbcl
2 This is SBCL 2.5.10, an implementation of ANSI Common Lisp.
3 * (ql :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 Harkens 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 * **David** (PG)
25 * 12:45 PM, 3:30 PM, 6:15 PM, 9:00 PM
26 * **Ella McCay** (PG-13)
27 * 10:20 AM, 1:05 PM, 3:50 PM, 6:35 PM
28 * **Eternity** (PG-13)
29 * 10:45 AM, 1:35 PM, 4:20 PM, 7:05 PM
30 * **Hamnet** (PG-13)
31 * 12:20 PM, 3:25 PM, 6:30 PM
32 * **The Housemaid** (R)
33 * 2:00 PM, 5:00 PM, 8:00 PM, 10:15 PM
34 * **Wicked: For Good**
35 * *Check theater for exact late-afternoon and evening showtimes.*
36
37 ### **Special Holiday Events**
38 * **How the Grinch Stole Christmas (25th Anniversary):** Screening as part of the Harkins Holiday Series.
39 * **Harkins Holiday Series:** Various seasonal classics are playing through December 22.
40
41 ---
42 **Theater Information:**
43 * **Address:** 4751 East Marketplace Dr, Flagstaff, AZ 86004
44 * **Phone:** (928) 233-3005
45
46 *Showtimes are subject to change. It is recommended to verify specific times on the official Harkins website or app before heading to the theater.*"
47 *
Using Google’s “Grounding Search” With Citations
This example is also near the bottom of the file gemini.lisp and adds the 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 added code:
1 (defun generate-with-search-and-citations (prompt &optional (model-id *model*))
2 (let* ((payload (make-hash-table :test 'equal)))
3 ;; Payload construction same as previous example):
4 (setf (gethash "contents" payload)
5 (list (let ((contents (make-hash-table :test 'equal)))
6 (setf (gethash "parts" contents)
7 (list (let ((part (make-hash-table :test 'equal)))
8 (setf (gethash "text" part) prompt)
9 part)))
10 contents)))
11 (setf (gethash "tools" payload)
12 (list (let ((tool (make-hash-table :test 'equal)))
13 (setf (gethash "google_search" tool)
14 (make-hash-table :test 'equal))
15 tool)))
16
17 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
18 (data (cl-json:encode-json-to-string payload))
19 (escaped-json (escape-json data))
20 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
21 api-url *google-api-key* escaped-json))
22 (response-string (run-curl-command curl-cmd))
23 (decoded-response (cl-json:decode-json-from-string response-string))
24 (candidates-pair (assoc :CANDIDATES decoded-response))
25 (candidates (cdr candidates-pair))
26 (candidate (first candidates))
27 ;; 1. Extract Content Text
28 (content-pair (assoc :CONTENT candidate))
29 (content (cdr content-pair))
30 (parts-pair (assoc :PARTS content))
31 (parts (cdr parts-pair))
32 (first-part (first parts))
33 (text-pair (assoc :TEXT first-part))
34 (text (cdr text-pair))
35 ;; 2. Extract Grounding Metadata
36 (metadata-pair (assoc :GROUNDING-METADATA candidate))
37 (metadata (cdr metadata-pair))
38 (chunks-pair (assoc :GROUNDING-CHUNKS metadata))
39 (chunks (cdr chunks-pair))
40 ;; 3. Loop through chunks to find Web sources
41 (citations (loop for chunk in chunks
42 for web-data-pair = (assoc :WEB chunk)
43 for web-data = (cdr web-data-pair)
44 when web-data
45 collect (cons (cdr (assoc :TITLE web-data))
46 (cdr (assoc :URI web-data))))))
47 ;; Return both text and citations
48 (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 "gemini-2.0-flash" "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 Google Interactions APIs, as of March 2026, are in beta and may change. You can find the documentation here https://ai.google.dev/gemini-api/docs/interactions.
**Note: As of March 23 2026, I am still working on the Interactions example code. Latest code will be in GitHub repository in loving-common-lisp/src/gemini/gemini_interactions_api.lisp.
The following Common Lisp implementation in the file gemini_interactions_api.lisp offers a robust framework for interacting with Google’s Gemini API, specifically focusing on multi-turn conversations and the orchestration of client-side tool use. By leveraging the cl-json library for serialization, the code facilitates a seamless exchange between Lisp’s symbolic data structures and the JSON-based REST requirements of the Gemini endpoint. The core logic handles the intricate “Turn 1” and “Turn 2” workflow: it first dispatches a user prompt alongside function declarations, parses potential function calls requested by the model, and then provides a mechanism to feed the results of those local computations back to the model to generate a final, grounded response. Key utility functions within the package automate the conversion between Lisp’s hyphenated naming conventions and the API’s expected camelCase format, ensuring that internal hash-table representations are perfectly aligned with the schema required for tool configurations and content parts.
1 (in-package #:gemini)
2
3 ;;; ====================================================================
4 ;;; Gemini Interactions API - Multi-turn conversations with tool use
5 ;;; ====================================================================
6 ;;;
7 ;;; Supports multi-turn conversations combining Google Search (built-in)
8 ;;; and custom function declarations (client-side tool use).
9 ;;;
10 ;;; Typical workflow:
11 ;;; 1. Build function declarations with MAKE-FUNCTION-DECLARATION
12 ;;; 2. Call GENERATE-WITH-TOOLS for Turn 1 -- returns TEXT, FUNCTION-CALLS, MODEL-CONTENT-HT
13 ;;; 3. Invoke the functions yourself and collect results
14 ;;; 4. Call CONTINUE-WITH-FUNCTION-RESPONSES for Turn 2 -- returns final TEXT
15 ;;; ====================================================================
16
17
18 ;;; ---- Internal utilities ----
19
20 (defun %symbol-name-to-camel-case (sym-name)
21 "Converts a Lisp symbol name string (e.g. \"FUNCTION-CALL\") to camelCase (\"functionCall\").
22 Consecutive hyphens (produced when cl-json decodes mixed snake_CamelCase keys) are collapsed."
23 (let* ((raw-words (loop for start = 0 then (1+ pos)
24 for pos = (position #\- sym-name :start start)
25 collect (subseq sym-name start pos)
26 while pos))
27 ;; Drop empty segments arising from consecutive hyphens (e.g. "SEARCH--SUGGESTIONS")
28 (words (remove-if (lambda (w) (zerop (length w))) raw-words)))
29 (if (null words)
30 (string-downcase sym-name)
31 (with-output-to-string (s)
32 (write-string (string-downcase (first words)) s)
33 (dolist (word (rest words))
34 (write-char (char-upcase (char word 0)) s)
35 (write-string (string-downcase (subseq word 1)) s))))))
36
37 (defun %is-decoded-alist-p (x)
38 "Returns T if X looks like a cl-json decoded JSON object (alist with symbol keys)."
39 (and (listp x) x (consp (first x)) (symbolp (caar x))))
40
41 (defun %decoded-to-ht (decoded)
42 "Recursively converts a cl-json decoded value back to hash-tables for re-encoding.
43 cl-json decodes JSON objects as alists with keyword keys (e.g. :FUNCTION-CALL).
44 This inverts that, producing hash-tables with camelCase string keys so the
45 value can be re-serialised faithfully with cl-json:encode-json-to-string."
46 (cond
47 ((%is-decoded-alist-p decoded)
48 (let ((ht (make-hash-table :test 'equal)))
49 (dolist (pair decoded ht)
50 (let* ((key (%symbol-name-to-camel-case (symbol-name (car pair))))
51 (val (%decoded-to-ht (cdr pair))))
52 (setf (gethash key ht) val)))))
53 ((listp decoded)
54 (mapcar #'%decoded-to-ht decoded))
55 (t decoded)))
56
57 (defun %make-content-ht (role parts)
58 "Creates a content hash-table with role and parts list."
59 (let ((ht (make-hash-table :test 'equal)))
60 (setf (gethash "role" ht) role
61 (gethash "parts" ht) parts)
62 ht))
63
64 (defun %make-text-part (text)
65 "Creates a text part hash-table."
66 (let ((ht (make-hash-table :test 'equal)))
67 (setf (gethash "text" ht) text)
68 ht))
69
70 (defun %make-function-response-part (name id response-data)
71 "Creates a functionResponse part hash-table.
72 NAME: function name string
73 ID: the function call ID returned by the model in Turn 1
74 RESPONSE-DATA: the value to return as the function result (string, number, etc.)"
75 (let ((resp-ht (make-hash-table :test 'equal))
76 (fr-ht (make-hash-table :test 'equal))
77 (part-ht (make-hash-table :test 'equal)))
78 ;; API expects: {"response": {"response": <data>}}
79 (setf (gethash "response" resp-ht) response-data)
80 (setf (gethash "name" fr-ht) name
81 (gethash "id" fr-ht) id
82 (gethash "response" fr-ht) resp-ht)
83 (setf (gethash "functionResponse" part-ht) fr-ht)
84 part-ht))
85
86 (defun %make-tools-list (function-declarations &optional google-search-p)
87 "Builds the tools array for the API payload.
88 FUNCTION-DECLARATIONS: list of hash-tables from MAKE-FUNCTION-DECLARATION, or NIL.
89 GOOGLE-SEARCH-P: when T, prepends the built-in Google Search tool."
90 (let ((tools '()))
91 (when function-declarations
92 (let ((fn-tool (make-hash-table :test 'equal)))
93 (setf (gethash "functionDeclarations" fn-tool) function-declarations)
94 (push fn-tool tools)))
95 (when google-search-p
96 (let ((gs-tool (make-hash-table :test 'equal)))
97 (setf (gethash "googleSearch" gs-tool) (make-hash-table :test 'equal))
98 (push gs-tool tools)))
99 (nreverse tools)))
100
101 (defun %make-tool-config ()
102 "Creates the toolConfig hash-table that asks the server to include its own tool invocations in the response."
103 (let ((ht (make-hash-table :test 'equal)))
104 (setf (gethash "includeServerSideToolInvocations" ht) t)
105 ht))
106
107 (defun %extract-function-calls (candidate)
108 "Returns a list of plists (:NAME :ID :ARGS) for every functionCall part in CANDIDATE.
109 ARGS is a cl-json decoded alist, e.g. ((:LOCATION . \"Barrow, AK\"))."
110 (let* ((content (cdr (assoc :CONTENT candidate)))
111 (parts (cdr (assoc :PARTS content))))
112 (loop for part in parts
113 for fc-pair = (assoc :FUNCTION-CALL part)
114 when fc-pair
115 collect (let ((fc (cdr fc-pair)))
116 (list :name (cdr (assoc :NAME fc))
117 :id (cdr (assoc :ID fc))
118 :args (cdr (assoc :ARGS fc)))))))
119
120 (defun %get-text-from-candidate (candidate)
121 "Returns the first text string found in CANDIDATE's parts, or NIL."
122 (let* ((content (cdr (assoc :CONTENT candidate)))
123 (parts (cdr (assoc :PARTS content))))
124 (loop for part in parts
125 for text-pair = (assoc :TEXT part)
126 when text-pair return (cdr text-pair))))
127
128
129 ;;; ---- Public API ----
130
131 (defun make-function-declaration (name description parameters &optional required-params)
132 "Creates a functionDeclaration hash-table suitable for GENERATE-WITH-TOOLS.
133
134 NAME: string -- the function name the model will invoke
135 DESCRIPTION: string -- natural-language description of what the function does
136 PARAMETERS: list of (param-name type description) triples, e.g.:
137 '((\"location\" \"STRING\" \"The city and state, e.g. San Francisco, CA\"))
138 REQUIRED-PARAMS: optional list of required parameter name strings, e.g.:
139 '(\"location\")
140
141 Example:
142 (make-function-declaration
143 \"getWeather\"
144 \"Get the weather in a given location\"
145 '((\"location\" \"STRING\" \"The city and state, e.g. San Francisco, CA\"))
146 '(\"location\"))"
147 (let ((decl-ht (make-hash-table :test 'equal))
148 (params-ht (make-hash-table :test 'equal))
149 (props-ht (make-hash-table :test 'equal)))
150 (dolist (param parameters)
151 (destructuring-bind (pname ptype pdesc) param
152 (let ((prop-ht (make-hash-table :test 'equal)))
153 (setf (gethash "type" prop-ht) ptype
154 (gethash "description" prop-ht) pdesc)
155 (setf (gethash pname props-ht) prop-ht))))
156 (setf (gethash "type" params-ht) "OBJECT"
157 (gethash "properties" params-ht) props-ht)
158 (when required-params
159 (setf (gethash "required" params-ht) required-params))
160 (setf (gethash "name" decl-ht) name
161 (gethash "description" decl-ht) description
162 (gethash "parameters" decl-ht) params-ht)
163 decl-ht))
164
165 (defun generate-with-tools (prompt function-declarations
166 &key (model-id *model*) google-search-p)
167 "Turn 1: Send PROMPT to the model with optional tool support.
168
169 PROMPT: the user's text question.
170 FUNCTION-DECLARATIONS: list of hash-tables from MAKE-FUNCTION-DECLARATION, or NIL.
171 :GOOGLE-SEARCH-P: when T, enables the built-in Google Search tool.
172 :MODEL-ID: model to use (defaults to *model*).
173
174 Returns three values:
175 TEXT - model's text reply, or NIL when it chose to call functions.
176 FUNCTION-CALLS - list of plists (:NAME :ID :ARGS) for each function call made.
177 ARGS is a cl-json alist, e.g. ((:LOCATION . \"Barrow, AK\")).
178 MODEL-CONTENT-HT - the model's content as a hash-table; pass this unchanged to
179 CONTINUE-WITH-FUNCTION-RESPONSES as the conversation history."
180 (let* ((payload (make-hash-table :test 'equal))
181 (user-content (%make-content-ht "user" (list (%make-text-part prompt)))))
182 (setf (gethash "contents" payload) (list user-content)
183 (gethash "tools" payload) (%make-tools-list function-declarations google-search-p)
184 (gethash "toolConfig" payload) (%make-tool-config))
185 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
186 (data (cl-json:encode-json-to-string payload))
187 (escaped-json (escape-json data))
188 (curl-cmd (format nil
189 "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
190 api-url *google-api-key* escaped-json))
191 (response-string (run-curl-command curl-cmd))
192 (decoded-response (cl-json:decode-json-from-string response-string))
193 (candidates (cdr (assoc :CANDIDATES decoded-response)))
194 (candidate (first candidates))
195 (text (%get-text-from-candidate candidate))
196 (function-calls (%extract-function-calls candidate))
197 (model-content-ht (%decoded-to-ht (cdr (assoc :CONTENT candidate)))))
198 (values text function-calls model-content-ht))))
199
200 (defun continue-with-function-responses (original-prompt model-content-ht
201 function-responses function-declarations
202 &key (model-id *model*) google-search-p)
203 "Turn 2+: Continue the conversation by supplying function call results.
204
205 ORIGINAL-PROMPT: the same user text string passed to GENERATE-WITH-TOOLS in Turn 1.
206 MODEL-CONTENT-HT: the third return value from GENERATE-WITH-TOOLS (the model's content).
207 FUNCTION-RESPONSES: list of plists with keys :NAME :ID :RESPONSE, one per function call, e.g.:
208 (list (list :name \"getWeather\" :id \"call_123\" :response \"Very cold. 22F.\"))
209 The :ID must match the :ID from the corresponding FUNCTION-CALLS entry returned by Turn 1.
210 FUNCTION-DECLARATIONS: same list used in GENERATE-WITH-TOOLS.
211 :GOOGLE-SEARCH-P: whether to include the Google Search tool (match Turn 1 setting).
212 :MODEL-ID: model to use (defaults to *model*).
213
214 Returns the model's final text response string."
215 (let* ((payload (make-hash-table :test 'equal))
216 ;; Rebuild Turn-1 user message
217 (user-content-1 (%make-content-ht "user" (list (%make-text-part original-prompt))))
218 ;; Turn-1 model response (already a correctly shaped hash-table)
219 ;; Turn-2 user message carrying the function results
220 (fn-parts (mapcar (lambda (fr)
221 (%make-function-response-part
222 (getf fr :name)
223 (getf fr :id)
224 (getf fr :response)))
225 function-responses))
226 (user-content-2 (%make-content-ht "user" fn-parts)))
227 (setf (gethash "contents" payload) (list user-content-1 model-content-ht user-content-2)
228 (gethash "tools" payload) (%make-tools-list function-declarations google-search-p)
229 (gethash "toolConfig" payload) (%make-tool-config))
230 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
231 (data (cl-json:encode-json-to-string payload))
232 (escaped-json (escape-json data))
233 (curl-cmd (format nil
234 "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
235 api-url *google-api-key* escaped-json))
236 (response-string (run-curl-command curl-cmd))
237 (decoded-response (cl-json:decode-json-from-string response-string))
238 (candidates (cdr (assoc :CANDIDATES decoded-response)))
239 (candidate (first candidates)))
240 (%get-text-from-candidate candidate))))
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, then it returns a set of function calls. The programmer is then responsible for executing these functions locally and passing the outputs to the second function, which reconstructs the conversation history to provide the model with the context needed to conclude the interaction.
Under the hood, the code makes use of hash tables and recursion to manage the transformation of data. Because the Gemini API is highly sensitive to the structure of “parts” and “roles” within its content arrays, the helper functions %make-content-ht and %make-function-response-part ensure that the JSON payload is correctly nested. Furthermore, the inclusion of a dedicated %symbol-name-to-camel-case utility demonstrates a conservative and careful approach to Lisp integration, allowing developers to work with idiomatic Lisp symbols while maintaining strict compatibility with the external web service.
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 model-content)
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 "What is the northernmost city in the United States? What's the weather like there today?"
35 model-content
36 fn-responses
37 (list *get-weather-fn*)
38 :google-search-p t)))
39 (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