Agents, Not If-Else: A Friendly Intro to Tool-Calling with CSVs

Agents, Not If-Else: A Friendly Intro to Tool-Calling with CSVs

posted Originally published at www.balysnotes.com 8 min read

Imagine this: You have an Excel or CSV (or any other) dataset, and you want quick, clear answers from it. Maybe something simple on the lines of: "What’s the average price by country?".
Also, you do not use Excel (or you just hate Microsoft and are an apple junkie, despite their recent uninspired "innovations" like re-arranging cameras and renaming abbreviations like AI), and you definitely do not have a data science degree and aren't planning on getting one.

With AI (or AGI, or ASI, or whichever new acronym the industry comes up with tomorrow), you probably don’t have to. Upload your file, ask your question, and the assistant will inspect the columns, validate types, choose the right calculation, and deliver a crisp summary. No data science degree and no big budgets on Microsoft software - just useful insights from your data.

That is the magic of agentic AI and tool-calling.

How Agentic AI Solves the Spreadsheet Sweat

Think of an "agent" as a seasoned analyst who:

  • Understands your question in plain language.
  • Chooses the right tools (average, sum, min, max).
  • Validates the data and asks clarifying questions when things are ambiguous.
  • Returns a concise answer with just the essentials.

Tool-calling is simply how the agent gets real work done: it calls small, well-defined functions (tools) to compute things accurately. If you say "average price," the agent picks the average tool, ensures "price" exists and is numeric, and then computes it.

No more trying to predict every user action with a jungle of if-else branches. People are gloriously unpredictable - remember all that blockchain hype? Door locks on the blockchain, toasters on the blockchain... in what world would your smart bed need blockchain?
Instead, define small, composable tools with clear contracts, and let the agent do the orchestration, validation and computation:

  • Infer intent from natural language
  • Inspect schema or preview to confirm columns and types
  • Select the right tool (average, sum, min, max)
  • Supply the correct parameters
  • Catch and explain errors in plain English
  • Ask clarifying questions when things are ambiguous

Even your grandpa or grandma does not need to video call you to ask how to answer a call on Skype (also, who uses Skype nowadays). The AI handles everything and picks the correct inputs for your tools so that users can just ask, and they get answers.

What Is Tool-Calling, Really?

If LLMs are your conversational layer, tools are your calculators, databases, and business logic. The LLM doesn’t need to "guess" results - it orchestrates:

  • Which tool to call
  • With which parameters
  • In what order
  • And how to present the answer back to you

This is how LLMs stop hallucinating numbers and start doing grounded, verifiable work.

A Friendly Real-World Aside

This shift is already happening in the industry — and at my workplace, farmbetter — where we’re equipping our WhatsApp bot with AI. Instead of hardcoding every flow and error message, we let an agent validate inputs, ask follow-up questions, and run the right tools. The result: more natural conversations and fewer dead ends.

Also: the UI for this demo is AI-generated. Because of course it is.

Try It Yourself

Upload a CSV and chat with it like a human. Ask "Total revenue by country" or "Average price." The agent will do the rest.

