Lab 4

Financial Research Agent

Turn the Lab 3 RAG system into an agent that decides for itself which tool to use, then pauses for human approval on high-stakes answers.

90 min3 toolsReAct loop6 provider optionsModule 4

The Colab button loads the notebook from the public GitHub repository, so it works only after the notebook is pushed there. Until then, download it and use Colab's File > Upload notebook.

What you will build

An agent with three tools. It reads a question, decides whether to search the documents, calculate a ratio or summarise a page, looks at what the tool returned, and repeats until it can answer. For high-stakes questions a person must approve the answer before it is returned.

Question→Model picks a tool→Tool runs→Result back to model→Final answer→Human approval if high-stakes

Prerequisites

  • ▸Read Module 4 sections 1, 2, 4 and 8 (agents, tools, ReAct, controls).
  • ▸Lab 3 is recommended. Its index is rebuilt inside this notebook, so you can still start here.
  • ▸Access to one model provider that supports tool calling (below).

Pick a model provider

Only the model changes between providers. The tools, the loop and the approval step are identical. Free options exist, but free tiers have rate limits and their terms change, so check each provider's current page.

OllamaFree, local
Run the notebook locally (Colab cannot reach your machine). Pick a model tagged "tools" in the Ollama library, for example llama3.1:8b. Small models call tools less reliably.
GroqFree tier
Fast hosted open models such as llama-3.3-70b-versatile. Rate-limited, and every agent step is a request.
Google GeminiFree tier
Key from Google AI Studio, used through Google's OpenAI-compatible endpoint. Use gemini-2.5-flash; newer Gemini versions need extra fields this notebook does not handle.
OpenRouterFree models, small limits
One key, many models. The openrouter/free router picks a free model that supports tools. Free daily limits are small, so keep max_steps low.
OpenAIPaid
gpt-4o-mini is a cheap default with reliable tool calling.
Anthropic ClaudePaid
Reliable tool use. The notebook converts messages into Anthropic's format for you.

Tip: agents depend on reliable tool calling. If a small free model keeps ignoring the tools, that is the model, not your code. Move up to a larger one.

Exercises

0

Choose your model provider

Why we do this: An agent needs a model that supports tool calling: instead of plain text, the model replies with a request such as 'call calculate_ratio with these numbers'. Not every model does this well, so the choice of model matters more here than in Lab 3.

What the code does: Defines one chat(messages, tools) function that works with six providers and always returns the reply in the same shape. Five providers share the OpenAI-style API. Anthropic has its own API, so a small adapter converts the messages.

Key terms
  • ▸Tool calling (function calling): the model asks for a named tool with arguments, and your program runs it.
  • ▸OpenAI-compatible API: many providers copy OpenAI's request format, so one code path serves them all.
PROVIDER = "groq"   # ollama | groq | gemini | openrouter | openai | anthropic

def chat(messages, tools=None):
    ...  # returns {"role": "assistant", "content": str, "tool_calls": [...]}

Expected output: A short greeting. An authentication or connection error means a wrong key or, for Ollama, that the server is not running or the model is not pulled.

What to notice: Model names and free-tier limits change often. If a model is rejected, check the provider's current list and edit MODEL. Limits count every model call, and an agent makes several per question.

1

Rebuild the Lab 3 index (recap)

Why we do this: The agent's first tool searches documents, so it needs the same ChromaDB index as Lab 3. The notebook is self-contained, so you do not need the Lab 3 notebook open.

What the code does: Downloads the SEBI memo, reads each page, cuts it into overlapping chunks, and stores them in Chroma with document and page metadata. It also keeps the raw text of every page in PAGES, because the third tool needs whole pages.

PAGES = {}   # {file name: {page number: text}}
col = chromadb.Client().get_or_create_collection("lab4")
col.add(ids=ids, documents=docs, metadatas=metas)

Expected output: The document names and a chunk count. If PAGES is empty, no PDF was found in the data/ folder.

What to notice: Any extra PDF you place in data/ (for example the RBI Master Direction from Lab 3) is loaded automatically.

2

Define three tools

Why we do this: A tool is an ordinary Python function plus a short description the model can read. The model never runs code. It only asks for a tool by name, and your program runs it. The description is how the model decides which tool fits (Module 4, section 2).

What the code does: search_documents looks up passages in the index. calculate_ratio does exact arithmetic in Python, because models are unreliable at arithmetic. summarise_section fetches one whole page and asks the model to summarise it. You write run_tool, which runs a tool by name and returns its result, or an error message instead of crashing.

Key terms
  • ▸Tool schema: the JSON description (name, purpose, parameters) sent to the model.
  • ▸Deterministic: the same input always gives the same output, like calculate_ratio.
