top of page

Agents, Tools, and Skills for a Working Mini AI Assistant

Updated: 2 days ago

Ask an AI chatbot what the weather was like in Chicago last week and it will guess. Give that same chatbot a tool that actually checks, and it will know. That gap, between a model that guesses and a system that knows, is exactly what agents, tools, and skills close. It's also why generative AI has become the most talked-about subject in IT. The good news is that the barrier to entry is lower than it looks. If you already know how to do a search in Google, you already know how to write a decent prompt. However, one prompt alone will only get you so far.  The real magic starts when you stop asking a model to know everything and start giving it agents, tools, and skills to lean on instead. This article walks through all three, with working code you can run yourself, using a real trip-planning example from start to finish.

 

What are tools, skills, and agents?

 

Let’s start by defining these concepts and walking through some practical examples.

 

Tools are generally provided by an outside source like an MCP (model context protocol) server.  When calling that server, you will generally be given a list of tools to help you with your prompt.  Let's suppose you want to provide information about the weather inside your prompt.  If you are calling the Weather MCP server, you will have access to the below tools.   The get_forecast tool will provide you with the current forecast.  If you wrote a prompt asking how much the weather changed in Chicago over the past week, this would help add the right instruction to make your search more successful.

 

  get_current_conditions

  get_forecast

  get_alerts

  get_historical_weather

  get_air_quality

  get_marine_conditions

  get_river_conditions

  get_wildfire_info

  search_location

  check_service_status

 

These are just examples of what a Tools MCP server might expose.  Our own working example builds two simpler custom tools instead, a calculator and a distance converter.

 

Skills are more like recipes.  If you are writing an article for Medium you can make one of your skills a style guide.  For most data engineering projects, these are often a list of calculations or a list of comparable values.  Think of these as lookup tables.  If your prompt asked to look up prices for all flights out of a Chicago area airport, the skill may plug in ORD (O’Hare), MDW (Midway), or CHI (all Chicago-area airports).  Below are common measurements that could be built into your skill

 

  Temperature. Celsius, Fahrenheit, Kelvin

  Length / Distance. Inches, Feet, Yards, Miles, Millimeters, Centimeters, Meters, Kilometers

  Mass / Weight. Ounces, Pounds, Stone, Grams, Kilograms, Metric Tons

  Volume / Capacity. Fluid Ounces, Cups, Pints, Quarts, Gallons, Milliliters, Liters, Cubic Meters

  Area. Square Feet, Square Meters, Acres, Hectares

  Speed. Miles per Hour (mph), Kilometers per Hour (km/h), Knots, Meters per Second (m/s)

  Time Zones. UTC/GMT offsets, Daylight Saving Time (DST) transitions, Unix timestamps to human-readable dates

  Digital Storage. Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)

  Currency. Live or historical exchange rates between global fiat currencies (USD, EUR, JPY, GBP) and major cryptocurrencies (BTC, ETH)

 

Like the tools above, this list is a broader illustration of what a skill's lookup table might contain.  Our working example only needs the distance rows, converting between miles, kilometers, meters, and feet.

 

Agents are what makes the AI process appear to reason.  In reality, your prompt goes through multiple loops that build onto the original prompt.  Your first prompt gives the system enough instruction to make the first step.  It then decides what the next step should be, and keeps going depending on how sophisticated your code actually is.  Think of it like a Choose Your Own Adventure book.   Each prompt may go in a different direction to find the appropriate answer.

 

Experiment with Sample Code.

 

For this code, we will follow this very simple architecture.  This code can be written in a Jupyter Notebook or Google Colab.  It is completely free and easy to adjust on your own.  As we look at each piece, please note that we will discuss this in a backwards order.  This part explains the code blocks, while the next section will show how everything actually works.


 


 Step 1 – Import the needed libraries

 

The requests, getpass, and os libraries are used to fetch the token and call the Groq LLM. The re library will help us find characters inside the prompt.  It is used in all of our tools and skills.

 

NOTE: You will need Python 3.10 or higher for this to work.  Also, if requests is not installed, run this command first: !pip install requests.

 

import re

import os

import getpass

import requests

 

Step 2 – Create a calculator tool

  

This tool scans the prompt for every dollar amount (using a regular expression) and adds them up.  As you can see, it is just looking for a $ followed by some number.  The numeric amounts are then summed together.  If you are using British pounds you would have to update this code.

  

