cd /news/ai-agents/ai-agents-with-function-calling-buil… · home › topics › ai-agents › article
[ARTICLE · art-149738] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

AI Agents with Function Calling: Build Tools LLMs Can Use

A developer published a from-scratch framework for building LLM agents using function calling, in which the model emits structured JSON to invoke predefined tools. The implementation defines an abstract Tool class with name, description, parameters and execute members, plus concrete tools for DuckDuckGo web search, sandboxed Python execution with an import allowlist and a 10-second subprocess timeout, and a math-only calculator.

by read6 min views1 publishedOct 12, 2026

Create AI agents that can execute code, search the web, and call APIs — using function calling.

Function calling (tool use) transforms LLMs from text generators into agents that can act. This tutorial builds a complete agent framework from scratch.

LLMs output structured JSON to invoke predefined functions:

User: "What's the weather in Tokyo?"
LLM: {"tool": "get_weather", "args": {"city": "Tokyo"}}
System: [executes function, returns result]
LLM: "It's 22°C and sunny in Tokyo."

requests library

import os
import requests
import json
from typing import Dict, Any, List
from abc import ABC, abstractmethod

class Tool(ABC):
    @property
    @abstractmethod
    def name(self) -> str: ...

    @property
    @abstractmethod
    def description(self) -> str: ...

    @property
    @abstractmethod
    def parameters(self) -> Dict: ...

    @abstractmethod
    def execute(self, **kwargs) -> Any: ...


class WebSearchTool(Tool):
    name = "web_search"
    description = "Search the web for current information"
    parameters = {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Search query"}
        },
        "required": ["query"]
    }

    def execute(self, query: str) -> str:
        url = "https://html.duckduckgo.com/html/"
        params = {"q": query}
        headers = {"User-Agent": "Mozilla/5.0"}

        try:
            response = requests.post(url, data=params, headers=headers, timeout=10)
            from bs4 import BeautifulSoup
            soup = BeautifulSoup(response.text, 'html.parser')
            results = soup.find_all('a', class_='result__snippet')
            snippets = [r.get_text()[:200] for r in results[:3]]
            return "\n".join(snippets) if snippets else "No results found"
        except Exception as e:
            return f"Search error: {e}"

class PythonExecutorTool(Tool):
    name = "execute_python"
    description = "Execute Python code and return output"
    parameters = {
        "type": "object",
        "properties": {
            "code": {"type": "string", "description": "Python code to execute"}
        },
        "required": ["code"]
    }

    def execute(self, code: str) -> str:
        import subprocess
        import tempfile
        import sys

        allowed_imports = {'json', 'math', 'random', 'datetime', 'statistics', 'itertools', 'collections'}

        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            temp_path = f.name

        try:
            result = subprocess.run(
                [sys.executable, temp_path],
                capture_output=True,
                text=True,
                timeout=10
            )
            os.unlink(temp_path)
            output = result.stdout
            if result.stderr:
                output += f"\nSTDERR: {result.stderr}"
            return output[:2000] if output else "(no output)"
        except subprocess.TimeoutExpired:
            os.unlink(temp_path)
            return "Error: Execution timeout (10s)"
        except Exception as e:
            return f"Error: {e}"

class CalculatorTool(Tool):
    name = "calculate"
    description = "Perform mathematical calculations"
    parameters = {
        "type": "object",
        "properties": {
            "expression": {"type": "string", "description": "Math expression to evaluate"}
        },
        "required": ["expression"]
    }

    def execute(self, expression: str) -> str:
        try:
            allowed_names = {"abs", "round", "min", "max", "sum", "pow"}
            allowed_names.update({k: v for k, v in __import__('math').__dict__.items() if not k.startswith('_')})

            code = compile(expression, "<string>", "eval")
            for name in code.co_names:
                if name not in allowed_names:
                    return f"Error: '{name}' not allowed"

            result = eval(code, {"__builtins__": {}}, allowed_names)
            return str(result)
        except Exception as e:
            return f"Error: {e}"

class FileReadTool(Tool):
    name = "read_file"
    description = "Read contents of a file"
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to read"}
        },
        "required": ["path"]
    }

    def execute(self, path: str) -> str:
        try:
            with open(path, 'r') as f:
                return f.read()[:5000]
        except Exception as e:
            return f"Error reading file: {e}"

class FileWriteTool(Tool):
    name = "write_file"
    description = "Write content to a file"
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to write"},
            "content": {"type": "string", "description": "Content to write"}
        },
        "required": ["path", "content"]
    }

    def execute(self, path: str, content: str) -> str:
        try:
            os.makedirs(os.path.dirname(path) or '.', exist_ok=True)
            with open(path, 'w') as f:
                f.write(content)
            return f"File written: {path}"
        except Exception as e:
            return f"Error writing file: {e}"

TOOLS = {
    "web_search": WebSearchTool(),
    "execute_python": PythonExecutorTool(),
    "calculate": CalculatorTool(),
    "read_file": FileReadTool(),
    "write_file": FileWriteTool()
}

def get_tool_schemas() -> List[Dict]:
    """Return OpenAI-compatible tool schemas."""
    return [
        {
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.parameters
            }
        }
        for tool in TOOLS.values()
    ]

def execute_tool(name: str, args: Dict) -> str:
    if name not in TOOLS:
        return f"Error: Unknown tool '{name}'"
    try:
        return TOOLS[name].execute(**args)
    except Exception as e:
        return f"Tool error: {e}"
python
import os
import json
import requests
from typing import List, Dict, Any, Optional
from tools import get_tool_schemas, execute_tool

