GuidesZero to Agent
Zero to Agent · Beginner

The 50-Line Agent: Build Your First Minimal AI Agent

Build a small agent loop that chooses a calculator tool, receives the result, and returns an answer through the AIWave OpenAI-compatible endpoint.

Beginner10 minutesPythonVerified 2026-09-24

An agent does not need a large framework. For a first version, you need a model, a small set of tools, and a loop that lets the model ask for a tool result before it answers.

This example uses one local calculator. The model receives a user request, returns a small JSON tool request, the program runs the calculator, and the model gets one more turn with the result. The code uses a manual JSON protocol rather than claiming native function calling support for a particular model.

The route and model come from the live AIWave quickstart checked on 2026-09-23:

  • Base URL: https://aiwave.live/v1
  • Model: deepseek-v4-flash
  • Client: OpenAI Python package

The complete agent

Create agent.py, set AIWAVE_API_KEY, and run it with a question such as What is 37 * 12?.

Python
import ast
import json
import os
import operator
import sys
from openai import OpenAI

OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
}


def calculate(expression: str) -> str:
    tree = ast.parse(expression, mode="eval").body
    if not isinstance(tree, ast.BinOp) or type(tree.op) not in OPS:
        raise ValueError("Use one arithmetic expression with +, -, *, or /")
    if not all(isinstance(node, ast.Constant) and isinstance(node.value, (int, float))
               for node in (tree.left, tree.right)):
        raise ValueError("Only numeric operands are allowed")
    return str(OPS[type(tree.op)](tree.left.value, tree.right.value))


def ask(client: OpenAI, messages: list[dict[str, str]]) -> str:
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=messages,
    )
    return response.choices[0].message.content or ""


def main() -> None:
    api_key = os.environ.get("AIWAVE_API_KEY")
    if not api_key:
        raise SystemExit("Set AIWAVE_API_KEY before running this agent")

    question = " ".join(sys.argv[1:]) or "What is 37 * 12?"
    client = OpenAI(api_key=api_key, base_url="https://aiwave.live/v1")
    messages = [
        {"role": "system", "content": (
            "You are a small tool-using agent. Return only JSON. "
            "For arithmetic, return {\"type\":\"tool_call\",\"tool\":\"calculator\","
            "\"input\":{\"expression\":\"37 * 12\"}}. Otherwise return "
            "{\"type\":\"final\",\"answer\":\"...\"}."
        )},
        {"role": "user", "content": question},
    ]

    first = json.loads(ask(client, messages))
    if first.get("type") == "tool_call" and first.get("tool") == "calculator":
        result = calculate(first["input"]["expression"])
        messages.extend([
            {"role": "assistant", "content": json.dumps(first)},
            {"role": "user", "content": f"Tool result: {result}. Return final JSON."},
        ])
        final = json.loads(ask(client, messages))
        print(final.get("answer", "The agent returned no answer."))
    else:
        print(first.get("answer", "The agent returned no answer."))


if __name__ == "__main__":
    main()

Validation: run python -m py_compile agent.py, then run the script with a controlled key. The expected output for What is 37 * 12? is 444. The request uses the same base URL and model verified on the live quickstart.

Run the first check without exposing the key:

curl
export AIWAVE_API_KEY="sk-your-api-key"
python -m py_compile agent.py
python agent.py "What is 37 * 12?"

Validation: compilation checks the local file; the second command performs one controlled API call and should print 444.

The two message shapes in the loop are intentionally small:

json
{"type":"tool_call","tool":"calculator","input":{"expression":"37 * 12"}}
{"type":"final","answer":"444"}

Validation: these are the only protocol shapes the example consumes. Reject a response that is not valid JSON instead of trying to execute it.

You can test the tool boundary without calling the API:

Python
from agent import calculate

assert calculate("37 * 12") == "444"
assert calculate("8 / 2") == "4.0"

for expression in ("__import__('os')", "open('notes.txt').read()"):
    try:
        calculate(expression)
    except ValueError:
        pass
    else:
        raise AssertionError(expression)

Validation: these assertions check allowed arithmetic and reject two unsafe inputs locally. They do not send a request or read a file.

If you add another tool, keep dispatch explicit and bounded:

Python
TOOLS = {
    "calculator": calculate,
}


def dispatch_tool(name: str, tool_input: dict[str, str]) -> str:
    if name not in TOOLS:
        raise ValueError(f"Unknown tool: {name}")
    expression = tool_input.get("expression", "")
    if not expression:
        raise ValueError("The calculator needs an expression")
    return TOOLS[name](expression)


MAX_TURNS = 2
turn = 0
while turn < MAX_TURNS:
    turn += 1
    # Call the model, validate its JSON, and stop on a final answer.
    # Never execute a tool name or argument before validating both fields.
    break
else:
    raise RuntimeError("Agent reached its turn limit")

Validation: this is a local design fragment. It demonstrates an allowlist, required input, and a hard turn limit; it does not make an API call.

Keep the tool contract readable in code:

Python
CALCULATOR_CONTRACT = {
    "name": "calculator",
    "input": {"expression": "string containing one arithmetic expression"},
    "output": "string containing the calculated result",
    "limits": {"operators": ["+", "-", "*", "/"], "turns": 2},
}

Validation: this is documentation data only. It makes the accepted tool shape reviewable before a second tool is added.

What the loop is doing

The first request asks for a decision. The model can either return a final answer or request the calculator. The program owns the tool, so the model cannot execute arbitrary Python or read local files. After the calculator returns a result, the program sends that result back and asks for final JSON.

This ownership boundary matters. A tool should accept a narrow input, validate it, and return a small result. When you add a web search or file tool later, give it its own allowlist, timeout, size limit, and error response. Do not turn the first prototype into a general-purpose shell runner.

The model may include reasoning_content because it is a reasoning model. Read the final answer from message.content and verify provider-specific fields before using them in control flow.

Add one user request at a time

Start with one tool and one loop. Log the validated tool input, result status, and request ID, but not the key or private prompt. Set a turn limit before adding tools.

When it works, add a second tool with a different input contract. Keep the model in configuration and test the same task before changing production traffic.

Use explicit limits as the agent grows:

Python
SAFE_DEFAULTS = {
    "max_turns": 2,  # bounded loop
    "max_expression_chars": 80,
    "max_response_chars": 4000,
    "request_timeout_seconds": 30,
    "allowed_tools": {"calculator"},
    "log_secret_values": False,
}
assert SAFE_DEFAULTS["max_turns"] > 0
assert "calculator" in SAFE_DEFAULTS["allowed_tools"]
assert SAFE_DEFAULTS["request_timeout_seconds"] <= 30
assert SAFE_DEFAULTS["log_secret_values"] is False

Validation: these local assertions document safe defaults; they do not send a request.

Self-check

  • [x] Uses the verified AIWave base URL and model ID.
  • [x] Demonstrates a model decision, tool execution, and second model turn.
  • [x] Uses a restricted calculator instead of arbitrary code execution.
  • [x] Does not claim native function calling support.
  • [x] Keeps the API key in an environment variable.
  • [x] Includes a syntax-check and controlled runtime validation method.
  • [x] No banned marketing terms, internal metrics, customer data, or real credentials.
  • [ ] Run the code with a controlled test key before publication.
  • [ ] Add internal links and run final HEAD checks before publication.
Next guide

Build a RAG Customer-Support Agent

Build a compact retrieval-augmented support bot with real embeddings, inspectable context, and a local corpus check.

Open next guide