def run_tool(name: str, arguments: str) -> str:
    # YOUR CODE HERE:
    # 1. Map tool names to the functions above.
    # 2. json.loads the arguments, call the function, return str(result).
    # 3. Unknown name or any exception -> return "Error: ..."
    raise NotImplementedError

Expected output: Running the three checks prints 13.50%, then an error about a zero denominator, then an error about an unknown tool. The errors come back as text and are not raised.

What to notice: Returning errors as text lets the model read them and try something else, instead of the whole agent crashing.

Finance link: calculate_ratio keeps every number reproducible. An auditor can re-run the exact calculation, which a model's mental arithmetic does not allow.

3

The ReAct loop

Why we do this: ReAct means reason, act, observe, repeat (Module 4, section 4). The model works out what it needs, requests a tool, sees the result, and decides whether it can answer or needs another tool.

What the code does: Keeps a growing messages list. Each round it asks the model what to do. If the reply contains tool calls, the loop runs each one and appends the result as a tool message. If the reply has no tool calls, that reply is the final answer. max_steps stops the loop if the model never finishes.

Key terms
  • ▸Step limit: a hard cap on rounds. It prevents infinite loops and runaway cost.
  • ▸Trace: the printed record of each tool call and its result.
for step in range(1, max_steps + 1):
    msg = chat(messages, TOOLS)
    messages.append(msg)
    if not msg.get("tool_calls"):
        return msg["content"]          # final answer
    for call in msg["tool_calls"]:     # act, then observe
        result = run_tool(call["function"]["name"], call["function"]["arguments"])
        messages.append({"role": "tool", "tool_call_id": call["id"], "content": result})

Expected output: A trace showing a search_documents call and then a final answer citing the SEBI memo. The exact query the model writes will vary.

What to notice: The model chose to search without being told to. Ask for a capital adequacy ratio from two numbers and you should see calculate_ratio instead, with no search. A model that answers without any tool call, or writes the call as plain text, does not support tool calling well. Try a larger model or another provider.

Finance link: The step limit is also a cost control. Each round is a paid or rate-limited model call (Module 4, section 8).

4

Human-in-the-loop checkpoint

Why we do this: Some answers should not go out without a person checking them. In finance that includes anything that looks like a compliance conclusion, an approval or a recommendation (Module 4, section 8, autonomy and control).

What the code does: is_high_stakes flags a question using keywords. You write run_with_approval, which runs the agent and, for flagged questions, shows the draft and asks a human to approve before returning it. A rejected answer is withheld.

def run_with_approval(question: str) -> str:
    # YOUR CODE HERE:
    # draft = run_agent(question)
    # if not high-stakes: return draft
    # else show draft, ask input("Approve? (y/n)"), return draft only on y
    raise NotImplementedError

Expected output: A normal question returns straight away. A flagged question prints the draft and waits for you to type y or n.

What to notice: A keyword list is a crude screen. It will miss some high-stakes questions and flag harmless ones. Real systems use risk tiers, classifiers and policy review. This is the simplest possible version of the idea.

Finance link: A human sign-off creates the accountability record that regulators expect for AI-assisted decisions (Module 5, sections 2 and 6).

5

Test the agent

Why we do this: Agents fail in new ways: picking the wrong tool, looping, or answering without evidence. You run three different tasks and read the trace each time.

What the code does: Runs three questions: a document question that should use search, a capital adequacy calculation that should use calculate_ratio and then pause for approval, and a page summary that should use summarise_section.

tests = [
    "According to the SEBI memo, who is responsible for the output of AI tools?",
    "A bank has total capital of 1,350 and risk-weighted assets of 10,000. What is its capital adequacy ratio, and does it meet a 9% minimum?",
    "Summarise page 3 of the SEBI memo (file sebi_ai_memo.pdf).",
]

Expected output: Question 1 uses search. Question 2 uses calculate_ratio (13.50%) and asks for your approval. Question 3 uses summarise_section. If a different tool is chosen, read the tool descriptions, because they are what the model relies on.

What to notice: Make one tool description vague and re-run. Tool selection usually gets worse, which shows how much the description matters. The 9% minimum in question 2 is supplied in the question: check the real figure against the RBI's Basel III capital regulations before relying on it.

Stretch goals

  • ▸Add a fourth tool, such as list_documents(), and see whether the agent uses it.
  • ▸Add the RBI Master Direction and ask a question that needs both documents.
  • ▸Count tool calls and rough token use per question to estimate cost.

Common errors

  • ▸The model never calls a tool: use a model tagged "tools" (Ollama) or switch provider.
  • ▸400 error about tools or thought signatures (Gemini): use gemini-2.5-flash.
  • ▸429 or rate limit: wait, or lower max_steps. Each round is one request.
  • ▸NotImplementedError: you have not completed run_tool or run_with_approval yet.