class Agent:
    def __init__(self, provider: str = "openrouter", model: str = None):
        self.provider = provider
        self.configs = {
            "openrouter": {
                "url": "https://openrouter.ai/api/v1/chat/completions",
                "key": os.getenv("OPENROUTER_API_KEY"),
                "model": model or "google/gemini-2.0-flash-exp:free"
            },
            "groq": {
                "url": "https://api.groq.com/openai/v1/chat/completions",
                "key": os.getenv("GROQ_API_KEY"),
                "model": model or "llama-3.1-70b-versatile"
            }
        }
        self.messages = []
        self.tools = get_tool_schemas()
        self.max_turns = 10

    def add_system_prompt(self, prompt: str):
        self.messages.insert(0, {"role": "system", "content": prompt})

    def run(self, user_input: str) -> str:
        self.messages.append({"role": "user", "content": user_input})

        for turn in range(self.max_turns):
            response = self._call_llm()

            if "tool_calls" in response:
                tool_results = []
                for tc in response["tool_calls"]:
                    name = tc["function"]["name"]
                    args = json.loads(tc["function"]["arguments"])
                    result = execute_tool(name, args)
                    tool_results.append({
                        "tool_call_id": tc["id"],
                        "role": "tool",
                        "name": name,
                        "content": result
                    })

                self.messages.append({"role": "assistant", "tool_calls": response["tool_calls"]})
                self.messages.extend(tool_results)

            else:
                content = response.get("content", "")
                self.messages.append({"role": "assistant", "content": content})
                return content

        return "Max turns reached"

    def _call_llm(self) -> Dict:
        config = self.configs[self.provider]

        headers = {
            "Authorization": f"Bearer {config['key']}",
            "Content-Type": "application/json"
        }

        data = {
            "model": config["model"],
            "messages": self.messages,
            "tools": self.tools,
            "tool_choice": "auto",
            "temperature": 0.1,
            "max_tokens": 2000
        }

        response = requests.post(config["url"], headers=headers, json=data, timeout=60)
        response.raise_for_status()
        return response.json()["choices"][0]["message"]

    def run_stream(self, user_input: str):
        """Streaming version."""
        self.messages.append({"role": "user", "content": user_input})

        for turn in range(self.max_turns):
            response = self._call_llm()

            if "tool_calls" in response:
                tool_results = []
                for tc in response["tool_calls"]:
                    name = tc["function"]["name"]
                    args = json.loads(tc["function"]["arguments"])
                    result = execute_tool(name, args)
                    tool_results.append({
                        "tool_call_id": tc["id"],
                        "role": "tool",
                        "name": name,
                        "content": result
                    })

                self.messages.append({"role": "assistant", "tool_calls": response["tool_calls"]})
                self.messages.extend(tool_results)
                continue

            content = response.get("content", "")
            self.messages.append({"role": "assistant", "content": content})

            for chunk in content.split():
                yield chunk + " "
            return
python
import os
from agent import Agent

agent = Agent(provider="openrouter")  # or "groq"

agent.add_system_prompt("""You are a helpful AI assistant with access to tools.
Use tools when you need current information, calculations, or code execution.
Think step by step. Be concise.
Available tools: web_search, execute_python, calculate, read_file, write_file""")

print("=== Query 1 ===")
result = agent.run("What's the current population of Tokyo? Calculate 15% of it.")
print(result)

print("\n=== Query 2 ===")
result = agent.run("""
Write a Python function to find the nth Fibonacci number, 
then compute the 20th Fibonacci number.
""")
print(result)

print("\n=== Query 3 ===")
result = agent.run("""
Create a file 'todo.txt' with these items:
1. Learn function calling
2. Build an agent
3. Deploy it
Then read it back.
""")
print(result)

print("\n=== Query 4 ===")
result = agent.run("""
Find the current price of Bitcoin, then calculate how much 
0.5 BTC would be worth in USD.
""")
print(result)
python
import json
from agent import Agent

class PersistentAgent(Agent):
    def __init__(self, *args, memory_file: str = "agent_memory.json", **kwargs):
        super().__init__(*args, **kwargs)
        self.memory_file = memory_file
        self.load_memory()

    def load_memory(self):
        try:
            with open(self.memory_file, 'r') as f:
                data = json.load(f)
                self.messages = data.get("messages", [])
        except FileNotFoundError:
            pass

    def save_memory(self):
        with open(self.memory_file, 'w') as f:
            json.dump({"messages": self.messages[-20:]}, f)  # Keep last 20

    def run(self, user_input: str) -> str:
        result = super().run(user_input)
        self.save_memory()
        return result

agent = PersistentAgent(memory_file="my_agent_memory.json")
print(agent.run("Remember my name is Alex"))
print(agent.run("What's my name?"))  # Will remember!
Provider Model Function Calling
OpenRouter google/gemini-2.0-flash-exp:free ✅ Excellent
OpenRouter mistralai/mistral-7b-instruct:free ✅ Good
Groq llama-3.1-70b-versatile ✅ Excellent
Groq llama-3.1-8b-instant ✅ Good
Together meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo ✅ Good

All components use free tiers. Function calling works on free models via OpenRouter and Groq.

You now have a working agent framework. Extend it with:

The pattern is always: define tool → register schema → execute → return result.

Part of the AI Agents series on Dev.to

── more in #ai-agents 4 stories · sorted by recency
── more on @duckduckgo 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/ai-agents-with-funct…] indexed:0 read:6min 2026-10-12 · —