def calculator_tool(prompt: str) -> str:

    print("    [Tool 1: Calculator] scanning prompt for dollar amounts...")

    dollar_amounts = re.findall(r'\$\s?(\d+(?:\.\d{1,2})?)', prompt)

 

    if not dollar_amounts:

        result = "no dollar amounts found"

        print(f"    [Tool 1: Calculator] {result}")

        return result

 

    values = [float(a) for a in dollar_amounts]

    total = sum(values)

    breakdown = " + ".join(f"${v:g}" for v in values)

    result = f"{breakdown} = ${total:.2f}"

    print(f"    [Tool 1: Calculator] found {len(values)} amount(s) {values} -> {result}")

    return result

 

Step 3 – Create the unit converter tool

 

This tool simply converts common units of distance.  It normalizes whatever unit you used (the UNIT_ALIASES dictionary) and then converts every leg to miles (the DISTANCE_TO_MILES dictionary).  This makes it easier to add everything up. You may want to add a miles-to-kilometers converter or a miles-to-steps converter depending on your use case.

 

UNIT_ALIASES = {

    "kilometers": "km", "kilometer": "km", "km": "km",

    "mile": "miles", "miles": "miles",

    "m": "meters", "meter": "meters", "meters": "meters",

    "ft": "feet", "foot": "feet", "feet": "feet",

}

 

DISTANCE_TO_MILES = {"km": 0.621371, "miles": 1.0, "meters": 0.000621371, "feet": 0.000189394}

 

def unit_converter_tool(prompt: str) -> str:

    print("    [Tool 2: Unit Converter] scanning prompt for distance legs...")

    legs = re.findall(r'(\d+(?:\.\d+)?)\s*(kilometers?|km|miles?|meters?|feet|ft)\b',

                       prompt, re.IGNORECASE)

 

    if not legs:

        result = "no distances found"

        print(f"    [Tool 2: Unit Converter] {result}")

        return result

 

    total_miles = 0.0

    breakdown = []

    for value, unit in legs:

        value = float(value)

        unit_norm = UNIT_ALIASES.get(unit.lower(), unit.lower())

        total_miles += value * DISTANCE_TO_MILES.get(unit_norm, 1.0)

        breakdown.append(f"{value:g} {unit_norm}")

    result = f"{' + '.join(breakdown)} = {round(total_miles, 2)} miles total"

    print(f"    [Tool 2: Unit Converter] found {len(legs)} leg(s) -> {result}")

    return result

 

Step 4 – Create the summarizer skill

 

This is just a lightweight summarizer.  It scores each sentence by stripping out the stop words shown in the stopwords set, then ranking what's left by how many meaningful words it contains.  This will help us remove sentences that do not add any value to our prompt and remove unnecessary token use.

 

def summarizer_skill(text: str, max_sentences: int = 2) -> str:

    print("    [Skill: Summarizer] scanning message for key sentence(s)...")

    sentences = [s for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s]

 

    if len(sentences) <= max_sentences:

        print(f"    [Skill: Summarizer] only {len(sentences)} sentence(s) -- returning as-is")

        return text.strip()

 

    stopwords = {"the","a","an","is","are","was","were","in","on","at","to","of","and",

                 "or","for","it","this","that","i","you","he","she","they","we","really"}

    words = re.findall(r'\b\w+\b', text.lower())

    freq = {}

    for w in words:

        if w not in stopwords:

            freq[w] = freq.get(w, 0) + 1

 

    scored = []

    for idx, sentence in enumerate(sentences):

        s_words = re.findall(r'\b\w+\b', sentence.lower())

        score = sum(freq.get(w, 0) for w in s_words)

        scored.append((score, idx, sentence))

 

    top = sorted(scored, key=lambda x: x[0], reverse=True)[:max_sentences]

    top_in_order = sorted(top, key=lambda x: x[1])

    summary = " ".join(s for , , s in top_in_order)

    print(f"    [Skill: Summarizer] kept {len(top_in_order)} of {len(sentences)} sentence(s) -> {summary}")

    return summary

 

 Step 5 – Connect to your LLM

 

This is where you connect to your LLM to get an actual response.  We are using Groq because it is free.  However, you will need to get your own API key from https://console.groq.com/keys to get this to work.

 

