Using Gemini Function Calling to Build a Research Agent

Using Gemini Function Calling to Build a Research Agent

Imagine you’ve hired a tiny digital research assistant. You ask it a question, it dives first into your local archive of, in my case, Karl Popper essays, then—if it still thirsts for knowledge—dips into Wikipedia, and finally whispers back its findings. All in under a hundred and fifty lines of code. Let’s eavesdrop on how this “research agent” is built, step by step, and then marvel at the real‑world transcript for our inductive statistics question.

You can find all my code in my GitHub repository here.

Rolling Out the Red Carpet

You may need to do some installs. Though, if you've been following along with my posts, you've already got all of these:

pip install google-generativeai wikipedia

Now for some setup:

import wikipedia
import google.generativeai as genai
from doc_retrieval_pipeline import DocRetrievalPipeline
from haystack import Document
from generator_model import get_secret
from llm_message_utils import send_message

# Gemini credentials, plucked safely from disk
gemini_key = get_secret(r'D:\Documents\Secrets\gemini_secret.txt')
genai.configure(api_key=gemini_key)

# Spin up our Popper archive retriever
doc_retriever = DocRetrievalPipeline(
    table_name="popper_archive",
    db_user_name="postgres",
    db_password=get_secret(r'D:\Documents\Secrets\postgres_password.txt'),
    postgres_host="localhost",
    postgres_port=5432,
    db_name="postgres",
    verbose=False,
    llm_top_k=5,
    retriever_top_k_docs=100,
    use_reranker=True,
    embedder_model_name="BAAI/llm-embedder",
)

# Start a fresh Gemini chat session
model = genai.GenerativeModel("gemini-2.0-flash")
chat  = model.start_chat(history=[])

Here, we gently coax Gemini awake, hand it our Postgres‑backed Karl Popper archive, and let it use our API key. Note that we're here using the DocRetrievalPipeline Haystack pipeline we've previously built. (Last encountered here.) We're also going to be using our 'one stop shop' Gemini message sender.

Teaching It to Read

Next, we equip our agent with two “tools”: one for your local Haystack document store and one for Wikipedia. But first, a little formatter to turn hay‑stacked metadata into tidy paragraphs.

def format_doc(doc: Document) -> str:
    lines = []
    for k, v in doc.meta.items():
        if not k.startswith("_"):
            lines.append(f"{k}: {v}")
    lines.append(doc.content)
    return "\n".join(lines)

This helper makes Popper’s chapter titles and page numbers look like a friendly librarian’s margin notes, not raw JSON dumps.

def search_datastore(datastore_query: str) -> Dict[str, str]:
    docs, _ = doc_retriever.generate_response(datastore_query, min_score=0.6)
    if not docs:
        return {"result": "No matching documents found."}
    snippets = [format_doc(d) for d in docs[:3]]
    return {"result": "\n\n".join(snippets)}

def search_wikipedia(wiki_page_search: str, question: str) -> Dict[str, str]:
    try:
        page = wikipedia.page(wiki_page_search, auto_suggest=False)
        return {"result": page.summary}
    except Exception as e:
        return {"result": f"Error fetching Wikipedia: {e}"}

The first function fetches and formats up to three matches from your Popper archive. The second function slips into Wikipedia and grabs the article summary and returns it.

Introducing Our Magic Words

Now we register these functions as tools that Gemini can invoke:

from google.generativeai.types import Tool, FunctionDeclaration

tools = [
    Tool(function_declarations=[
        FunctionDeclaration(
            name="search_datastore",
            description="Query the local Popper archive.",
            parameters={
                "type": "object",
                "properties": {"datastore_query": {"type": "string"}},
                "required": ["datastore_query"],
            },
        ),
        FunctionDeclaration(
            name="search_wikipedia",
            description="Look up a summary on Wikipedia.",
            parameters={
                "type": "object",
                "properties": {
                    "wiki_page_search": {"type": "string"},
                    "question":         {"type": "string"},
                },
                "required": ["wiki_page_search", "question"],
            },
        ),
    ])
]

This tiny registry is the secret handshake between your Python code and Gemini’s function‑calling machinery.

The Heartbeat Loop

Here’s the core loop that sends your question, catches any function_calls, executes them, and feeds the results back—until Gemini has no more tools to invoke:

def run(question: str): prompt = ( "You may call searchdatastore, then searchwikipedia. " "Gather what you need and answer.\n\n" f"Question: {question}" ) response = send_message(chat, prompt, tools=tools)

