⭐ You can better see the step-by-step changes and the complete codebase for this article at https://bchaicourse.github.io/HAI2026-Interactive-Slides/lecture2 ⭐
This post continues from Building an Interactive Data Analysis Tool (Part 1). In that post, we built a Streamlit app where users filter a movie dataset with sidebar controls and ask questions in natural language. The app used a hardcoded 3-step pipeline to answer those questions: generate Python code, execute it, and interpret the result. It worked, but the pipeline was rigid. If the generated code failed, the app showed the error and stopped. If a question needed a different approach, the pipeline could not adapt.
In this post, we turn that static pipeline into an agentic system. Instead of following fixed steps, the LLM will choose its own tools, reason about intermediate results, retry when something goes wrong, and pause for human approval before taking action.
<aside> 🛠
What we will build: An upgraded version of the Part 1 app where the LLM autonomously selects tools, reasons through multi-step analysis, self-corrects when code fails, and requests human approval before executing actions. The result is a data analysis agent that can recover from mistakes and respond to user feedback in real time.
You can find the 📦 complete implementation 📦 at the github link below:
https://github.com/bchaicourse/HAI2026-Week5-Practice/tree/main
</aside>
<aside> 💡
What you will learn:
Function calling lets the LLM choose which tools to use and with what arguments, instead of following a fixed pipeline. Before applying this to our data analysis tool, let's understand how it works with a simple example.
A tool definition tells the LLM what a tool does and what arguments it expects. The OpenAI SDK provides pydantic_function_tool to generate tool definitions from Pydantic models:
from openai import pydantic_function_tool
from pydantic import BaseModel, Field
class Plus(BaseModel):
"""Add two numbers together."""
a: float = Field(description="The first number")
b: float = Field(description="The second number")
"""Add two numbers together.""") becomes the tool's descriptionCalling pydantic_function_tool(Plus) converts this Pydantic model into a JSON structure:
{
"type": "function",
"function": {
"name": "Plus",
"strict": true,
"parameters": {
"description": "Add two numbers together.",
"properties": {
"a": { "description": "The first number", "title": "A", "type": "number" },
"b": { "description": "The second number", "title": "B", "type": "number" }
},
"required": ["a", "b"],
"title": "Plus",
"type": "object",
"additionalProperties": false
},
"description": "Add two numbers together."
}
}
You don't need to memorize this structure. It's a predefined format for communicating tool information between your code and the API. The key fields to understand are:
name: The class name, used as the tool identifier.description: From the docstring. The LLM reads this to decide when to use the tool.