GROQ_API_KEY = os.environ.get("GROQ_API_KEY") or getpass.getpass(

    "Enter your free Groq API key (from https://console.groq.com/keys), "

    "or press Enter to skip: "

)

 

GROQ_MODEL = "openai/gpt-oss-20b"

 

def call_llm(augmented_prompt: str,

             system_prompt: str = "You are a helpful, concise assistant.") -> str:

    if not GROQ_API_KEY:

        return ("[No LLM reply -- no Groq API key was provided. Get a free one at "

                 "https://console.groq.com/keys, then re-run the setup cell above.]\n"

                 f"Here is the augmented prompt that would have been sent:\n\"\"\"\n{augmented_prompt}\n\"\"\"")

    try:

        response = requests.post(

            GROQ_ENDPOINT,

            headers={

                "Content-Type": "application/json",

                "Authorization": f"Bearer {GROQ_API_KEY}",

            },

            json={

                "model": GROQ_MODEL,

                "messages": [

                    {"role": "system", "content": system_prompt},

                    {"role": "user", "content": augmented_prompt},

                ],

                "temperature": 0.7,

                "max_tokens": 400,

            },

            timeout=30,

        )

        response.raise_for_status()

        data = response.json()

        return data["choices"][0]["message"]["content"].strip()

    except requests.exceptions.RequestException as e:

        return f"[LLM request failed -- {e}]"

    except (KeyError, IndexError, ValueError):

        return "[LLM returned an unexpected response format.]"

 

print("LLM configured." if GROQ_API_KEY else "No key entered -- running in fallback mode.")

 

Step 6 – Build your agents

 

This is where you set up your agents.  The agents add additional context to your prompt.  Notice how they call tools and skills to get accurate data for the prompt.

 

def show_step(step_num: int, label: str, content: str) -> None:

    """Small helper so every agent prints its pipeline the same, readable way."""

    print(f"\n  STEP {step_num} - {label}:")

    for line in str(content).splitlines() or [""]:

        print(f"    {line}")

 

