How I Built a Production-Ready AI Agent on PHP, cPanel, and Gemini Flash

Most AI agent tutorials assume you are starting from a modern stack: Python, Docker, Redis, background workers, vector databases, and cloud infrastructure already built for orchestration.

That is useful if you are working inside a mature platform team. But a lot of real software still runs on plain PHP, MySQL, and cPanel, and many builders still need to ship useful AI features without first rebuilding their entire infrastructure.

That was the challenge behind this project.

I wanted to build an AI agent that could:

  1. accept a user request,
  2. decide whether it needed a tool,
  3. execute that tool in PHP,
  4. store conversation state in MySQL,
  5. and keep reasoning until it produced a final answer.

In this article, I will walk through how I built that system using:

  1. PHP for orchestration
  2. MySQL for memory
  3. Gemini Flash for reasoning and function calling
  4. cPanel for deployment on standard shared hosting

The point is not to show off a fancy demo. The point is to show that a production-ready AI agent can be practical, understandable, and cheap to run.

By the end, you will understand:

  1. how the agent loop works
  2. how function calling connects an LLM to real tools
  3. how to persist memory without overengineering
  4. how to deploy the whole thing on hosting most developers already know how to use

The Real Problem With Most AI Agent Tutorials

Most tutorials stop at the fun part.

They show you how to send a prompt to an LLM and maybe how to call one tool once. That is enough to prove the concept, but it is not enough to build something that actually behaves like an agent in the real world.

A real agent needs more than a clever prompt.

It needs to:

  1. understand when it should use a tool
  2. pass structured arguments to that tool
  3. handle the tool output
  4. continue the conversation
  5. remember what happened before

That is where a lot of examples fall apart.

The system I built focuses on that missing middle layer, the orchestration between model, tools, and memory.

Why This Stack Works

The reason I chose PHP, MySQL, Gemini Flash, and cPanel is simple: this stack is familiar, accessible, and good enough for a surprising number of production use cases.

Here is what each piece does:

  1. PHP handles HTTP requests and the agent loop
  2. MySQL stores chat memory and saved data
  3. Gemini Flash decides whether the agent should answer directly or use a tool
  4. cPanel makes deployment possible on standard shared hosting

The important idea is that the agent is not a monolith. It is a loop:

  1. receive user input
  2. send context to the model
  3. let the model request a tool if needed
  4. execute the tool
  5. send the result back
  6. repeat until the model produces a final answer

That pattern is small, but it scales surprisingly well.

Architecture Overview

At a high level, the system looks like this:

[User Chat] -> [PHP Endpoint] -> [Gemini Flash]
      ^              |               |
      |              |               v
      +------- [MySQL Memory] <- [Tool Call / Tool Result]

The separation matters:

  1. Gemini Flash does the reasoning
  2. PHP does the execution
  3. MySQL preserves context
  4. The endpoint exposes the whole thing to the outside world

That keeps the system understandable, and more importantly, it keeps it debuggable.

Project Structure

A clean file structure makes the system easier to maintain and extend.

Use something like this:

/public_html/agent/
├── index.php
├── agent.php
├── gemini.php
├── db.php
├── memory.php
├── tool_registry.php
├── tools/
│   ├── save_note.php
│   ├── search_web.php
│   ├── send_email.php
│   └── .htaccess
└── .htaccess

Inside tools/.htaccess, block public access:

Deny from all

The tools should be callable by your application, not directly exposed to the internet.

Set Up the MySQL Database

An agent without memory is just a stateless request handler.

To make this useful, we need persistence. For this version, two tables are enough:

  1. one for conversation history
  2. one for saved notes

Run this in phpMyAdmin:

CREATE DATABASE IF NOT EXISTS ai_agent;
USE ai_agent;

CREATE TABLE agent_memory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(64) NOT NULL,
    role VARCHAR(20) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_session_id (session_id)
);

CREATE TABLE agent_notes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(64) NOT NULL,
    note TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

This is intentionally simple.

You do not need a giant schema to get started. You need enough structure to preserve context and store tool output reliably.

Connect PHP to MySQL

