NestDaddy
AI agents

How to build an AI agent from scratch in Python (free, no framework)

An AI agent is a model, some tools and a loop. Build one in about 60 lines of Python with a free local model, watch it read today's news, and fix the hallucination every beginner agent has.

NestDaddy team · · 6 min read

Every agent framework, from LangChain to CrewAI, is built on one small idea. Once you have written it yourself, the frameworks stop being magic. This guide builds a working agent in about 60 lines of plain Python, with a free model that runs on your own computer, and no framework at all.

The agent will answer "what happened in AI today?" with real headlines and links, and you will see the most common bug in AI agents (making up facts) and the one rule that fixes it.

Watch the video: How to build an AI agent from scratch

Prefer video? The same tutorial is on YouTube (6 minutes). Full code: nestdaddy-examples/python/ai-agent-from-scratch.

Try it live A real response from the NestDaddy API. No sign-up needed.
GET
Press Run to call the API.
Demo key, limited calls per visitor. Get your free API key

What an AI agent really is

A language model on its own only knows what it learned in training. It cannot look anything up, it does not know today's date, and it cannot act. An agent gives it three things:

  1. Tools. Normal functions in your code: search the news, read a web page, query a database.
  2. A manual. For each tool, a short description and its inputs as a JSON schema. This is what the model reads to decide when to use a tool.
  3. A loop. The model either answers, or asks for a tool. Your code runs the tool, gives the result back, and the model decides again, until it can answer.

The model never runs code itself. It writes a request ("call news_search with these arguments") and your code decides what to do with it. That is all "function calling" means.

Setup: two free things

A model. Ollama runs open models on your own computer. Pull one that supports tools; qwen2.5:7b works well on a normal laptop with a GPU:

ollama pull qwen2.5:7b

If your computer is slow, any OpenAI-compatible API works too (Gemini, OpenAI, Groq). You change one line.

Fresh information. The agent needs a source of today's news. This guide uses NestDaddy's news and search API, which returns clean JSON from its own index. The free plan gives you 1,000 calls a month with no card, which is plenty for learning:

export NESTDADDY_API_KEY="your-free-key"
pip install openai requests

We use the openai library only because almost every model, Ollama included, speaks its format.

Step 1: the model alone

import os
from openai import OpenAI

llm = OpenAI(base_url=os.getenv("LLM_BASE_URL", "http://localhost:11434/v1"),
             api_key=os.getenv("LLM_API_KEY", "ollama"))

reply = llm.chat.completions.create(
    model=os.getenv("LLM_MODEL", "qwen2.5:7b"),
    messages=[{"role": "user", "content": "What happened in AI today?"}],
)
print(reply.choices[0].message.content)

The answer we got started with "As of my last update in October 2023...". The model is not broken. It simply has no way to look.

Step 2: give it a tool

A tool is a plain function. This one calls the news API and returns a short list. Keep it small: everything you return is text the model has to read, and less noise means better answers.

NEWS_API = "https://nestdaddy.com/api/v1"
HEADERS = {"X-API-Key": os.getenv("NESTDADDY_API_KEY", "")}

def news_search(query, days=3):
    r = requests.get(f"{NEWS_API}/news/search", headers=HEADERS, timeout=30,
                     params={"q": query, "days": days, "limit": 5})
    r.raise_for_status()
    return [{"title": a["title"], "source": a.get("source_domain"),
             "date": a.get("published_timestamp"), "url": a["url"]}
            for a in r.json().get("results", [])]

Then the manual. The description matters more than you think: "use it for anything that happened recently" is the sentence that makes the model reach for this tool at the right moment.

TOOLS = {"news_search": news_search}

TOOL_SPECS = [
    {"type": "function", "function": {
        "name": "news_search",
        "description": "Search the latest news. Use it for anything that happened recently.",
        "parameters": {"type": "object", "properties": {
            "query": {"type": "string", "description": "Short key words, like a search box"},
            "days": {"type": "integer", "description": "How many days back to search, 1 to 30"}},
            "required": ["query"]}}},
]

Step 3: the agent loop

This is the heart of every agent:

def run_agent(question, max_steps=6):
    messages = [
        {"role": "system", "content": f"You are a helpful assistant. Today is {date.today()}. "
                                      "Use your tools for anything recent. Cite the URLs you used."},
        {"role": "user", "content": question},
    ]
    for _ in range(max_steps):
        reply = llm.chat.completions.create(model=MODEL, messages=messages, tools=TOOL_SPECS).choices[0].message
        if not reply.tool_calls:          # no tool needed: this is the final answer
            return reply.content
        messages.append(reply)
        for call in reply.tool_calls:     # the model asked for one or more tools
            args = json.loads(call.function.arguments or "{}")
            print(f"  -> {call.function.name}({args})")
            try:
                result = TOOLS[call.function.name](**args)
            except Exception as e:        # give errors back to the model instead of crashing
                result = f"error: {e}"
            messages.append({"role": "tool", "tool_call_id": call.id,
                             "content": json.dumps(result, ensure_ascii=False)[:6000]})
    return "Stopped: too many steps."

Four details worth noticing:

  • The system message tells the model today's date. It does not know it otherwise.
  • Errors go back to the model as text, so it can try something else instead of crashing your program.
  • Each tool result is linked to the request by its tool_call_id.
  • max_steps stops a confused model from looping forever.

Run it and the agent searches the news (-> news_search({'query': 'AI', 'days': 1})), then answers with that day's real headlines and their links. Same model as step 1. Now it can look.

Step 4: read the full article, and fix a hallucination

Headlines are a start, but a detailed question needs the whole article. So add a second tool:

def read_page(url):
    r = requests.get(f"{NEWS_API}/extract", headers=HEADERS, timeout=30, params={"url": url})
    r.raise_for_status()
    return r.json().get("text", "")[:4000]

Add it to TOOLS and give it a manual entry ("Read the full text of a web page, for example a news article").

Then we asked: "What did Anthropic recommend this week for AI safety evaluations? Give me the key points." The first time, the agent searched, saw one headline, and wrote ten confident key points: transparency, ethical guidelines, public engagement. None of them were in the article. It never read it. This is the most common bug in AI agents: the model fills the gap between a headline and a detailed answer with things that sound right.

The fix was one rule in the system message:

"Use your tools for anything recent. Search results are only headlines: "
"before you give details, read the best article with read_page. "
"Only state facts you found with your tools, and cite the URLs you used."

Same question again: the agent searched, chose the article, called read_page, and only then answered. The points it gave (preregistration of analysis methods, validating automated graders against human experts) are really in the article. We never told it which page to read or when. We only gave it the rule.

What to build next

You now know what is inside every agent framework: a model, tools, a manual and a loop. From here you can:

  • add memory, so it remembers earlier questions;
  • add more tools: web search, research papers, your own database;
  • plug the same tools into Claude, Cursor or VS Code with MCP, without writing the loop yourself.

The full code (three files, one per step) is on GitHub, MIT licensed. If you build something with it, a free NestDaddy key gives your agent 1,000 news and search calls a month: get one here.

More guides