def trip_planner_agent(prompt: str) -> str:

    """Agent 1. Condition: 2+ dollar costs AND 2+ distances -- a multi-stop itinerary."""

    print("[Router] -> Agent 1: Trip Planner Agent activated (detected an itinerary: multiple costs + multiple distances)")

    show_step(1, "Original prompt", prompt)

 

    cost_result = calculator_tool(prompt)

    show_step(2, "Tool result (Calculator -- total cost)", cost_result)

 

    distance_result = unit_converter_tool(prompt)

    show_step(3, "Tool result (Unit Converter -- total distance)", distance_result)

 

    augmented_prompt = (

        f"The user asked: \"{prompt}\"\n\n"

        f"A calculator tool already computed the total cost: {cost_result}\n"

        f"A distance tool already computed the total distance traveled: {distance_result}\n\n"

        "Using those two verified totals (don't redo either calculation yourself), give the "

        "user a short, friendly trip summary that reports both totals clearly."

    )

    show_step(4, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

 

    reply = call_llm(augmented_prompt)

    show_step(5, "Final response from Groq", reply)

 

    return f"🧳 Agent 1: Trip Planner Agent:\n{reply}"

 

def text_agent(prompt: str) -> str:

    """Agent 2. Condition: default for any prompt, OR chained after Agent 1

    when the itinerary prompt also has an extra narrative sentence."""

    print("[Router] -> Agent 2: Text Analysis Agent activated")

    show_step(1, "Original prompt", prompt)

 

    summary = summarizer_skill(prompt)

    show_step(2, "Skill result (Summarizer)", summary)

 

    augmented_prompt = (

        f"The user wrote: \"{prompt}\"\n\n"

        f"Automatic summary of their message: {summary}\n\n"

        "Write a short, thoughtful, natural-sounding reply to the user that responds "

        "to what they actually said, informed by (but not just repeating) this summary."

    )

    show_step(3, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

 

    reply = call_llm(augmented_prompt)

    show_step(4, "Final response from Groq", reply)

 

    return f"📝 Agent 2: Text Analysis Agent:\n{reply}"

 

Step 7 – Route to the correct agent

 

The router is really the brains of the operation.  It looks for certain characteristics in your prompt and then sends it to the right agent(s).  In this situation, has_extra_narrative is a condition the router checks only after the trip_planner_agent already matched, and the text_agent is called either as the default for any prompt or alongside the trip_planner_agent whenever that condition comes back true.

 

def looks_like_itinerary(prompt: str) -> bool:

    dollar_amounts = re.findall(r'\$\s?\d+(?:\.\d{1,2})?', prompt)

    distance_legs = re.findall(r'\d+(?:\.\d+)?\s*(?:kilometers?|km|miles?|meters?|feet|ft)\b',

                                prompt, re.IGNORECASE)

    return len(dollar_amounts) >= 2 and len(distance_legs) >= 2

 

def has_extra_narrative(prompt: str) -> bool:

    sentences = [s for s in re.split(r'(?<=[.!?])\s+', prompt.strip()) if s]

    extra = [

        s for s in sentences

        if "$" not in s

        and not re.search(r'\b(?:miles?|km|kilometers?|feet|ft)\b', s, re.IGNORECASE)

        and not s.strip().endswith("?")

    ]

    return len(extra) >= 1

 

def route_prompt(prompt: str) -> str:

    if looks_like_itinerary(prompt):

        reply = trip_planner_agent(prompt)

        if has_extra_narrative(prompt):

            print("[Router] -> also routing to Agent 2: Text Analysis Agent (extra narrative sentence detected)")

            reply += "\n\n" + text_agent(prompt)

        return reply

    else:

        return text_agent(prompt)

 

Step 8 – Testing your results

 

Now it is time to test everything out.

 

prompt = input("Ask me anything: ")

 

print("=" * 60)

print(f"PROMPT: {prompt}")

print("=" * 60)

final_answer = route_prompt(prompt)

print(f"\n  >>> RETURNED: {final_answer}")

print("-" * 60 + "\n")

 

Understanding the logic.

 

Now, let's use the prompt below and walk through each individual step.

 

"Today I went to a Cubs game at Wrigley Field ($55 ticket), then drove 5.5 miles to the Art Institute of Chicago ($32 admission), then drove 1.5 miles to the Shedd Aquarium ($20 admission), and finally drove 2.3 miles to Giordano's Pizza in River North for a $30 deep dish. What was my total cost for the day, and how many miles did I travel? It was a jam-packed day of baseball, art, sea life, and deep-dish pizza, and I'd love a short recap for my trip journal."

 

 

Steps 1–6: the router activates Agent 1, which calls its tools and gets its reply.
Steps 1–6: the router activates Agent 1, which calls its tools and gets its reply.

Step 1 – Routing to the Agent

 

Everything starts with the prompt router.  If it finds something that resembles a currency, like a $, and a distance of length, like miles or km, it directs the prompt to the trip_planner_agent (Agent 1).  It then checks has_extra_narrative, a condition that only runs after the trip_planner_agent already matched, and that looks for an extra sentence that isn't about cost or distance.  If it finds one, the router also calls the text_agent (Agent 2).  If neither the itinerary pattern nor an extra sentence is found, the router falls back to the text_agent alone.  In this scenario, both trip_planner_agent and the has_extra_narrative condition are triggered, so both agents run.  I color coded the prompt below to show what caused each one to fire.

 

"Today I went to a Cubs game at Wrigley Field ($55 ticket), then drove 5.5 miles to the Art Institute of Chicago ($32 admission), then drove 1.5 miles to the Shedd Aquarium ($20 admission), and finally drove 2.3 miles to Giordano's Pizza in River North for a $30 deep dish. What was my total cost for the day, and how many miles did I travel? It was a jam-packed day of baseball, art, sea life, and deep-dish pizza, and I'd love a short recap for my trip journal."

 

Step 2 – Trip planner agent calls its tools

 

The trip_planner_agent then calls both the calculator and unit_converter tools

 

Step 3 – Call the calculator tool

 

The calculator tool finds every dollar amount and adds them up, returning the string below.

 

$55 + $32 + $20 + $30 = $137.00

 

Step 4 – Call the unit converter tool

 

The unit_converter tool then locates all distances and returns the below string

 

5.5 miles + 1.5 miles + 2.3 miles = 9.3 miles total

 

Step 5 – Create updated prompt

 

Both results are returned to the trip_planner_agent, which uses them to rebuild the prompt so it looks like this.  Notice that the part in red has been added.

 

Today I went to a Cubs game at Wrigley Field ($55 ticket), then drove 5.5 miles to the Art Institute of Chicago ($32 admission), then drove 1.5 miles to the Shedd Aquarium ($20 admission), and finally drove 2.3 miles to Giordano's Pizza in River North for a $30 deep dish. What was my total cost for the day, and how many miles did I travel? It was a jam-packed day of baseball, art, sea life, and deep-dish pizza, and I'd love a short recap for my trip journal

   

    A calculator tool already computed the total cost: $55 + $32 + $20 + $30 = $137.00

    A distance tool already computed the total distance traveled: 5.5 miles + 1.5 miles + 2.3 miles = 9.3 miles total

   

    Using those two verified totals (don't redo either calculation yourself), give the user a short, friendly trip summary that reports both totals clearly.

 

Step 6 – Send results to GROQ

 

This new prompt is sent to GROQ (the LLM for this illustration), and it might return a response like this one.

 

Sounds like a fantastic Chicago adventure! 

    - Total cost: $137 

    - Miles driven: 9.3 mi 

   

    Enjoy the memories (and the deep‑dish)!

 


Steps 7–9: Agent 2 runs alongside Agent 1, and their replies are combined into the final answer.
Steps 7–9: Agent 2 runs alongside Agent 1, and their replies are combined into the final answer.

 

Step 7 – Use text_agent

 

After the first agent ran, the has_extra_narrative condition came back true, so the text_agent was activated.  The original prompt was then sent to the summarizer skill.

 

Step 8 – Use summarize skill

 

The summarizer skill took the original prompt and, because it had more than its two-sentence limit, kept only the two most important sentences and dropped the middle question entirely.  See the original prompt below and how it compares to what came out the other side.

 

Original Prompt

 

Today I went to a Cubs game at Wrigley Field ($55 ticket), then drove 5.5 miles to the Art Institute of Chicago ($32 admission), then drove 1.5 miles to the Shedd Aquarium ($20 admission), and finally drove 2.3 miles to Giordano's Pizza in River North for a $30 deep dish. What was my total cost for the day, and how many miles did I travel? It was a jam-packed day of baseball, art, sea life, and deep-dish pizza, and I'd love a short recap for my trip journal.

 

Prompt After the summarizer skill

 

Today I went to a Cubs game at Wrigley Field ($55 ticket), then drove 5.5 miles to the Art Institute of Chicago ($32 admission), then drove 1.5 miles to the Shedd Aquarium ($20 admission), and finally drove 2.3 miles to Giordano's Pizza in River North for a $30 deep dish. It was a jam-packed day of baseball, art, sea life, and deep-dish pizza, and I'd love a short recap for my trip journal.

 

 Step 9 – Send results to GROQ again

 

This narrative-only prompt is sent to GROQ, and it returns a thoughtful journal reflection without ever recalculating the totals itself.  The dollar and mile figures shown below came from Agent 1 earlier in the pipeline, not from this step, which is exactly the point of dividing the work between agents in the first place.

 

    You spent a total of $137 and drove 9.3 miles that day. 

   

    Quick recap for your journal: 

    - Cubs game at Wrigley Field – $55 

    - 5.5 mi to the Art Institute – $32 

    - 1.5 mi to Shedd Aquarium – $20 

    - 2.3 mi to Giordano’s for a deep‑dish pizza – $30 

   

    A full‑day mix of baseball, art, sea life, and a delicious slice of Chicago pizza. What a jam‑packed adventure!

 

 Making the case for agents, skills, and tools.

 

As you can see in this example, the agents, skills, and tools are what let the system rebuild your prompt behind the scenes, without you ever seeing the extra steps.  The router and agents work together to decide what tools or skills are needed, gather that information, rewrite the prompt around it, and send the result to an LLM.  In the real world, you'd chain together several rounds of this, letting your system naturally enrich and refine the prompt every time it learns something new.

 

The tools in this example are simple functions, but in a production system they'd normally be hosted on an MCP server built by someone else, ready for you to call directly.  Skills would also look slightly different in the real world as a developer would generally write that guidance into a markdown file rather than a Python function like the one used here.

 

You don't need a large team or a complicated framework to get started, just a router, a couple of tools, and a willingness to let the model lean on them instead of guessing. Swap in your own tools, skills, or agents and see how you can make this your own. This simplified code gave you that foundation, so you can go build your own tools, skills, and agents for whatever problem is sitting on your desk.

 

 


Comments


bottom of page