Live demo: [https://ask-your-csv-agent.vercel.app]
GitHub: [https://github.com/TylerMutai/ask-your-csv-agent.git]

If you just want the big picture, you can stop here. For devs who want to see how the sausage is made, do read on.


Hands-On: Building a CSV Agent with Next.js and TypeScript

We’ll build a simple analysis agent with four tools: average, sum, min, and max. The agent:

  • Works only with the loaded CSV
  • Never guesses column names or types
  • Validates numeric columns before computing
  • Stays concise and helpful (no words of affirmation like: Spectacularly superb question)

1) Define the Agent

Before we begin: This guide uses OpenAI’s Agent SDK in a Next.js app with TypeScript (@openai/agents).
If you prefer Python, there's a Python SDK with the same a very similar API.
On another stack? The patterns and concepts highlighted in this guide map cleanly to other AI providers (e.g., Google Gemini - I have another blog on this focused also on AI and tool calling that you can check out (here)[https://www.balysnotes.com/integrating-model-context-protocol-with-gemini].

Also, here's the entire project codebase: [https://github.com/TylerMutai/ask-your-csv-agent.git]

Moving on...

We’ll give the agent clear instructions and register our tools. The agent decides when to call them.

import {Agent} from "@openai/agents";
import averageTool from "@/tools/average";
import minTool from "@/tools/min";
import maxTool from "@/tools/max";
import sumTool from "@/tools/sum";

const mainAgent = new Agent({
  name: 'Assistant',
  instructions: `
You are a data analysis agent that answers questions about a CSV file. Your job is to compute accurate summaries (e.g., average, sum, count, min, max), optionally grouped by one or more columns, and return concise, helpful answers.

Behavior
- Work only with the currently loaded CSV. If no file is loaded, ask the user to upload one.
- Never guess column names or types. When unsure, inspect the schema or preview a few rows first.
- Validate that target columns exist and are numeric before computing numeric metrics; if not, ask a clarifying question or suggest valid columns.
- Prefer aggregated results over raw row dumps. If a result set would be large, summarize and show only the top 10 groups.
- Be concise: provide a one-sentence conclusion followed by a small, readable table of results (if applicable). Include units and time ranges when known.
- For ambiguous requests (e.g., "average price"), ask which column to use or propose likely matches.
- If grouping is requested (e.g., "by country" or "per month"), perform a grouped aggregation and sort results sensibly.
- If an operation is not possible (missing column, wrong type), explain the issue and propose alternatives.

Tool use
- Use schema/columns tools before calculations if uncertain.
- Use preview to verify formatting (e.g., dates).
- Use aggregate tools for grouped metrics.
- Use filter/query tools for conditions.
- Use a plotting tool when asked or when trends are natural.

Constraints and formatting
- Limit previews to ≤20 rows.
- Limit output tables to ≤10 rows.
- Do not fabricate values or columns.
- Return answers in this order:
  1) Short conclusion (one or two sentences).
  2) Compact table with clear headers.
  3) Notes about assumptions or limitations (only if relevant).
`,
  tools: [averageTool, minTool, maxTool, sumTool],
});

export default mainAgent;

Key idea: the LLM doesn’t compute - your tools do. The agent just decides which tool to call and how.

2) Create a Tool: Average

We’ll parse the CSV safely, validate columns, and compute the mean. If something’s off, we throw precise errors that the agent turns into friendly explanations.

import {tool} from "@openai/agents";
import {z} from "zod/v3";
import parseAndFormatCsvData from "@/utils/parseAndFormatCsvData";

const averageTool = tool({
  name: 'average_tool',
  description: 'Finds the average of a given set of data.',
  parameters: z.object({
    data: z.string(),
    columns: z.array(z.string()),
  }),
  execute: async ({data, columns}) => {
    const records = parseAndFormatCsvData({data, columns});
    const averages: { [key: string]: number } = {};

    for (const column of columns) {
      const sum = records.reduce((acc: number, row: { [key: string]: unknown }) => acc + Number(row[column]), 0);
      averages[column] = sum / records.length;
    }

    return `averages according to columns: ${JSON.stringify(averages)}`;
  },
});

export default averageTool;

And here’s our CSV parser and validator:

import {parse} from "csv-parse/sync";

const parseAndFormatCsvData = ({data, columns}: { data: string, columns: string[] }) => {
  const records: { [key: string]: unknown }[] = parse(data, {
    columns: true,
    skipEmptyLines: true,
    skipRecordsWithError: true,
  });

  for (const record of records) {
    for (const column of columns) {
      if (!record[column]) {
        throw new Error(`Column ${column} not found in data`);
      }
      if (isNaN(Number(record[column]))) {
        throw new Error(`Column ${column} contains non-numeric values`);
      }
    }
  }

  return records;
};

export default parseAndFormatCsvData;

Why this matters: instead of a generic "Something went wrong," the agent can say "Column price contains non-numeric values; try unit_price instead?" That’s kinder UX and better debugging.

3) Run the Agent on an API Route

We’ll post chat messages plus the CSV to an API route. The agent gets the CSV as context and replies with a concise answer.

import {AgentInputItem, run} from "@openai/agents";
import mainAgent from "@/agents/mainAgent";

export async function POST(req: Request) {
  try {
    const {csvData, messages} = await req.json();

    const conversationMessages: AgentInputItem[] = messages.map((msg: { role: string; content: string }) => ({
      role: msg.role,
      content: [{type: msg.role === 'assistant' ? 'output_text' : 'input_text', text: msg.content}],
    }));

    const result = await run(mainAgent, conversationMessages, {
      context: csvData,
    });

    return Response.json({response: result.finalOutput});
  } catch (error) {
    console.error("Chat API error:", error);
    return Response.json({error: "Failed to process request"}, {status: 500});
  }
}

Note: You can also inject the CSV as the first user message if you prefer; passing it as context keeps prompts tidy.

4) A Minimal Frontend Chat

