Home / Articles

Build a Mini AI Assistant From Scratch With Agents, Tools and Skills

A small Python project that routes a prompt to agents, calls a calculator and unit-converter tool, runs a summariser skill and sends the rebuilt prompt to an LLM.

Publisher: REACTAPP.TOP12 min read2649 words

This article is published in English.

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

Ask a chatbot what the weather in Chicago was like last week and it will guess. Hand the same chatbot a tool that really checks, and it will know. The distance between a model that guesses and a system that knows is what agents, tools and skills close, and it explains much of why generative AI has become such a talked-about corner of IT.

The entry barrier is lower than it looks. If you can run a decent Google search, you can write a decent prompt. A single prompt only goes so far, though. The interesting part begins when you stop expecting the model to know everything and give it agents, tools and skills to lean on. Below you'll find all three explained, with Python you can run yourself, built around a small trip-planning example from start to finish.

Tools, skills and agents defined

Tools usually come from an outside source such as an MCP (Model Context Protocol) server. Call that server and you typically get back a list of tools to help with your prompt. Say you want weather information inside a prompt. A weather MCP server might expose tools like these, with get_forecast returning the current forecast. If your prompt asked how much Chicago's weather shifted over the past week, such a tool would add the right instruction to make the lookup succeed.

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

That list is only a sample of what a tools server might offer. The hands-on example further down swaps these for two simpler custom tools: a calculator and a distance converter.

Skills are closer to recipes. If you write for Medium, one of your skills could be a style guide. In many data engineering projects they are lists of calculations or comparable values, which makes them something like lookup tables. If a prompt asked for prices on every flight out of the Chicago area, a skill might fill in ORD (O'Hare), MDW (Midway) or CHI, the code covering all of the city's airports. Here are measurement families a skill could hold:

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

Storage =  Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)

Again, that's a broad illustration of what a skill's lookup table might contain. The example here needs only the distance rows, converting among miles, kilometres, metres and feet.

Agents are what make an AI process look as if it's reasoning. In practice, your prompt passes through several loops, each building on the original. The first prompt gives the system enough direction for a first step. It then decides what the next step should be and continues for as long as your code allows. Think of a Choose Your Own Adventure book: each pass can branch in a different direction to reach a fitting answer.

Building it in code

The architecture is intentionally simple. You can write the code in a Jupyter notebook or Google Colab, it costs nothing and it's easy to adjust. The walkthrough covers the code pieces first, in reverse order of execution, and the section after that shows how everything behaves when a real prompt goes through.

Step 1: imports

The requests, getpass and os libraries fetch the key and call the Groq LLM. The re library helps find patterns of characters inside the prompt, and every tool and skill relies on it. You need Python 3.10 or newer. If requests isn't installed, run !pip install requests first.

import re
import os
import getpass
import requests

Step 2: the calculator

A regular expression pulls out each dollar amount in the prompt and the function adds them together. It simply looks for a $ followed by digits and adds up the numbers. If you work in British pounds, you'd need to change the pattern.

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: a unit converter tool

This one converts common distance units. It first normalises whatever unit the writer used, via the UNIT_ALIASES dictionary, then turns every leg into miles with the DISTANCE_TO_MILES dictionary, which makes totalling easy. Depending on your use case, you might add kilometre or step-count conversions as well.

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: a summariser skill

A lightweight summariser. It scores each sentence by removing the stop words listed in the stopwords set and ranking what's left by how many meaningful words remain. The aim is to drop sentences that add nothing to the prompt and cut needless 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: connecting an LLM

Here the code talks to a real model. Groq is used because it has a free tier, but you need your own API key from the Groq console for it 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"
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"

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: the agents

This is where the agents get set up. They add context to your prompt, and you'll see them calling the tools and skills to bring back accurate data.

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: the router

The router is the brains of the operation. It looks for particular features in a prompt and sends it to the right agent or agents. One detail: has_extra_narrative is a condition checked only after trip_planner_agent has already matched. The text_agent runs either as the default for any prompt or alongside the trip planner whenever that condition is 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 "quot; 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: trying it out

Time to test everything.

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")

Following one prompt through the system

Take this prompt and trace each stage:

"A Cubs game at Wrigley Field ($55 ticket) kicked off the day. Next came a 5.5-mile drive to the Art Institute of Chicago ($32 entry), 1.5 miles on to the Shedd Aquarium ($20) and 2.3 more miles to a $30 deep-dish dinner at Giordano's in River North. What did the day cost in total, and how far was the route? Baseball, art, sea life and pizza made it a packed day, and a short recap for the trip journal would be lovely."

Routing

Everything begins at the prompt router. If it spots something resembling currency, such as a $, together with a distance in miles or km, it directs the prompt to the trip planner, Agent 1. Next it evaluates has_extra_narrative, which only runs once the trip planner matched and looks for an extra sentence unrelated to cost or distance. If one exists, the router also calls the text agent, Agent 2. When neither the itinerary pattern nor an extra sentence turns up, the text agent handles the prompt by itself. Here both conditions fire, so both agents run. Highlighting the prompt in colour makes it clear which parts triggered each one.

The trip planner calls its tools

The trip planner agent calls both the calculator and the unit converter.

The calculator finds every dollar amount and returns one string:

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

The unit converter finds the distances and returns:

5.5 mi, 1.5 mi and 2.3 mi add up to 9.3 mi

The prompt gets rebuilt

Both results go back to the trip planner, which uses them to rebuild the prompt, appending the computed totals to the original text. That new prompt goes to Groq, the LLM used for the demonstration, which might reply along these lines:

What a great day in Chicago.

  • Spent: $137
  • Distance: 9.3 mi

Hold on to the memories, and the deep-dish.

The text agent and the summariser

After the first agent finished, has_extra_narrative came back true, so the text agent was activated and the original prompt went to the summariser skill. Because the prompt had more sentences than the skill's two-sentence limit, it kept the two highest-scoring sentences and dropped the middle question altogether. Comparing the original prompt with the one that comes out shows the question about cost and miles gone and the narrative sentences left.

That narrative-only prompt goes to Groq a second time, and the model produces a thoughtful journal reflection without recalculating anything. The dollar and mile figures in its reply came from Agent 1 earlier in the pipeline and not from this step, which is the whole point of dividing the work between agents:

Your day cost $137 in total and covered 9.3 miles.

Journal recap:

  • Wrigley Field ballgame: $55
  • Art Institute, 5.5 mi away: $32
  • 1.5 mi on to the Shedd Aquarium: $20
  • 2.3 mi to deep-dish at Giordano's: $30

Baseball, art, sea creatures and Chicago pizza made for a packed day out.

Why this pattern is worth having

The example shows that agents, skills and tools let a system rebuild your prompt behind the scenes, with you never seeing the extra steps. A router and agents work together to decide which tools or skills are needed, gather the facts, reshape the prompt around them and pass the outcome on to the LLM. In real deployments you would chain several rounds, letting the system enrich and refine the prompt each time it learns something new.

The tools here are plain functions. In production they would more often live on an MCP server someone else built, ready to call. Skills would look a little different too, since developers usually write that guidance in a markdown file instead of a Python function like the one above.

You don't need a big team or a heavyweight framework to start. One router plus two or three small tools, and a model allowed to rely on them instead of improvising, will get you going. Swap in your own tools, skills or agents and make it yours, then build the ones for whatever problem is sitting on your desk.