Next, create a small database helper so the rest of the code stays clean.

db.php:

<?php

function db(): PDO
{
    static $pdo = null;

    if ($pdo === null) {
        $pdo = new PDO(
            "mysql:host=localhost;dbname=ai_agent;charset=utf8mb4",
            "db_user",
            "db_password",
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    }

    return $pdo;
}

This file exists for one reason: to make every database call use the same connection logic.

That might look basic, but basic is good here. Complexity should live in the agent behavior, not in the database bootstrap code.

Call Gemini Flash from PHP

The model wrapper should handle the API call and normalize the response into something the agent loop can reason about.

gemini.php:

<?php

function gemini_request(array $contents, array $tools = []): array
{
    $apiKey = "YOUR_GEMINI_API_KEY";
    $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" . $apiKey;

    $payload = [
        "contents" => $contents
    ];

    if (!empty($tools)) {
        $payload["tools"] = [
            [
                "functionDeclarations" => $tools
            ]
        ];
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json"
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_TIMEOUT => 30
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new RuntimeException("Gemini API error: " . $response);
    }

    return json_decode($response, true);
}

function parse_gemini_response(array $response): array
{
    $part = $response["candidates"][0]["content"]["parts"][0] ?? [];

    if (isset($part["functionCall"])) {
        return [
            "type" => "function_call",
            "name" => $part["functionCall"]["name"],
            "args" => $part["functionCall"]["args"] ?? []
        ];
    }

    return [
        "type" => "text",
        "text" => $part["text"] ?? ""
    ];
}

This wrapper does two jobs:

  1. Sends the model the conversation and tool definitions
  2. Converts the result into either plain text or a function call

That keeps the agent loop clean and predictable.

Define the Tool Registry

The model should not be able to call arbitrary functions. It should only know about the tools you explicitly allow.

tool_registry.php:

<?php

function tool_definitions(): array
{
    return [
        [
            "name" => "save_note",
            "description" => "Save an important note to the database.",
            "parameters" => [
                "type" => "object",
                "properties" => [
                    "note" => [
                        "type" => "string"
                    ]
                ],
                "required" => ["note"]
            ]
        ],
        [
            "name" => "search_web",
            "description" => "Search the web for current information.",
            "parameters" => [
                "type" => "object",
                "properties" => [
                    "query" => [
                        "type" => "string"
                    ]
                ],
                "required" => ["query"]
            ]
        ],
        [
            "name" => "send_email",
            "description" => "Send an email when the user explicitly asks.",
            "parameters" => [
                "type" => "object",
                "properties" => [
                    "to" => ["type" => "string"],
                    "subject" => ["type" => "string"],
                    "body" => ["type" => "string"]
                ],
                "required" => ["to", "subject", "body"]
            ]
        ]
    ];
}

This is the agent’s allowed action set.

That constraint is important. The model can request actions, but it cannot invent new capabilities on its own.

Build the Tools

Now comes the part where the model’s intent becomes a real action.

Save Note Tool

tools/save_note.php:

<?php

require_once __DIR__ . "/../db.php";

function save_note_tool(array $args, string $sessionId): string
{
    $note = trim($args["note"] ?? "");

    if ($note === "") {
        return json_encode(["success" => false, "message" => "Empty note"]);
    }

    $stmt = db()->prepare(
        "INSERT INTO agent_notes (session_id, note) VALUES (:session_id, :note)"
    );

    $stmt->execute([
        ":session_id" => $sessionId,
        ":note" => $note
    ]);

    return json_encode(["success" => true, "message" => "Note saved"]);
}

This tool demonstrates the basic production pattern:

  1. validate input
  2. write to the database
  3. return a structured response

Search Web Tool

tools/search_web.php:

<?php

function search_web_tool(array $args): string
{
    $query = urlencode($args["query"] ?? "");
    $url = "https://api.example.com/search?q=" . $query;

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    return $response ?: json_encode(["error" => "Search failed"]);
}

In a real build, you would swap the placeholder URL for an actual search API.

The pattern is what matters:

  1. receive structured arguments
  2. call an external service
  3. return the result to the agent

Send Email Tool

tools/send_email.php:

<?php

function send_email_tool(array $args): string
{
    $to = filter_var($args["to"] ?? "", FILTER_VALIDATE_EMAIL);
    $subject = trim($args["subject"] ?? "");
    $body = trim($args["body"] ?? "");

    if (!$to) {
        return json_encode(["success" => false, "message" => "Invalid email"]);
    }

    $headers = "From: agent@yourdomain.comrn";
    $headers .= "Content-Type: text/plain; charset=UTF-8rn";

    $sent = mail($to, $subject, $body, $headers);

    return json_encode([
        "success" => $sent,
        "message" => $sent ? "Email sent" : "Email failed"
    ]);
}

Again, the shape is the same:

  1. validate
  2. act
  3. return machine-readable output

That consistency makes the agent easier to extend later.

Add MySQL Conversation Memory

Memory is what lets the agent behave like a conversation instead of a one-off request.

memory.php:

<?php

require_once __DIR__ . "/db.php";

function load_memory(string $sessionId): array
{
    $stmt = db()->prepare(
        "SELECT role, content FROM agent_memory WHERE session_id = :session_id ORDER BY id ASC"
    );

    $stmt->execute([":session_id" => $sessionId]);

    $history = [];

    foreach ($stmt->fetchAll() as $row) {
        $history[] = [
            "role" => $row["role"],
            "parts" => [
                ["text" => $row["content"]]
            ]
        ];
    }

    return $history;
}

function save_memory(string $sessionId, array $history): void
{
    $pdo = db();

    $pdo->prepare("DELETE FROM agent_memory WHERE session_id = :session_id")
        ->execute([":session_id" => $sessionId]);

    $stmt = $pdo->prepare(
        "INSERT INTO agent_memory (session_id, role, content) VALUES (:session_id, :role, :content)"
    );

    foreach ($history as $turn) {
        $text = $turn["parts"][0]["text"] ?? json_encode($turn["parts"][0]);

        $stmt->execute([
            ":session_id" => $sessionId,
            ":role" => $turn["role"],
            ":content" => $text
        ]);
    }
}

This is a pragmatic approach for a first version.

It is not the most sophisticated memory system possible, but it is simple, durable, and easy to understand.

Create the Agent Loop

This is the core of the entire system.

The agent loop is where the conversation becomes more than a single API call. It lets the model decide when to use tools, then returns the result to the model until it produces a final response.

agent.php:

<?php

require_once __DIR__ . "/gemini.php";
require_once __DIR__ . "/memory.php";
require_once __DIR__ . "/tool_registry.php";
require_once __DIR__ . "/tools/save_note.php";
require_once __DIR__ . "/tools/search_web.php";
require_once __DIR__ . "/tools/send_email.php";

function run_tool(string $name, array $args, string $sessionId): string
{
    return match ($name) {
        "save_note" => save_note_tool($args, $sessionId),
        "search_web" => search_web_tool($args),
        "send_email" => send_email_tool($args),
        default => json_encode(["success" => false, "message" => "Unknown tool"])
    };
}

function run_agent(string $message, string $sessionId): string
{
    $history = load_memory($sessionId);

    $history[] = [
        "role" => "user",
        "parts" => [
            ["text" => $message]
        ]
    ];

    $tools = tool_definitions();
    $limit = 5;
    $step = 0;

    while ($step < $limit) {
        $step++;

        $response = gemini_request($history, $tools);
        $parsed = parse_gemini_response($response);

        if ($parsed["type"] === "text") {
            $history[] = [
                "role" => "model",
                "parts" => [
                    ["text" => $parsed["text"]]
                ]
            ];

            save_memory($sessionId, $history);
            return $parsed["text"];
        }

        if ($parsed["type"] === "function_call") {
            $toolName = $parsed["name"];
            $toolArgs = $parsed["args"];

            $result = run_tool($toolName, $toolArgs, $sessionId);

            $history[] = [
                "role" => "model",
                "parts" => [
                    [
                        "functionCall" => [
                            "name" => $toolName,
                            "args" => $toolArgs
                        ]
                    ]
                ]
            ];

            $history[] = [
                "role" => "function",
                "parts" => [
                    [
                        "functionResponse" => [
                            "name" => $toolName,
                            "response" => [
                                "content" => $result
                            ]
                        ]
                    ]
                ]
            ];
        }
    }

    save_memory($sessionId, $history);
    return "I could not complete the task within the allowed number of steps.";
}

This is the real architecture lesson.

The model is not doing everything. It is deciding what should happen next. PHP executes the action. The tool result goes back into the loop. Then the model keeps going.

That is how you turn a language model into an agent.

Expose the Public API Endpoint

Now we need one public entry point that receives user input and returns JSON.

index.php:

<?php

require_once __DIR__ . "/agent.php";

header("Content-Type: application/json");

$input = json_decode(file_get_contents("php://input"), true);

$message = trim($input["message"] ?? "");
$sessionId = trim($input["session_id"] ?? "");

if ($message === "" || $sessionId === "") {
    http_response_code(400);
    echo json_encode(["error" => "message and session_id are required"]);
    exit;
}

try {
    $reply = run_agent($message, $sessionId);

    echo json_encode([
        "reply" => $reply,
        "session_id" => $sessionId
    ]);
} catch (Throwable $e) {
    http_response_code(500);
    echo json_encode([
        "error" => $e->getMessage()
    ]);
}

This endpoint stays small on purpose.

Its job is not to reason. Its job is to validate the request, call the agent, and return the response.

That is clean API design.

Deploy on cPanel

One of the main reasons this stack is useful is that it can be deployed on infrastructure many teams already have.

Deployment steps:

  1. Upload the files to /public_html/agent/
  2. Create the database and user in cPanel
  3. Grant the user access to the database
  4. Update db.php with the real MySQL credentials
  5. Put your Gemini API key into gemini.php
  6. Make sure PHP 8.1 or newer is selected
  7. Make sure cURL is enabled
  8. Protect the tools folder with .htaccess

Once that is done, your endpoint should be live at something like:

https://yourdomain.com/agent/index.php

The nice part here is that the stack does not require special hosting assumptions. It runs in a very common web environment.

Test the Agent

Before calling this production-ready, test the three things that matter most:

  1. request handling
  2. tool execution
  3. memory persistence

Save a note

curl -X POST https://yourdomain.com/agent/index.php 
  -H "Content-Type: application/json" 
  -d '{"message":"Save a note that our launch is on 1 September 2026","session_id":"demo123"}'

Search the web

curl -X POST https://yourdomain.com/agent/index.php 
  -H "Content-Type: application/json" 
  -d '{"message":"Search the web for current Gemini Flash information","session_id":"demo123"}'

Test memory

curl -X POST https://yourdomain.com/agent/index.php 
  -H "Content-Type: application/json" 
  -d '{"message":"What note did I save earlier?","session_id":"demo123"}'

If that last call works, then the memory layer is doing its job.

What This Architecture Teaches Us

The biggest takeaway from this project is not that PHP can call an LLM.

It is that production-ready AI does not have to be overengineered.

A few practical lessons stand out:

  1. A simple stack can still be powerful. You do not need heavyweight cloud infrastructure to ship useful AI features.
  2. Function calling is the key bridge. The model becomes much more useful when it can safely trigger real tools.
  3. Memory changes everything. Stateless prompts are not enough if you want continuity.
  4. Clear boundaries make the system safer. The model reasons, PHP executes, and MySQL persists.
  5. Practical beats trendy. A system that is easy to deploy and maintain is often more valuable than one built on fashionable infrastructure.

That is why this architecture is interesting. It is not trying to impress you with complexity. It is trying to solve the actual problem.

Conclusion

You do not need a giant cloud stack to build a useful AI agent.

With PHP, MySQL, Gemini Flash, and cPanel, you can build a system that reasons, calls tools, stores memory, and runs on infrastructure that is cheap, familiar, and already available to many developers.

That makes this approach especially useful for founders, indie hackers, and small teams that want to ship real AI functionality without turning their backend into a science project.

The core lesson is simple: production-ready AI does not have to be complicated. It just has to be well structured.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.