Daily Use REPL: Gemini with Search and Cache
In a previous chapter we built a persistent cache engine using SQLite. Now we put it to practical use: an interactive command-line REPL that queries Google’s Gemini API, optionally grounds answers with live Google Search results, and accumulates a persistent cache of useful responses that automatically enriches future prompts.
This chapter is a SWI-Prolog port of the Common Lisp daily-use tool. It demonstrates how Prolog’s pattern matching and term manipulation make building an interactive command dispatcher particularly clean.
Design Overview
The REPL supports six commands:
| Input | Action |
|---|---|
<text> |
Ask Gemini a question |
!<text> |
Ask with Google Search grounding |
> |
Cache the last answer |
! |
Clear cache entries older than 1 week |
h / help
|
Show help |
q / quit
|
Exit |
The architecture layers three components:
- Keyword extraction — Splits user queries into meaningful terms by removing stop words, punctuation, and short tokens.
- Cache context builder — Uses extracted keywords to retrieve relevant cached entries via the
cache_enginelibrary, then prepends them as context to the Gemini prompt. - Gemini API client — Sends prompts to the Google Generative Language API, with optional Google Search grounding via the
toolsparameter.
Keyword Extraction
Before looking up the cache, we need to identify meaningful terms in the user’s query. The extract_keywords/2 predicate handles this pipeline:
1 :- module(daily_use, [
2 main/0,
3 extract_keywords/2,
4 build_context_from_cache/3
5 ]).
6
7 :- use_module(library(http/http_client)).
8 :- use_module(library(http/http_json)).
9 :- use_module(library(readutil)).
Stop words are declared as unit clauses — a natural Prolog idiom that makes lookups efficient via first-argument indexing (partial list, edited for brevity):
1 stop_word(a).
2 stop_word(an).
3 stop_word(the).
4 stop_word(am). stop_word(it). stop_word(its).
5 stop_word(in).
6 stop_word(not).
7 stop_word(no).
8 stop_word(nor). stop_word(so). stop_word(yet).
9 stop_word(this). stop_word(that). stop_word(these). stop_word(those).
10 stop_word(what). stop_word(which). stop_word(who). stop_word(whom).
The extraction pipeline downcases, splits, strips punctuation, and filters:
1 extract_keywords(Text, Keywords) :-
2 downcase_atom(Text, Lower),
3 atom_string(Lower, LowerStr),
4 split_string(LowerStr, " \t\n", " \t\n", WordStrs),
5 maplist(strip_punctuation, WordStrs, Cleaned),
6 include(meaningful_word, Cleaned, MeaningfulStrs),
7 maplist(atom_string_conv, MeaningfulStrs, Keywords).
8
9 %% strip_punctuation(+WordStr, -CleanStr)
10 strip_punctuation(Word, Clean) :-
11 string_chars(Word, Chars),
12 include(non_punct, Chars, CleanChars),
13 string_chars(Clean, CleanChars).
14
15 non_punct(C) :-
16 \+ member(C, ['?','!','.',',',';',':','"','\'','(',')','[',']','{',
17 '}']).
18
19 meaningful_word(W) :-
20 string_length(W, Len),
21 Len > 2,
22 atom_string(A, W),
23 \+ stop_word(A).
This approach mirrors the Common Lisp version’s extract-keywords function, but uses Prolog’s maplist/3 and include/3 higher-order predicates instead of mapcar and remove-if.
Cache Context Builder
The context builder bridges keyword extraction and the cache engine:
1 build_context_from_cache(Connection, Query, Context) :-
2 extract_keywords(Query, Keywords),
3 ( Keywords = [] ->
4 Context = ""
5 ;
6 cache_engine:cache_lookup(Connection, Keywords, Items,
7 [limit(10), match_any(true)]),
8 ( Items = [] ->
9 Context = ""
10 ;
11 format_context_items(Items, Formatted),
12 format(atom(Context),
13 "Use the following context from previous conversations when answering:\n\n~w\n---\n\n",
14 [Formatted])
15 )
16 ).
17
18 format_context_items([], '').
19 format_context_items([Item|Rest], Formatted) :-
20 format_context_items(Rest, RestFmt),
21 format(atom(Formatted), "- ~w\n~w", [Item, RestFmt]).
The key design decision is using match_any(true) — OR matching across keywords. This casts a wider net, retrieving any cached entry that mentions at least one of the query’s keywords, rather than requiring all terms to match.
Gemini API Integration
The module calls the Gemini API directly using SWI-Prolog’s HTTP libraries, following the same pattern as the llm_client project:
1 call_gemini_api(Prompt, SearchP, Response) :-
2 getenv('GOOGLE_API_KEY', ApiKey),
3 model(Model),
4 format(atom(URL),
5 'https://generativelanguage.googleapis.com/v1beta/models/~w:generateContent?key=~w',
6 [Model, ApiKey]),
7 build_payload(Prompt, SearchP, Payload),
8 http_post(URL, json(Payload), Result, [json_object(dict)]),
9 extract_text_response(Result, Response).
When search grounding is enabled (!<query>), the payload includes a tools array with google_search:
1 build_payload(Prompt, false, Payload) :-
2 Payload = json([
3 contents=[json([
4 parts=[json([text=Prompt])]
5 ])]
6 ]).
7 build_payload(Prompt, true, Payload) :-
8 Payload = json([
9 contents=[json([
10 parts=[json([text=Prompt])]
11 ])],
12 tools=[json([
13 google_search=json([])
14 ])]
15 ]).
Notice how Prolog’s multi-clause predicates eliminate the need for if/else branching — the two build_payload/3 clauses pattern-match on the SearchP argument.
The REPL Loop
The REPL reads lines from standard input and dispatches on the input pattern using Prolog’s clause-based dispatch:
1 repl_loop :-
2 format("~n Gemini Daily-Use REPL (type 'h' for help)~n~n"),
3 repl_iteration.
4
5 repl_iteration :-
6 format("gemini> "),
7 flush_output,
8 catch(
9 read_line_to_string(current_input, RawInput),
10 _,
11 ( format("~nGoodbye.~n"), ! )
12 ),
13 ( RawInput == end_of_file ->
14 format("~nGoodbye.~n")
15 ;
16 normalize_space(atom(Trimmed), RawInput),
17 process_input(Trimmed),
18 repl_iteration
19 ).
Each command is a separate process_input/1 clause. This is cleaner than the Common Lisp version’s cond block — each clause is self-contained and the cut (!) prevents fallthrough:
1 process_input(quit) :- !, format("Goodbye.~n"), halt(0).
2 process_input(exit) :- !, format("Goodbye.~n"), halt(0).
3
4 % Help
5 process_input(h) :- !, print_help.
6 process_input(help) :- !, print_help.
7
8 % ">" — cache last answer
9 process_input('>') :- !,
10 ( last_answer(Ans) ->
11 cache_connection(Conn),
12 cache_engine:cache_add(Conn, Ans),
13 cache_engine:cache_count(Conn, N),
14 format(" [Cached. ~w items total]~n", [N])
15 ;
16 format(" [No answer to cache yet]~n")
17 ).
18
19 % "!" alone — clear old cache entries
20 process_input('!') :- !,
21 cache_connection(Conn),
22 cache_engine:cache_count(Conn, Before),
23 cache_engine:cache_clear_older_one_week(Conn),
24 cache_engine:cache_count(Conn, After),
25 Cleared is Before - After,
26 format(" [Cleared ~w old entries. ~w items remain]~n", [Cleared,
27 After]).
Running the REPL
Set your API key and run:
1 $ export GOOGLE_API_KEY=your-key-here
2 $ cd source-code/daily_use
3 $ make run
Or directly:
1 $ swipl run.pl
Here is an example session:
1 Gemini Daily-Use REPL (type 'h' for help)
2
3 gemini> !what is the weather in Sedona AZ today?
4 [Searching...]
5
6 Currently in Sedona, AZ it is partly cloudy and 78°F (26°C).
7
8 gemini> >
9 [Cached. 1 items total]
10 gemini> what should I wear in Sedona today?
11 [Thinking...]
12
13 Based on the current weather in Sedona (78°F and partly cloudy),
14 light layers would be ideal...
15
16 gemini> q
17 Goodbye.
18 [Cache closed]
Notice in the second query, the cached weather information was automatically included as context — the keyword “Sedona” matched the cached entry, giving Gemini the local conditions without needing another search.
Wrap Up
This REPL demonstrates several Prolog strengths applied to a practical tool:
- Clause-based dispatch replaces procedural
switch/condstatements with clean, self-documenting pattern matching. - Higher-order predicates (
maplist,include) provide the same functional pipeline as Common Lisp’smapcarandremove-if. - Dynamic predicates (
last_answer/1,cache_connection/1) provide mutable state where needed, while keeping the rest of the code purely declarative. - Module composition — the daily_use module imports
cache_enginefor persistence and uses the standard HTTP libraries for API calls, demonstrating how Prolog modules compose cleanly.