Laravel AI SDK. This tool is not meant to help you code, but to make your application intelligent.
If you want your user to be able to:
- Generate an image of a "space donut" from a form.
- Automatically translate a comment.
- Interact with a chatbot that uses local or external models.
Then you use the SDK. It's a library that lets you fire prompts directly from your PHP code. Unlike Boost, this is a regular dependency of the application, as it will be part of the features your end users will enjoy.
Installation and Configuration of the Laravel AI SDK
Unlike Laravel Boost, the Laravel AI SDK is what allows us to get our hands dirty by creating our own prompts and artificial intelligence logic within the app.
First steps:
- Installation: Run the installation command:
$ composer require laravel/ai$ php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"$ php artisan migrate
- Database: You will see that migrations are created for conversations and agents tables. It is essential to run
php artisan migrateso that the SDK has a place to save the history and configuration. - API Key: In your
.envfile, you must configure your key.ANTHROPIC_API_KEY= COHERE_API_KEY= ELEVENLABS_API_KEY= GEMINI_API_KEY= MISTRAL_API_KEY= OLLAMA_API_KEY= OPENAI_API_KEY= JINA_API_KEY= VOYAGEAI_API_KEY= XAI_API_KEY=
If you are looking for a free option to start, Google AI Studio is one of the best. You just have to go in, generate your Gemini API Key and paste it into your project.
The Heart of the SDK: The Agents
The big difference here is that everything works through Agents.
- An Agent is like a "virtual employee" to whom you assign a specific task (e.g.: "You are a translator", "You are an SEO expert", "You are an image generator").
- Once the agent is configured, you interact with it through prompts.
use Laravel\Ai\Enums\Lab;
use function Laravel\Ai\agent;
Route::get('/laravel-ai-text', function () {
$response = agent(
instructions: 'You are an expert assistant in Laravel.',)->prompt(
'Generate a list of 3 Laravel 13 topics in JSON format',
provider: Lab::Gemini,
model: <YOURIDLOCALMODAL>);
dd($response);
});What we saw is an anonymous agent, since the recommended structure is through agents, which we will see later.
Google Antigravity: Power without Complications
We have already overcome the learning curve of Laravel Boost and OpenCode, so Google Antigravity will seem like a walk in the park. It is simpler because it is less abstract: it is an IDE that already has AI in its "DNA".
The big difference: Integration vs. Personalization
We must be clear here: they don't "match".
- OpenCode (VS Code): It is an open environment. You install the agents, configure the Markdown files and have surgical control.
- Anti Gravity: It is a closed environment. It is more limited in terms of external rules (you can't directly copy and paste the OpenCode guides here), but in return it offers top-tier models (Gemini 3 Pro, Claude, Sonnet) for free in its current plan.
Configuration and Rules (Custom Instructions)
Although it is more closed, it doesn't mean you can't give it orders. In the three points of the interface, you can customize the rules, either globally or for a specific workspace.
- When you save these rules, a
gemini.mdfile is generated (very similar to what we saw before). - My advice: I don't waste much time configuring this here. I use Anti Gravity for specific developments where I give it all the necessary information in the prompt myself.
Agents: Planning vs. Fast (Phase)
As in OpenCode, here we have two working modes:
- Planning Mode: Ideal for complex tasks. The AI analyzes the whole project, proposes a plan and, if you give the OK, executes. It can even open a browser to verify if what it did works in real time.
- Fast Mode (Phase): It's the one I use the most. It is for quick and direct changes. You tell it: "Modify this file to include the CKEditor plugin" and it does it instantly.
My Workflow
When I work with AntiGravity, I follow a very marked cycle:
- Iteration: I request the change and wait for it to finish.
- Testing: If it works, I sync with Git (
git commit). - Correction: If it doesn't work, I ask it to correct.
- Reversion: If I see that the AI is getting confused and not getting it right, I use
git checkoutorgit reset, clear the table and start again from a safe point.
Visual advantages
One thing I love about this tool is the visual help. It marks in yellow exactly which lines it is going to change before applying them. That gives you a security that you don't always have in the VS Code terminal.
Which one to choose? OpenCode vs. Antigravity
If you are on the free plan, this is the dilemma:
- OpenCode: You have total customization, you can use all the skills from Laravel Boost and you have enormous flexibility, but it requires that you know the tool well.
- Antigravity: You have the best models on the market (including Claude, GPT…) for free and a much friendlier interface for fast development, but with less capacity for deep auditing.
In summary: I use OpenCode for audits, database reviews or complex Laravel architectures, and Antigravity to create new features quickly and visually.
Redesign Practice with Google Antigravity
Let's put Google Anti Gravity to the test with a real exercise: a simple redesign of the Laravel application we are building. Although I trust the tool, my first recommendation is always the same: sync with Git before starting. That way, if you don't like the result, a git reset brings you back to life in seconds.
1. Setting the Stage (Context)
So that the AI doesn't hallucinate, it's best to give it context. In this case:
- I copied the relative path of the
cssfolder (where the blog and dashboard styles are located). - I looked for an inspiration image on the internet to give it as a visual reference.
2. The Prompt: Clear and Direct
My goal is not to change the HTML structure (so as not to complicate the course), but only the style. I used Fast Mode and the Gemini 3 Flash model. The prompt was something like this:
"Perform a simple redesign of the app, overwriting as much as possible only the CSS located at:
resources/css/blog.css
resources/css/dashboard.css
I want you to be inspired by the attached image."
Note: If you have doubts about how to approach a major change, use Planning Mode; but for something as specific as this, Fast Mode is perfect.
Execution and Results
Unlike OpenCode, here you see how the AI starts scanning the project visually. It marks in yellow exactly which CSS lines it is going to change or add. It is a brutal visual help.
This is a perfect example of how to interact with the AI in Anti Gravity:
- You ask: "Redesign this".
- You wait: You see how it applies the changes visually.
- You test: You register, authenticate and browse the app.
- You decide: Do you like the result? If the answer is "it's pretty good", you sync. If not, you revert with Git.
In my case, although there were small bugs that I will fix "behind the scenes", the result works for me.
Implement confirmation dialog when deleting
To close this practice with Google Antigravity, we are going to solve a pending detail: the confirmation dialog for deleting records. Currently, if you press "Delete", the post is deleted directly without asking, which is dangerous.
The Prompt and the Context
The good thing about Antigravity is that you don't need to be excessively specific if the code is well structured. I simply passed it the index.blade.php file of categories and asked:
Implement a confirmation dialog for the delete button:
resources/views/dashboard/post/index.blade.php
resources/views/dashboard/category/index.blade.php
I didn't have to tell it which button it was; the AI analyzed the context, found the tag that says "Delete" and understood perfectly where it should intervene.
The Solution: Native and Effective JavaScript
Look at what the AI did (you can see it in the yellow change bubbles): it implemented a solution with JavaScript directly in the onsubmit event of the form.
resources/views/dashboard/post/index.blade.php
resources/views/dashboard/category/index.blade.php
<form action="{{ route('category.destroy', $category) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this category?')">How does this logic work?
- It uses the native
confirm()function, which displays a popup window in the browser. - This function returns true (if you click OK) or false (if you click Cancel).
- By placing it inside the
onsubmit, if the user cancels, the form receives afalseand automatically stops the submission. It's a clean and direct way to protect data without needing to install heavy libraries.
Conclusion
This second practice demonstrates why I use Antigravity so much for what I call "spiritual development" (those quick adjustments that give you peace of mind).
If we had wanted something more complex, like a separate module in dashboard.js, the AI would have been in charge of creating the file, exporting the module and importing it correctly. What would take us several minutes of code "carpentry", the AI resolves in 3 or 4 minutes.
Laravel SDK AI
How to use a Local LLM with Laravel AI SDK and Jan.ai or LM Studio?
In this section, I will show you how to use a local large language model (LLM) using Jan.ai or LM Studio in conjunction with the Laravel AI SDK. This is an ideal solution if you are in regions where services like Gemini or ChatGPT are blocked, or if you are simply looking for privacy and cost savings.
What is a local LLM?
An LLM (Large Language Model) is an artificial intelligence designed to understand and generate human language. By running it locally, you take advantage of your own machine's power without depending on an internet connection or paid plans. The advantages are clear:
- Privacy: Your data never leaves your computer.
- No costs: There are no token limits or monthly subscriptions.
- No blocks: You do not depend on geographical restrictions.
Tools and Requirements
To run a local AI, you need software that manages the model and a computer with a certain amount of power. The most popular options are Ollama, LM Studio, and Jan.ai.
Hardware Considerations (RAM vs. VRAM)
The power of the model you choose will depend on your memory:
- Windows/Linux: They rely heavily on the video card's VRAM. If the model exceeds your capacity (e.g., you have 12GB of VRAM and try to run a heavy model), the system will use the RAM, and performance will drop drastically.
- Mac (M Chips): They use unified memory. This means the CPU and GPU share the same RAM (e.g., 24GB), which allows for running larger models (like those with 31B parameters) more smoothly.
Local Server Configuration
Both Jan.ai and LM Studio follow the OpenAI API standard, which greatly facilitates integration with Laravel.
- Installation: Download and install Jan.ai from its official site.
- Activate the Server: Go to Settings > Local API Server and activate it.
- Endpoint: By default, the server will run at http://localhost:1337/v1.
- API Key: You can configure a simple key (e.g., 12345) in the local server settings.
LM Studio, like Jan.ai, is another excellent option available to us for installing local AIs, and it also features a local server just like Jan.ai; therefore, we can also use it for our project in Flask WITH VIRTUALLY NO CHANGES, but unlike Jan, LM Studio DOES NOT use a BEARER protection token and by default its port is 1234 (which you can change); in the following table, I summarize what was mentioned:
| Feature | Jan.ai | LM Studio |
|---|---|---|
| Default URL | http://localhost:1337/v1 | http://localhost:1234/v1 |
| API Key | Any text (e.g., "sk-local") | Not required / Ignored |
| Compatibility | OpenAI Style (v1) | 100% OpenAI-compatible |
| Headers | Content-Type + Authorization | Content-Type (Auth optional) |
| Main Engine | nitro (specialized in GGUF) | llama.cpp |
You can download LM Studio from:
Its operation is very similar to Jan.ai, so you won't get lost; furthermore, just like Jan, you can connect to LM Studio exactly the same way because LM Studio exposes a 100% OpenAI-compatible API, in which we have some interesting endpoints we can use:
- v1/chat/completions
- /v1/models
LM Studio does not use an API KEY
This means you must remove the Authorization header because if you send it, LM Studio either ignores it or throws a 401 in some cases.
Integration with Laravel AI SDK
To connect Laravel with your local AI, we must perform a small "trick" in the .env file. Since the Laravel SDK does not have a native driver for Jan.ai, we will use the Deepseek or OpenAI driver to "bridge" (OpenAI Driver Emulation) the connection.
Configuration in the .env
# We use the Deepseek driver to take advantage of its endpoint compatibility
AI_DRIVER=deepseek
DEEPSEEK_API_KEY=your_local_key_12345
DEEPSEEK_BASE_URL=http://localhost:1337/v1Publish the configuration file:
$ php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"Connect a Local LLM with Jan.ai to a Python app with Flask
In this section, we are going to connect to the Local LLM.
Activate the local server
Before connecting, we must activate the Jan.ai server:
Settings → Local App Server → Set your key → Start
In LM Studio: Go to the Local Server tab (the double arrow or developer icon).
Click the Start Server button. (Unlike Jan.ai, you don't need to configure an API key here; access is direct via the base URL provided by the program.)
Base controller to connect to the LLM
Regarding the main part, the controller that will connect to the LLM, we have:
- URL and headers
We define the endpoint URL to which we will make the request:
http://localhost:port/v1/chat/completions
And the headers that Jan needs:
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer 12345"
}- Model → the one you have selected in Jan (you must write it identically, for example: gemma-3-12b).
- Message → JSON array with the user role and the content.
In this example: “tell me a joke”.
data = {
"model": "gemma-3-12b-it-IQ4_XS",
"messages": [
# {"role": "user", "content": request.form.get("prompt", "Tell me a joke.")}
{"role": "user", "content": prompt}
]
}Making the request
Since this can fail, we wrap it in a try block.
We perform a requests.post(...) sending:
- URL.
- headers.
- json=data (no body, because it is explicitly JSON).
Then we validate the response: if response.status_code == 200, everything is perfect.
- 404 would be route not found.
- 500 = something exploded on the server or you sent something invalid.
Execution in code
To make a request, you can use an agent or a direct call. It is vital that the model name matches exactly with the ID provided by Jan.ai (you can check it in the list of downloaded models or via Postman).
use Illuminate\Support\Facades\AI;
$response = AI::chat('Generate a list of 3 Laravel 13 topics in JSON format')
->withDriver('deepseek')
->withModel('gemma-2-9b-it') // Replace with your local model ID
->send();
return $response;Which local model to select
If you don't know which model to download, I recommend starting with Gemma (from Google) or Llama. If you have doubts about which one your computer supports, you can take a screenshot of your technical specifications and ask the AI itself; it will tell you if a light 7B parameter model suits you or a more robust 31B one.
Prompt: I have a Mac mini M4 Pro, which one should I use? Give me a description for what to use each one for and which one you recommend.
- The "Heavyweights" (31B) - For Complex Reasoning
Models with 31 billion parameters. They are the smartest on the list.
- Gemma-4-31B-It-GGUF:
- What for: Ideal for when you need the AI to understand complex software architecture, solve difficult bugs in Laravel 13, or draft deep educational content for your academy.
- Performance on M4 Pro: It will run well, but will consume a lot of RAM (about 16-18 GB for the model alone). If you have 24GB or more of RAM, this is your primary model.
- The "Balance" (26B) - The All-rounder
Models with 26 billion parameters.
- Gemma-4-26B-A4B-It-GGUF:
- What for: It is a "Mixture of Experts" (MoE) architecture. It is faster than the 31B but maintains almost the same intelligence. Perfect for daily code generation and unit testing.
- Performance on M4 Pro: It will fly. It is the sweet spot between speed and "brain".
- The "Lights" (E4B / E2B) - For Automation and Speed
Models with 4 and 2 billion parameters.
- Gemma-4-E4B / E2B:
- What for: These are "Edge" models, designed to be ultra-fast. Use them for simple tasks like code autocompletion, JSON formatting, or summarizing short texts.
- Performance on M4 Pro: Instantaneous. You will hardly notice it processing.
Analysis for the RTX 5070
Unlike the Apple processor, the 5070 has Tensor cores specifically designed for AI. This means the models you choose will respond almost in real-time.
- Gemma-4-26B-A4B-It-GGUF (Recommended):
- Why: Being a 26B MoE (Mixture of Experts) architecture, it only activates a fraction of its parameters for each word (in this case, 4 billion active).
- On the 5070: It will fit very well in VRAM if you use Q4_K_M or Q5 type quantization. You will have a text generation speed superior to the Mac.
- Gemma-4-31B-It-GGUF (The Limit):
- Why: This model is dense (always uses all 31B).
- On the 5070: It is likely to "overflow" the video card's VRAM. When a model doesn't fit in the video card, it moves to normal RAM (the i7 14700K), and the speed drops drastically (from 50 tokens per second to 2 or 3).
- Usage: Only use it if you need the highest possible precision for a complex tutorial and don't mind waiting a few extra seconds.
- Gemma-4-E4B-It-GGUF (For development):
- Why: It is small (4B).
- On the 5070: It is instantaneous. You can use it as a co-pilot that suggests code in real-time while you write your books or Laravel courses without the GPU fan even flinching.
With this, you now have your own fully private and functional artificial intelligence environment in Laravel!
Practical Anonymous Agents Examples: Chat, Code, and Sentiment Analysis
To give you more variety, I have prepared three additional examples. Although the base is similar to what we have seen, these cases will help you visualize how to structure a real and functional project in the future, beyond a simple static request.
I have created a new controller with three specific methods:
- General Chat: An open conversation.
- Code Generation: Creating structures such as Laravel migrations.
- Sentiment Analysis: Determining if a message is positive, negative, or neutral.
routes\web.php
// Basic Chat
Route::get('/agents/chat', [AgentsTestController::class, 'chat']);
// Code generation
Route::get('/agents/generate-code', [AgentsTestController::class, 'generateCode']);
// Sentiment analysis
Route::get('/agents/analyze', [AgentsTestController::class, 'analyze']);app\Http\Controllers\AgentsTestController.php
<?php
namespace App\Http\Controllers;
use App\Ai\Agents\PokemonAgent;
use App\Ai\Agents\QuizAgent;
use App\Ai\Agents\QuizGenerator;
use App\Ai\Agents\SalesCoach;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Ai\Enums\Lab;
use function Laravel\Ai\agent;
/**
* Controller for examples using Gemma 3 with local AI (Ollama).
*/
class AgentsTestController extends Controller
{
/**
* Basic Chat
*/
public function chat(Request $request): JsonResponse
{
$message = $request->input('message', 'Hello, tell me what you know about Laravel');
$response = agent(
instructions: 'You are a helpful and concise assistant.',
)->prompt(
$message,
model: <YOURIDLOCALMODAL>
);
return response()->json([
'answer' => $response->text,
'model' => 'gemma3:12b-it-IQ4_XS (Ollama)',
]);
}
/**
* Code generation
*/
public function generateCode(Request $request): JsonResponse
{
$type = $request->input('type', 'users migration');
$response = agent(
instructions: 'You are a Laravel expert. You generate clean code and follow best practices.',
)->prompt(
"Generate {$type} in Laravel 13. Only give me the code, without explanations.",
model: <YOURIDLOCALMODAL>
);
return response()->json([
'code' => $response->text,
'request' => $type,
]);
}
/**
* Text analysis
*/
public function analyze(Request $request): JsonResponse
{
$text = $request->input('text', 'This product is incredible, I totally recommend it');
$response = agent(
instructions: 'You are a sentiment analyzer. You respond only with positive, negative, or neutral.',
)->prompt(
"Classify the following text: {$text}",
model: <YOURIDLOCALMODAL>
);
return response()->json([
'sentiment' => trim($response->text),
'original_text' => $text,
]);
}
}Analysis of the new examples
1. Dynamism with request()->input()
For this to be a real web application, we need the user to interact. I have configured the methods to look for a field called "text" in the request. If it doesn't exist, we use a default value:
$input = $request->input('text', 'What do you know about Laravel?');This will allow us to easily connect our logic to a user form in the future.
2. Anonymous Agents and Sentiment Analysis
In these examples, we are using Anonymous Agents. These are agents that we define "on the fly" with specific instructions. For example, for the sentiment analyzer:
Instruction: "You are an expert at analyzing sentiments. Respond only: Positive, Negative, or Neutral".
Input: "This product is incredible, I recommend it!"
Expected result: Positive.
3. Custom Code Generation
Here we combine a fixed prompt with the user's input. We tell the AI that it is an expert in Laravel 13 and that it must generate code without additional explanations.
$type = $request->input('type', 'users migration');
->prompt("Generate {$type} in Laravel 13. Only give me the code, without explanations.",***)If the user asks for "users migration", the system generates the exact database schema.
Response examples
http://larapackage.test/agents/chat
{"answer":"Laravel is an open-source PHP framework designed to facilitate web application development following the MVC (Model-View-Controller) pattern. Its main features are:\n\n- **Eloquent ORM**: active and simple model for interacting with databases.\n- **Declarative routes**: clear definition of URLs and associated controllers.\n- **Middleware**: filtering HTTP requests before reaching business logic.\n- **Blade**: lightweight template engine that allows writing views with clean PHP syntax.\n- **Migrations & Seeders**: structured management of database schema and initial data.\n- **Queues and events**: asynchronous processing and event-based architecture.\n- **Security**: CSRF protection, password hashing, out-of-the-box authentication.\n\nLaravel follows the \u201cconvention over configuration\u201d principle, which speeds up productivity without sacrificing flexibility. It is ideal for both small projects and complex enterprise applications.","model":"gemma3:12b-it-IQ4_XS (Ollama)"}http://larapackage.test/agents/generate-code
{"code":"```php\n<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n public function up(): void\n {\n Schema::create('users', function (Blueprint $table) {\n $table->id();\n $table->string('name');\n $table->string('email')->unique();\n $table->timestamp('email_verified_at')->nullable();\n $table->string('password');\n $table->rememberToken();\n $table->timestamps();\n });\n }\n\n public function down(): void\n {\n Schema::dropIfExists('users');\n }\n};\n```","request":"users migration"}http://larapackage.test/agents/analyze
{"sentiment":"positive","original_text":"This product is incredible, I totally recommend it"}Final observations about the responses
Remember that, even if the prompt is the same, AIs are stochastic; that is, they will not always give the exact same answer, which is part of the magic of natural language processing.
If you notice that the connection takes time or fails occasionally, it is usually due to the load on your hardware (CPU/GPU) when processing the model locally and not a programming error. We already have the foundations! In the next class, we will stop using anonymous agents to create structured Agents, with their own classes and a much more robust logic.
There are many errors that can occur such as:
Illuminate\Http\Client\ConnectionException
vendor\laravel\framework\src\Illuminate\Http\Client\PendingRequest.php:1805
cURL error 28: Operation timed out after 60013 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://127.0.0.1:1337/v1/chat/completions
--
Error [400]: Unknown errorStructured Agents: Beyond Anonymous Agents
So far we have worked with anonymous agents, which are the fastest and simplest way to start. However, in the Laravel AI SDK documentation, we find what we can call "named agents" or agent classes. These allow us to organize AI logic in a much more professional and reusable way.
1. Structure of an Agent
When creating an agent (like the SalesCoach example), we find a very interesting structure:
- Constructor: Allows passing external data to the agent.
- Instructions (instructions): Defines the behavior or role of the AI.
- Messages (messages): An internal method to load previous conversations or historical context. This is vital so the AI doesn't "start from scratch" in each interaction.
- Tools (tools): Allow the AI to execute programmatic functions (for example, generating a random number or querying an external API) to solve specific tasks.
Creating Agents via CLI
To generate these agents, the SDK offers two main commands that define how the AI's response will be:
Standard Agent:
$ php artisan make:agent SalesCoachCreates a basic structure with instructions, messages, and tools, but without a mandatory output format.
Agent with Structure (Schema):
$ php artisan make:agent SalesCoach --structured<?php
namespace App\Ai\Agents;
use App\Ai\Tools\RetrievePreviousTranscripts;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;
class SalesCoach implements Agent, HasTools //Conversational
{
use Promptable;
public function __construct(public ?User $user = null) {}
/**
* Get the instructions that the agent should follow.
*/
public function instructions(): Stringable|string
{
return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
}
/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
***
}
/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'feedback' => $schema->string()->required(),
'score' => $schema->integer()->min(1)->max(10)->required(),
];
}
}This is the most interesting one for us, as it implements a Schema.
In Laravel we are used to everything being heavily structured. If you query a Post model, you know exactly what fields to expect. If you use an all() method, you expect a collection. With AI, if we don't define a schema, the response format can vary and break our application.
Why use structured responses?
By defining a schema, we force the AI to make its response fit exactly into the format we need (usually JSON). This is excellent because:
- Integration: You can map the AI response directly to a Livewire component or a controller.
- Consistency: If you have a planets application, you can demand that the AI always returns: name, description, diameter, and density.
- Security: You ensure that your code can process the data without unexpected formatting errors.
The Pokémon example
For this example, I have configured an expert Pokémon agent. The instruction is clear: "You are a Pokémon expert, generate realistic data in exact JSON format." Although the SDK allows defining the structure through code, it is always good practice to reinforce the desired format in the prompt.
This part is fundamental because it works very similarly to form validations in Laravel, ensuring data integrity.
routes\web.php
// Structured Output: Pokemon list with PokemonAgent
Route::get('/agents/pokemon-list', [AgentsTestController::class, 'listPokemon']);app\Http\Controllers\AgentsTestController.php
public function listPokemon(Request $request): JsonResponse
{
$result = (new PokemonAgent)->prompt(
'Generate a list of 3 different Pokemon',
model: <YOURIDLOCALMODAL>
timeout: 120
);
// dd($result);
return response()->json([
'result' => $result->toArray(),
'schema' => [
'pokemon' => [
'*' => [
'name' => 'string',
'type' => 'enum',
'size' => 'integer',
],
],
],
]);
}The agent looks like:
app\Ai\Agents\PokemonAgent.php
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
* Agent to generate Pokemon list with structured schema.
*
* Schema returned:
* - pokemon: array of objects
* - name: string
* - type: enum (fire, water, electric, grass, normal, flying, psychic)
* - size: integer (10-500 cm)
*/
// php artisan make:agent SimpleAgent --structured
class PokemonAgent implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): Stringable|string
{
return 'You are a Pokemon expert. You generate realistic Pokemon lists in exact JSON format.';
}
public function schema(JsonSchema $schema): array
{
return [
'pokemon' => $schema->array()->items(
$schema->object([
'name' => $schema->string()->required(),
'type' => $schema->string()->enum([
'fire',
'water',
'electric',
'grass',
'normal',
'flying',
'psychic',
'rock',
'ground',
])->required(),
'size' => $schema->integer()->min(10)->max(500)->required(),
])
)->required(),
];
}
}To ensure that the AI returns exactly what our application needs, we use the internal validation methods of the SDK. The structure is defined as follows:
- Name: String type and required.
- Types: A list of allowed values (fire, water, electric, etc.).
- Size: An integer value with a defined minimum and maximum range.
Quiz Generator Example (Q&A)
The main instruction for this agent is: "Generate questions and answers based on the content of the posts; generate True or False type questions." To ensure the output is processable, we define a Schema with three mandatory fields:
- Question: The statement of the inquiry.
- Answer: The boolean value (True/False).
- Explanation: The reasoning behind the answer to help the student.
routes\web.php
// True/False Quiz based on Posts (simple version with prompt)
Route::get('/agents/quiz-simple', [AgentsTestController::class, 'quizPostsSimple']);We can pass the number of questions we want (defaulting to 5) and a list of identifiers (IDs). To process the IDs, we use the PHP explode function, which converts a comma-separated string (e.g., 1,2,3) into an array, allowing us to use Eloquent's whereIn method.
app\Http\Controllers\AgentsTestController.php
public function quizPostsSimple(Request $request): JsonResponse
{
$ids = $request->input('ids'); // 1,2,3
$cantidad = $request->input('cantidad', 5);
$posts = Post::when($ids, fn($query) => $query->whereIn('id', array_filter(explode(',', $ids))), fn($query) => $query->where('posted', 'yes')->limit(1))
->get(['title', 'content']);
if ($posts->isEmpty()) {
return response()->json(['error' => 'No hay contenido'], 404);
}
$contenido = $posts->map(fn($post) => "Título: {$post->title}\nContenido: {$post->content}")->join("\n\n---\n\n");
$prompt = "Basándote en los siguientes posts, genera {$cantidad} preguntas de verdadero o falso:\n\n{$contenido}";
$resultado = (new QuizAgent)->prompt(
$prompt,
model: <YOURIDLOCALMODAL>
);
return response()->json([
'quiz' => $resultado->toArray(),
'posts_utilizados' => $posts->count(),
]);
}Once the post is obtained, we extract the title and the body of the article to feed the agent. It is important to distinguish between the agent's instruction (its general role) and the specific task (the current prompt), which in this case tells it exactly how many questions to generate based on the provided text.
The agent remains as follows:
app\Ai\Agents\QuizAgent.php
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
* Agent to generate True/False quizzes based on posts.
*
* Returned Schema:
* - questions: array of objects
* - question: string
* - answer: boolean (true = true, false = false)
* - explanation: string
*/
class QuizAgent implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): Stringable|string
{
return 'You are a quiz question generator. Based on the content of the posts, you generate true or false questions. The questions must be clear and the correct answer must be based on the content.';
}
public function schema(JsonSchema $schema): array
{
return [
'preguntas' => $schema->array()->items(
$schema->object([
'pregunta' => $schema->string()->required(),
'respuesta' => $schema->boolean()->required(),
'explicacion' => $schema->string()->required(),
])
)->required(),
];
}
}
Example output:
- Question: "Is Laravel Volt a runtime environment for PHP?"
- Answer: "False".
- Explanation: "Laravel Volt is a functional API for Livewire that allows writing logic and templates in the same file."
The importance of the Schema
Here we reach what you mention as the "Achilles heel" of AI in software development: the lack of structure.
In Laravel we are used to everything being heavily structured. If you query a Post model, you know exactly what fields to expect. If you use an all() method, you expect a collection. With AI, if we don't define a schema, the response format can vary and break our application.
Why use structured responses?
By defining a schema, we force the AI to make its response fit exactly into the format we need (usually JSON). This is excellent because:
- Integration: You can map the AI response directly to a Livewire component or a controller.
- Consistency: If you have a planets application, you can demand that the AI always returns: name, description, diameter, and density.
- Security: You ensure that your code can process the data without unexpected formatting errors.
You already know how to generate these agents and why structure is the key to integrating artificial intelligence into a developer's workflow. We won't always want simple text on the screen; most of the time we will need data that our application can understand and process.
The Quiz Agent: From Example to Real-World Utility
I want to delve deeper into the "Quiz" agent that we implemented earlier. Beyond the theoretical examples we've already seen, I want us to analyze it from a functional and realistic point of view. My purpose is for you to understand how to adapt a similar scheme for any other task you need in your own projects.
To recap: this agent has a fixed task, which is to generate questions and answers in a specific format. On this occasion, I am not using the course project, but one of my own where I am migrating my current application to a clean environment. I have decided to integrate this quiz generator because, as you know, my books and courses are closely related (one-to-one relationship); therefore, all the written content is perfect material for evaluating the student.
The Problem with Manual Creation
Creating labs or quizzes manually (for example, for Udemy) is a real pain. It requires a lot of time to think about which questions fit best in each section, write the correct and incorrect options, and give them variety. Due to lack of time, I often don't include them, and this is precisely where AI becomes an invaluable tool to solve a real problem in my workflow.
Technical Implementation with Livewire and PHP
In this implementation, I use Livewire. To call the method that generates the quiz, I use a wire:click from the view. If you haven't mastered Livewire yet, don't worry (we'll see it later); for now, think of this simply as a form that, upon submission, executes a function on the server written in PHP:
public function generarQuiz()
{
// $sections = BookSection::where('book_id', $this->model->id)->where('posted', 'yes')->where('orden', '>', 0)->get();
$sections = BookSection::where('book_id', $this->model->id)->where('posted', 'yes')->where('orden', 4)->get();
foreach ($sections as $s) {
$resultado = (new QuizAgent)
***
<flux:button size="xs" variant="danger" wire:click='generarQuiz'>
<flux:icon name="trash" />
</flux:button> The main advantage here is total integration. Unlike other approaches where you might use an external tool or an isolated assistant (such as custom GPTs or CLI tools), here everything happens within the same application. We don't need to connect external databases or export files; the PHP code reads the book chapter directly from the database and passes it to the agent.
Content and Context Analysis
The content is extensive since they are ENTIRE chapters of my books. Making 30 or 40 questions from this content manually could take an hour per chapter. With the agent, I simply pass the full text and it handles the heavy lifting.
Advantages Over the External Assistants Model
- Before reaching this solution, I used a much more rudimentary scheme:
- Extracted the information from the database.
- Pasted it into a local assistant (using Jan.ai or LM Studio).
- Requested the output in JSON.
- Converted that JSON to SQL to insert it into my database.
This flow was horrible and constantly failed due to context limits or incomplete responses. Direct integration in Laravel eliminates these ambiguities. By having the agent encapsulated, I can pass "super-prompts" that include the full content of the chapter without leaving my development environment.
Real-World Testing and Local Performance
I am currently testing this system with a local LLM. It is important to mention that, although I have a powerful Mac, trying to run very heavy models (like one with 31B parameters) can cause 500 errors without a clear description, simply because the system runs out of memory or exceeds timeout limits:
DeepSeek Error [500]: server_error - Compute error.
LARAVEL
13.4.0
PHP
8.4.19For these practices, I have configured Gemma 3 12B and I am evaluating the results. Note that the process can take time (which is why using Queues and Jobs in Laravel is ideal), but the result is coherent:
- Generated Question: "Why does the text describe the creation of a form in pure PHP as something 'ugly'?" which was based on one of the chapters at https://www.desarrollolibre.net/blog/php/essential-php-course-the-direct-route-to-laravel-and-codeigniter
- Quality: The logic of the response is in harmony with the original text and the explanation provided has total pedagogical coherence.
Conclusion and Next Steps
This "Pro" approach allows the AI to read your documents, Excels, or databases like crazy and generate immediate value. In the following classes, we will keep tweaking this assistant to improve the output format and optimize the processing of longer chapters, since the larger the content, the more processing power we require.
AI Agent + Work Queues
We are going to take our agent to the next level by using work queues. The reason is simple: AIs are often slow to respond, especially when we depend on third-party services or heavy local processes.
Laravel + AI
The beauty of having Laravel as an extremely robust framework, which allows us to do everything, together with AI, is that in this case we are not going to use Laravel as the bus for development, but as a tool to call and perform an action. In this case, just as we have been doing since the previous class, remember that I am linking it; it is the same agent, only here I called it differently, which is "chunks." I will explain why in a moment.
Laravel offers us a robust framework to handle these background tasks, allowing the AI to work as an external tool without blocking the main thread of our application.
The Technical Challenge: Massive Data Processing with AI
In a real development environment, the main problem is not just connecting to the AI, but how to manage the flow of information effectively. My current use case is a clear example: I need to generate a large number of questions and answers based on the theoretical content of my books.
Although this flow can be adapted to a thousand different scenarios, the constant remains the same: you have a large amount of data that must be processed without compromising the stability of the application.
1. The Risk of Interface Blocking
It is not viable to process these requests directly on the main thread. If we try to call the AI synchronously, the application will inevitably "hang" for seconds or even minutes. This creates a poor user experience and can cause timeout errors in the browser or on the server.
2. Efficiency and Scalability
For the processing to be truly effective, we must implement an architecture that allows:
- Decoupling the request from the processing: The user requests the generation and the application responds immediately, while the heavy lifting happens in the background.
- Work Queue Management: Utilize tools like Laravel's queues to manage tasks, allowing them to run one after another in an organized manner.
- Chunking: Divide the information into manageable pieces to avoid saturating server memory or AI context limits.
The Problem: The Blocked Main Thread
Previously, we called the agent directly from a Livewire class. By doing so via wire:click, the browser and the application would "hang" while waiting for the AI response.
In a real environment, if you need to process large volumes of data (such as generating hundreds of questions based on books), you cannot allow the interface to freeze. To solve this, we migrated the logic to a Laravel Job.
Load Management and Use of Local AIs
For our system to be stable, it is not enough to send tasks to the queue; we must be strategic with resource consumption. If you saturate your machine, the operating system (whether Windows or macOS) could terminate processes due to lack of memory or excessive load, which would interrupt the generation of your content.
1. The "Drip" Task Strategy
The main recommendation is to avoid sending a massive amount of jobs all at once. It is much more effective to pass a small group of tasks and, as the Laravel Workers become free, send the next batch.
In my case, I am processing fragments of my books. Instead of trying to have the AI "read" the entire book at once, I pass specific fragments directly in the message.
2. Input Format: Context in the Prompt
We are currently using the most traditional method: including all the theoretical content directly in the prompt's introduction.
- Advantage: It is direct and easy to implement at the start.
- Limitation: The prompt size has a limit (context window). If the fragment is too large, the AI may lose precision or simply fail.
3. Implementation with Local AIs
One of the great advantages of this flow is the possibility of using local models. This is ideal for working with sensitive data or simply to save on API costs.
- Ollama: It is currently the most robust and "official" option for integrating local models into applications.
- Alternatives: Although excellent tools like Jan.ai or LM Studio exist, direct integration via libraries is usually more optimized for Ollama.
4. Why use Local AIs?
The use of local models (like Llama 3 or Mistral) allows us to perform unlimited tests at no additional cost. We only need to ensure that our PC has the necessary power (especially in the GPU) to respond in acceptable times.
In addition to handling ALL data privately
Structure of the Work Queue (Job)
The Job is responsible for receiving content fragments (segmented into chunks) so that the AI does not collapse. These are the key configuration points:
- Execution Time: I have configured a high timeout (for example, 900 seconds) and the set_time_limit(0) method to prevent PHP or the queue system from killing the process prematurely.
- Tries: It is vital to handle exceptions. If the AI fails due to workload, the system will automatically retry the task.
public $timeout = 900; public $maxExceptions = 3; public $memory = 512; public $tries = 3;- In addition to other parameters such as the amount of allocated memory.
- Content Segmentation: We do not pass the entire book. We divide the text into fragments of about 1,500 characters. This ensures that local models (like Gemma 3 12B via Jan.ai or Ollama) can process the information effectively.
foreach ($sections as $s) { $chunks = mb_str_split($s->content, 15000, 'UTF-8'); foreach ($chunks as $content) { GenerateChunkQuizJob::dispatch(
JSON Schema Definition and Data Model
For the AI response to be useful, it must be structured. I have updated the schema to return a "Trivia" type format (Duolingo style):
- Question: The challenge text.
- Options: An array of strictly 4 options.
- Correct Answer: The index of the valid option and an explanation of why it is correct or incorrect.
$resultado = (new QuizAgent)
->prompt(
"
Act as a programming teacher.
Read this fragment of my book: '{$content}'
Generate a quiz of {$this->numQuestions} questions based solely on that text.
Respond ONLY with valid JSON, without additional text or markdown.
Required structure:
{
\"questions\": [
{
\"text\": \"question\",
\"options\": [\"option1\", \"option2\", \"option3\", \"option4\"],
\"options_message\": [\"message1\", \"message2\", \"message3\", \"message4\"],
\"correct_answer_index\": 0,
\"language\": \"spanish\"
}
]
}
Give it to me in {$this->language}
",
model: $this->modelAi
);
public function schema(JsonSchema $schema): array
{
return [
'questions' => $schema->array()->items(
$schema->object([
'text' => $schema->string()->required(),
'options' => $schema->array()->items(
$schema->string()
)->min(4)->max(4)->required(),
'options_message' => $schema->array()->items(
$schema->string()
)->min(4)->max(4)->required(),
'correct_answer_index' => $schema->integer()->required(),
'language' => $schema->string()->enum(['english', 'spanish'])->required(),
])
)->required(),
];
}The migration looks like this:
Schema::create('learning_questions', function (Blueprint $table) {
$table->id();
$table->text('text');
// $table->enum('question_type', ['multiple_choice', 'code'])->default('multiple_choice');
$table->json('options'); // Stores options as a JSON array
$table->json('options_message'); // Stores messages as a JSON array
$table->unsignedInteger('correct_answer_index');
$table->enum('language', ['english', 'spanish'])->default('spanish');
// Assuming book_sections and tutorial_sections are other tables
$table->foreignId('book_section_id')->nullable()->constrained('book_sections')->onDelete('cascade');
$table->foreignId('tutorial_section_id')->nullable()->constrained('tutorial_sections')->onDelete('cascade');
$table->timestamps();
});The interesting thing about this system is the synergy we achieve between an Eloquent model and the responses generated by the AI. By using JSON-type fields in our database migrations, we eliminate the friction of having to map each field individually, allowing the data to flow directly from the language model's response into our application.
1. The Use of JSON Fields in Migrations
- In the Learning model, we have defined the table structure with JSON columns. This offers us enormous flexibility:
- Direct Persistence: We save the object exactly as returned by the AI (after prior validation).
- Ease of Processing: Laravel automatically converts these fields into PHP arrays or collections, facilitating their use in the frontend or business logic.
2. Linking Between Laravel and AI
The key here is the direct relationship. We are not simply receiving plain text; we are integrating the AI's reasoning capability with Laravel's robustness.
- The flow is clean: we send a book chapter as context.
- The AI generates the questions and options under a predefined schema.
- Laravel receives, validates, and inserts the result into the database.
3. Automation of Data Flow
The ultimate goal is total automation. Unlike a manual process where you would have to copy and paste each question, here the AI acts as an intelligent data processor.
- Quality Verification: Although the AI does the heavy lifting, the system includes a validation layer to ensure the response is valid JSON and meets requirements before insertion.
- Consistency: By tying the prompt to a specific book chapter, we guarantee that the answers are not "made up" at random, but are strictly based on the theoretical content provided.
This approach transforms Laravel into more than just a web development framework; it turns it into a knowledge management engine. By delegating structured content creation to the AI and data management to Eloquent, we can generate massive educational platforms with minimal manual effort.
Response Validation and "Cleaning"
We cannot blindly trust that the AI will return a perfect JSON. Therefore, we implement validation logic:
- Extract JSON: A method to clean the AI's response and extract only the JSON code block, removing additional text.
- Integrity Check: We verify that the JSON is decodable and contains all required fields before inserting it into the database.
- Error Logs: If decoding fails, the error is recorded in storage/logs/laravel.log for debugging.
if (! $jsonText) {
\Log::error('GenerateQuizJob: No JSON found in response', [
'book_section_id' => $this->bookSection->id,
'response' => mb_substr($responseText, 0, 1000),
]);
return;
}
$data = json_decode($jsonText, true);
if (json_last_error() !== JSON_ERROR_NONE) {
\Log::error('GenerateQuizJob: Invalid JSON after extract', [
'book_section_id' => $this->bookSection->id,
'json_error' => json_last_error_msg(),
]);
return;
}
if (! isset($data['preguntas'])) {
\Log::warning('GenerateQuizJob: No preguntas key in response', [
'book_section_id' => $this->bookSection->id,
'keys' => array_keys($data ?? []),
]);
return;
}
foreach ($data['preguntas'] as $pregunta) {
LearningQuestion::create([
'text' => $pregunta['text'],
'options' => $pregunta['options'],
'options_message' => $pregunta['options_message'] ?? [],
'correct_answer_index' => $pregunta['correct_answer_index'],
'language' => $pregunta['language'],
'book_section_id' => $this->bookSection->id,
]);
}Real-Time Execution
To get everything running, we execute the command:
$ php artisan queue:workThe complete codes are as follows:
app\Jobs\GenerateChunkQuizJob.php
<?php
namespace App\Jobs;
use App\Ai\Agents\QuizAgent;
use App\Models\BookSection;
use App\Models\LearningQuestion;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\SerializesModels;
class GenerateChunkQuizJob implements ShouldQueue
{
use Queueable, SerializesModels;
public $timeout = 900;
public $maxExceptions = 3;
public $memory = 512;
public function __construct(
public BookSection $bookSection,
public string $contentChunk,
public int $numQuestions = 3,
public string $modelAi = 'gemma-3-12b-it-IQ4_XS',
public string $language = 'español',
) {}
public function handle(): void
{
$resultado = (new QuizAgent)
->prompt(
"
Act as a programming teacher.
Read this fragment of my book: '{$this->contentChunk}'
Generate a quiz of {$this->numQuestions} questions based solely on that text.
Respond ONLY with valid JSON, without additional text or markdown.
Required structure:
{
\"preguntas\": [
{
\"text\": \"pregunta\",
\"options\": [\"opción1\", \"opción2\", \"opción3\", \"opción4\"],
\"options_message\": [\"mensaje1\", \"mensaje2\", \"mensaje3\", \"mensaje4\"],
\"correct_answer_index\": 0,
\"language\": \"spanish\"
}
]
}
Give it to me in {$this->language}
",
model: $this->modelAi
);
$responseText = trim((string) $resultado);
$jsonText = $this->extractJson($responseText);
if (!$jsonText) {
\Log::error('GenerateChunkQuizJob: No JSON found in response', [
'book_section_id' => $this->bookSection->id,
'response' => mb_substr($responseText, 0, 1000),
]);
return;
}
$data = json_decode($jsonText, true);
if (json_last_error() !== JSON_ERROR_NONE) {
\Log::error('GenerateChunkQuizJob: Invalid JSON after extract', [
'book_section_id' => $this->bookSection->id,
'json_error' => json_last_error_msg(),
]);
return;
}
if (!isset($data['preguntas'])) {
\Log::warning('GenerateChunkQuizJob: No preguntas key in response', [
'book_section_id' => $this->bookSection->id,
'keys' => array_keys($data ?? []),
]);
return;
}
foreach ($data['preguntas'] as $pregunta) {
LearningQuestion::create([
'text' => $pregunta['text'],
'options' => $pregunta['options'],
'options_message' => $pregunta['options_message'] ?? [],
'correct_answer_index' => $pregunta['correct_answer_index'],
'language' => $pregunta['language'],
'book_section_id' => $this->bookSection->id,
]);
}
}
private function extractJson(string $text): ?string
{
$text = trim($text);
if (preg_match('/```json\s*([\s\S]*?)```/', $text, $matches)) {
return trim($matches[1]);
}
if (preg_match('/\{[\s\S]*\}/', $text, $matches)) {
return trim($matches[0]);
}
return null;
}
}
app\Ai\Agents\QuizAgent.php
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;
/**
* Agent to generate quiz questions with multiple options.
*
* Returned schema:
* - preguntas: array of objects
* - text: string
* - options: array of 4 strings
* - options_message: array of 4 strings
* - correct_answer_index: integer (0-3)
* - language: enum ('english', 'spanish')
*/
class QuizAgent implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): Stringable|string
{
return 'You are a generator of quiz questions with multiple options. Generate questions with 4 theoretical options. Valid options are saved in "options" as a JSON array. True/false or success/error messages are saved in "options_message" as a JSON array. The correct option is indicated in "correct_answer_index" (0-3) and the language in "language".';
}
public function schema(JsonSchema $schema): array
{
return [
'preguntas' => $schema->array()->items(
$schema->object([
'text' => $schema->string()->required(),
'options' => $schema->array()->items(
$schema->string()
)->min(4)->max(4)->required(),
'options_message' => $schema->array()->items(
$schema->string()
)->min(4)->max(4)->required(),
'correct_answer_index' => $schema->integer()->required(),
'language' => $schema->string()->enum(['english', 'spanish'])->required(),
])
)->required(),
];
}
}public function generateQuiz()
{
$sections = BookSection::where('book_id', $this->model->id)
->where('posted', 'yes')
->where('orden', '>', 0)
->get();
foreach ($sections as $s) {
$chunks = mb_str_split($s->content, 15000, 'UTF-8');
foreach ($chunks as $content) {
GenerateChunkQuizJob::dispatch(
$s,
$content,
3,
'gemma-3-12b-it-IQ4_XS',
'español'
);
}
}
Session::flash('status', 'Tasks stored in the queue to generate the book questions');
}Other considerations
You might need to modify other server parameters in your php.ini such as:
- max_execution_time: It is likely set to 60. Change it to 0 (infinite) or 900.
- memory_limit: If the AI model or text processing consumes a lot of RAM, the process dies. Set it to at least 512M.
All these changes are to avoid errors like:
INFO Processing jobs from the [default] queue.
App\Jobs\GenerateQuizJob ..... RUNNING
App\Jobs\GenerateQuizJob ..... 1m FAIL
zsh: killed php artisan queue:workYou may notice that the work stops automatically after a minute or a fixed time; remember that AIs take a long time to respond, and if we add the prompt and the heavy task to this, we must maintain a balance between our computer's resources and the task we are performing.