while True:
    candidate = response.candidates[0]
    did_call = False

    for part in candidate.content.parts:
        # 1) Plain text? Print it.
        if getattr(part, "text", None):
            print(part.text.strip())

        # 2) Function call? Handle it.
        if getattr(part, "function_call", None):
            did_call = True
            func = part.function_call
            name = func.name
            raw_args = func.args or {}
            pretty_args = {}
            for k, v in raw_args.items():
                if hasattr(v, "WhichOneof"):
                    kind = v.WhichOneof("kind")
                    pretty_args[k] = getattr(v, kind)
                else:
                    pretty_args[k] = v

            print(f"\n[Calling {name} with args {pretty_args}]")
            dispatch = {
                "search_datastore": search_datastore,
                "search_wikipedia": search_wikipedia,
            }
            result = dispatch[name](**pretty_args)["result"]
            print(f"[Result]: {result}\n")

            # Feed the result back into Gemini
            response = send_message(chat, result, tools=tools)
            break

    if not did_call:
        # No more function calls → done
        break

Basically we call Gemini and it will give a response. Likely (if things go as intended) it will return with a function call request. We call that function and return the results to Gemini and it will keep calling functions until it's ready to give an answer. Once it stops calling functions it should return a result and that is our researched answer.

Showtime: Watching It Think

Let’s run it with our burning question:

if __name__ == "__main__":
    run("Is induction valid in some cases, particularly when doing statistics?")

The results are pretty amazing for such a small program:

Prompt: You may call search_datastore and then search_wikipedia to gather information. Preferably call both. Then answer the question.

Question: Is induction valid in some cases, particularly when doing statistics?

The agent first makes a search_datastore call:

[Calling search_datastore with args {'datastore_query': 'validity of induction in statistics'}]
[Result]: item_#: 5
page_#: 40
item_id: ch3
file_path: documents\Karl Popper - The Two Fundamental Problems of the Theory of Knowledge-Routledge (2008).epub
book_title: The Two Fundamental Problems of the Theory of Knowledge
paragraph_#: 26
chapter_title: Chapter III THE PROBLEM OF INDUCTION
And yet, all these arguments together still do not suffice to guarantee the validity of the principle of induction.
The inference from our observations as to the validity of the principle of induction in turn represents a generalisation, an inductive inference. And in this generalisation again we (tacitly or explicitly) make an assumption analogous to the earlier assumption formulated in the principle of induction. The only difference is that this time it consists not in the induction of a natural law, but in the induction of a principle of induction.

...

Then it calls search_datastore again with a different question:

[Calling search_datastore with args {'datastore_query': 'inductive reasoning in statistics'}]
[Result]: page_#: 365
file_path: documents\Realism and the Aim of Science -- Karl Popper -- 2017.pdf
book_title: Realism and the Aim of Science -- Karl Popper -- 2017
section_name: CRITICISM OF PROBABILISTIC INDUCTION
Inductive reasoning is in some respects similar to deductive reasoning, although not quite as conclusive. This situation may be explained by assuming that a conclusive (or an almost conclusive) argument corresponds to the probability 1, an inconclusive argument to a lower probability. Thus inductive reasoning will be part of probability logic; and so will some form or other of the simple inductive rule which is the simplest and most elementary form of all inductive reasoning.    

...

Then it tries search_wikipedia:

[Calling search_wikipedia with args {'wiki_page_search': 'Statistical inference', 'question': 'statistical inference'}]
[Result]: Statistical inference is the process of using data analysis to infer properties of an underlying probability distribution. Inferential statistical analysis infers properties of a population, for example by testing hypotheses and deriving estimates. It is assumed that the observed data set is sampled from a larger population.
Inferential statistics can be contrasted with descriptive statistics. Descriptive statistics is solely concerned with properties of the observed data, and it does not rest on the assumption that the data come from a larger population. In machine learning, the term inference is sometimes used instead to mean "make a prediction, by evaluating an already trained model"; in this context inferring properties of the model is referred to as training or learning (rather than inference), and using a model for prediction is referred to as inference (instead of prediction); see also predictive inference.
 Have you called both functions yet? If not, please do so before answering.

With all that information researched, here is the research agents final response to me:

Response:
Based on the information gathered:

*   **Induction is not deductively valid.** Karl Popper emphasizes that induction is never valid in the sense of deductive reasoning. In cases where all instances have been observed, it essentially becomes deduction.
*   **Induction is used in statistics.** Statistical inference uses data analysis to infer properties of an underlying probability distribution. It infers properties of a population by testing hypotheses and deriving estimates.
*   **Induction is related to probability.** Some sources suggest inductive reasoning can be related to probability logic, where conclusive arguments have a probability of 1, and inconclusive arguments have lower probabilities.
*   **Alternative perspectives exist.** Some argue that inductive reasoning has its own standards and can be considered "inductively valid" if it conforms to those standards. However, this doesn't make it deductively valid.

In summary, while induction is not deductively valid, it is used in statistics to make inferences about populations based on observed data. The validity of induction in statistics relies on probabilistic reasoning and specific inductive standards rather than deductive certainty.

That's a pretty impressive result!

This example gives you the basic idea of both how to do function calling for Google's Gemini and also how to create a very basic Large Language Model agent to do research for you.

SHARE


comments powered by Disqus

Follow Us

Latest Posts

subscribe to our newsletter