A simple chat interface in Next.js. Paste this into a client component and wire it to your API route.

"use client";

import {useEffect, useRef, useState} from "react";

interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
  timestamp: Date;
}

export default function ChatInterface({csvData, fileName, onReset}: {csvData: string; fileName: string; onReset: () => void;}) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({behavior: "smooth"});
  }, [messages]);

  const handleSendMessage = async (userMessage: string) => {
    if (!userMessage.trim()) return;

    const newUserMessage: Message = {
      id: Date.now().toString(),
      role: "user",
      content: userMessage,
      timestamp: new Date(),
    };

    setMessages((prev) => [...prev, newUserMessage]);
    setIsLoading(true);

    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({
          csvData,
          messages: [...messages, newUserMessage].map((m) => ({role: m.role, content: m.content})),
        }),
      });

      const data = await response.json();
      const assistantMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: "assistant",
        content: data.response ?? "No response",
        timestamp: new Date(),
      };
      setMessages((prev) => [...prev, assistantMessage]);
    } catch (e) {
      setMessages((prev) => [...prev, {
        id: (Date.now() + 2).toString(),
        role: "assistant",
        content: "Sorry, I encountered an error. Please try again.",
        timestamp: new Date(),
      }]);
    } finally {
      setIsLoading(false);
    }
  };

  // Render your message list and input here
  return <div>
    {/* Message list */}
    <div ref={messagesEndRef} />
  </div>;
}

Why Tool-Calling Changes the UX (and Programming)

  • Natural interaction: Users ask for outcomes, not functions. "Top 10 customers by spend" beats "Click Reports → Pivot...".
  • Better error handling: Instead of 400 Bad Request, the agent explains what went wrong and suggests fixes.
  • Fewer edge-case branches: Devs define tools and constraints; the agent orchestrates logic paths on the fly.
  • Maintainability: Add or swap tools without rewriting flows.
  • Safer by design: Validation happens before computation; the agent refuses to guess.

What’s Next

  • Add group-by and filter tools, and a simple plotting tool for trends.
  • Stream results and clarifying questions.
  • Integrate with WhatsApp (hello, farmbetter -)), Slack, or internal dashboards.
  • Keep the UI simple - let the agent handle the complexity.

Wrapping Up

We’re moving from "write all the if-else" to "define good tools, then let agents orchestrate." Users win with clearer answers and fewer roadblocks. Developers win with cleaner boundaries and reusable logic.

How might agentic AI simplify your next product? Try the demo, peek at the code, and tell me what you build next!

If you read this far, tweet to the author to show them you care. Tweet a Thanks

More Posts

Integrating Model Context Protocol with Gemini: The Definitive Guide to Modern Tool Calling

Brian Baliach - Aug 14

AI Agents Explained Simply (and with Humor) - Almost Human Webseries

Nikhilesh Tayal - Sep 16

Building an AI-Powered Restaurant Management System with OpenAI Agents SDK

Ramandeep Singh - Jun 30

An internal AI implementation is not a weekend hackathon project.

Nikhilesh Tayal - Aug 26

AI: An Engineer’s Ally, Not a Replacement

Vladimir Semenov - Jul 18
chevron_left