<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aaron Lumsden</title>
    <description>The latest articles on DEV Community by Aaron Lumsden (@aaronlumsden).</description>
    <link>https://dev.to/aaronlumsden</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F161301%2F5f274269-aad5-4fae-a223-3fce77bf57f7.jpg</url>
      <title>DEV Community: Aaron Lumsden</title>
      <link>https://dev.to/aaronlumsden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronlumsden"/>
    <language>en</language>
    <item>
      <title>Vizra Evals and Pest's Evals Plugin: When You Want Which</title>
      <dc:creator>Aaron Lumsden</dc:creator>
      <pubDate>Thu, 27 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aaronlumsden/vizra-evals-and-pests-evals-plugin-when-you-want-which-2f3j</link>
      <guid>https://dev.to/aaronlumsden/vizra-evals-and-pests-evals-plugin-when-you-want-which-2f3j</guid>
      <description>&lt;p&gt;If you are testing AI agents in Laravel, there are now two packages with "evals" in the description, and the obvious question is whether you need both.&lt;/p&gt;

&lt;p&gt;Short answer: probably not, and which one depends on a single question. Do you need to know whether your agent is good &lt;em&gt;now&lt;/em&gt;, or whether it is worse than it was &lt;em&gt;last month&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;This is a genuine comparison rather than a sales pitch. I wrote one of them, and there is a section below telling you when to use the other one on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with what they share
&lt;/h2&gt;

&lt;p&gt;Vizra Evals is built on Pest. Not alongside it, not as an alternative to it. Your evals are Pest tests, they live in your test suite, and they run through the Pest binary.&lt;/p&gt;

&lt;p&gt;Both packages also use the same activation contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/pest             &lt;span class="c"&gt;# evals skipped&lt;/span&gt;
./vendor/bin/pest &lt;span class="nt"&gt;--evals&lt;/span&gt;     &lt;span class="c"&gt;# evals run against the real model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--evals&lt;/code&gt; flag and the &lt;code&gt;PEST_EVALS&lt;/code&gt; environment variable mean the same thing to both. That is deliberate. You can have both installed in one suite and neither will fight the other for the flag.&lt;/p&gt;

&lt;p&gt;So this is not a migration decision. Nothing has to be torn out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pest's evals plugin does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pestphp/pest-plugin-evals&lt;/code&gt; scores agent output inside an expectation. You point it at an agent, give it a prompt, and assert on the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CapitalCityAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'What is the capital of Japan?'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBeCorrect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Tokyo'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has a genuinely broad set of expectations. Deterministic ones that cost nothing: &lt;code&gt;toContain()&lt;/code&gt;, &lt;code&gt;toMatch()&lt;/code&gt;, &lt;code&gt;toBe()&lt;/code&gt;, &lt;code&gt;toBeJson()&lt;/code&gt;, &lt;code&gt;toHaveToolCalls()&lt;/code&gt; and &lt;code&gt;toFollowTrajectory()&lt;/code&gt;, which checks tools were called in a given order. Then scored ones that call a model: &lt;code&gt;toBeCorrect()&lt;/code&gt;, &lt;code&gt;toBeRelevant()&lt;/code&gt;, &lt;code&gt;toBeSafe()&lt;/code&gt;, &lt;code&gt;toBeSimilar()&lt;/code&gt; for semantic similarity via embeddings, &lt;code&gt;toSatisfy()&lt;/code&gt; for plain-English criteria, and &lt;code&gt;toPassScorer()&lt;/code&gt; for your own.&lt;/p&gt;

&lt;p&gt;Scorers return 0.0 to 1.0 against a configurable threshold. &lt;code&gt;toBeCorrect()&lt;/code&gt; is more nuanced than a boolean: an exact match scores 1.0, approximately equal 0.9, a superset of the expected answer 0.8, a subset 0.6, and a contradiction 0.0.&lt;/p&gt;

&lt;p&gt;It handles nondeterminism too. &lt;code&gt;repeat()&lt;/code&gt; runs the same prompt several times and asserts every expectation against all of the samples, so a test only passes when the agent is consistent. That is the right instinct and a lot of eval tooling misses it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prompt()&lt;/code&gt; also takes a plain closure, not just an agent class, so you can score anything that returns a string.&lt;/p&gt;

&lt;p&gt;This is a well-built plugin. If you have read this far expecting me to find fault with it, that is not where this is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vizra Evals adds
&lt;/h2&gt;

&lt;p&gt;One thing, and everything else follows from it: &lt;strong&gt;it keeps the runs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every sample, every score, every judge's reasoning, every tool call, every token cost, written to your own database. That turns a passing test into a data point rather than an event.&lt;/p&gt;

&lt;p&gt;Once runs are persisted, three things become possible that are not possible from a single assertion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A baseline.&lt;/strong&gt; Your first passing run becomes the reference. Everything after it is measured against that rather than against a fixed threshold you guessed at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Row-level regression detection.&lt;/strong&gt; Rows are joined across runs by a hash of their content, so the framework knows that "What is your refund policy?" in today's run is the same row as in last month's. When that specific row drops, the build fails and names it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Eval [pest: answers support questions from documented policy] score 61.7%,
pass rate 33.3% across 6 samples.
Gate failed: 2 rows regressed against the reference run (allowed: 0).
  ↓ regressed: "What is your refund policy?" 96.7% → 51.7%
  ↓ regressed: "Can I return it?" 93.3% → 55.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "the suite scored 61.7%". Which two inputs got worse, and by how much. That difference matters most in a pull request, where the useful question is never "is this good" but "is this worse than what we had".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;History you can look at.&lt;/strong&gt; A second package, &lt;code&gt;vizra/evals-ui&lt;/code&gt;, mounts a dashboard route inside your own app: score trends per suite, any sample's assertions and judge reasoning one click deep, two runs side by side. It reads the same database your evals already write to.&lt;/p&gt;

&lt;p&gt;The eval itself looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'answers support questions from documented policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SupportBot&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toPassEval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$eval&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;base_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'evals/support.jsonl'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;costBelow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Answers using only documented store policy.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minScore&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxRegressions&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;maxRegressions: 0&lt;/code&gt;. That parameter cannot exist without stored history, and it is the whole argument for this package in one keyword.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you want Pest's plugin on its own
&lt;/h2&gt;

&lt;p&gt;Plenty of the time. Genuinely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have a handful of agents and a handful of checks.&lt;/strong&gt; If what you need is "this classifier returns valid JSON" or "this agent never recommends a competitor", that is an expectation, and adding a database and a dashboard to it buys you nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want zero extra infrastructure.&lt;/strong&gt; Vizra needs a migration. Pest's plugin needs nothing. If you are evaluating in a package rather than an application, or in something with no database at all, that is close to decisive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You are exploring rather than shipping.&lt;/strong&gt; Working out whether the prompt is any good in the first place is a different activity from defending it over time. Expectations are the faster loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your agent's quality genuinely does not drift.&lt;/strong&gt; Some don't. A structured extraction task with a pinned model and a stable prompt can sit still for a year. If nothing changes, there is nothing to compare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't have a baseline worth defending yet.&lt;/strong&gt; Baselines are only useful once you have a version you are happy with. On week one you are still finding it.&lt;/p&gt;

&lt;p&gt;In all of those, the honest recommendation is to use Pest's plugin and stop reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the baseline starts to matter
&lt;/h2&gt;

&lt;p&gt;The switch tends to flip at a specific moment, and it is usually a question someone asks you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Did the answers get worse after we changed the model?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With expectations alone, the answer is a shrug, or a threshold that either failed or didn't. You cannot compare against a number you never wrote down.&lt;/p&gt;

&lt;p&gt;Concretely, the baseline earns its keep when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More than one person changes the prompts.&lt;/strong&gt; You want the reviewer to see which rows moved, not to take someone's word that it seems fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are changing retrieval.&lt;/strong&gt; Swapping an embedding model, changing chunk size, adding a reranker. These shift quality in ways that look fine on the three examples you happen to test and quietly get worse on the fiftieth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to explain a regression weeks later.&lt;/strong&gt; "It got worse sometime in the last month" is a very different investigation from "it got worse on the 14th, on these two rows, and here is what the judge said about each".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is creeping.&lt;/strong&gt; Per-sample cost is recorded, so you can see it climb rather than discover it on an invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something is at stake.&lt;/strong&gt; Regulated work, customer-facing answers, anything where "we think it's fine" is not an acceptable answer to an auditor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running both
&lt;/h2&gt;

&lt;p&gt;There is no conflict. A reasonable setup uses Pest's expectations for quick assertions during development, and Vizra evals for the suites you want to defend over time. Both respond to &lt;code&gt;--evals&lt;/code&gt;, so one command runs everything.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.vizra.ai/evals/pest" rel="noopener noreferrer"&gt;docs cover running them side by side&lt;/a&gt; if you want the detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pest evals plugin&lt;/th&gt;
&lt;th&gt;Vizra Evals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Question it answers&lt;/td&gt;
&lt;td&gt;Is this agent good?&lt;/td&gt;
&lt;td&gt;Is it worse than last month?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sampling&lt;/td&gt;
&lt;td&gt;repeat()&lt;/td&gt;
&lt;td&gt;samples()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scored assertions&lt;/td&gt;
&lt;td&gt;Correct, relevant, safe, similar, custom&lt;/td&gt;
&lt;td&gt;Judge with criteria and dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool call assertions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results persisted&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, your database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline comparison&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, per row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional, self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;A migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activation&lt;/td&gt;
&lt;td&gt;--evals&lt;/td&gt;
&lt;td&gt;--evals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are starting today, start with Pest's plugin. It is less to install and it answers the first question you will have.&lt;/p&gt;

&lt;p&gt;Come back to this when someone asks whether the answers got worse, and you realise you have no way to know.&lt;/p&gt;

&lt;p&gt;If you want to see what kept history looks like before installing anything, there is a &lt;a href="https://vizra.ai/demo?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blog-pest-comparison" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; with six weeks of runs behind it.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Test AI Agents in Laravel (Beyond Fakes)</title>
      <dc:creator>Aaron Lumsden</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:42:33 +0000</pubDate>
      <link>https://dev.to/aaronlumsden/how-to-test-ai-agents-in-laravel-beyond-fakes-4de0</link>
      <guid>https://dev.to/aaronlumsden/how-to-test-ai-agents-in-laravel-beyond-fakes-4de0</guid>
      <description>&lt;p&gt;Your test suite is green. All twelve AI tests pass, none of them touch a provider, and the whole run takes 200ms. Then someone tightens a system prompt on Tuesday, and three weeks later a customer forwards you a support reply that confidently invented a refund policy you do not have.&lt;/p&gt;

&lt;p&gt;Every test still passed. They were never testing for that.&lt;/p&gt;

&lt;p&gt;This post is about the gap: what fakes are genuinely good at, what they structurally cannot tell you, and how to test the part they leave out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fakes are good, and you should use them
&lt;/h2&gt;

&lt;p&gt;The Laravel AI SDK ships a faking layer that works like every other test double in the framework. You define responses up front and assert the right prompts went out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Ai\Facades\Ai&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Ai&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fakeAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SupportBot&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'We offer a 30 day return window.'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/chat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Can I return this?'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertOk&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Ai&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertAgentWasPrompted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SupportBot&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'return'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a &lt;a href="https://laravel-news.com/ship-ai-with-laravel-test-your-ai-system-with-zero-api-calls" rel="noopener noreferrer"&gt;good Laravel News tutorial&lt;/a&gt; on this, and it is worth your time. &lt;code&gt;preventStrayPrompts()&lt;/code&gt; in particular is the sort of thing you want on from day one. It fails the test if a code path calls a model you did not expect, which is how you find the accidental LLM call inside a loop before your bill does.&lt;/p&gt;

&lt;p&gt;What fakes give you is fast, free, deterministic tests of &lt;strong&gt;your code&lt;/strong&gt;. Does the route authenticate. Does the tool get called with the right arguments. Does the job get queued. Does the response get persisted. Every one of those is your logic, and every one of those should be tested with fakes, in CI, on every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a fake cannot tell you
&lt;/h2&gt;

&lt;p&gt;A fake returns the answer you wrote. That is the entire point of it, and it is also the boundary.&lt;/p&gt;

&lt;p&gt;When you assert that &lt;code&gt;SupportBot&lt;/code&gt; returned "We offer a 30 day return window", you are asserting that &lt;em&gt;you typed that string into the test&lt;/em&gt;. The model was never consulted. So the test passes identically whether the real agent is excellent, mediocre, or has been quietly degraded by a prompt change three commits ago.&lt;/p&gt;

&lt;p&gt;Two different questions, and it is worth being blunt about which is which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Does my code work?&lt;/strong&gt; Fakes. Fast, free, deterministic, run on every commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the answer any good?&lt;/strong&gt; Fakes cannot help. Nothing in a normal test suite can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams test the first and assume the second. It holds right up until it doesn't, and the way you find out is a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you cannot just write a normal test for it
&lt;/h2&gt;

&lt;p&gt;The obvious next thought is to skip the fake and call the real model. That breaks immediately, for a reason that is easy to state and easy to underestimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same input produces a different answer every time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask an agent the same question five times and you get five phrasings, sometimes five different levels of correctness. So &lt;code&gt;assertEquals&lt;/code&gt; is useless, and &lt;code&gt;assertStringContainsString&lt;/code&gt; is a coin flip you have dressed up as a verdict. It passes or fails depending on which roll you happened to run in CI.&lt;/p&gt;

&lt;p&gt;Worse, a single run tells you nothing about the distribution. An agent that gets it right 95% of the time and one that gets it right 55% of the time both look identical if you sample them once and get lucky. The thing you actually care about is the shape of the answers, not one of them.&lt;/p&gt;

&lt;p&gt;That is what an eval is: run each input several times, score every response, and look at the distribution rather than a single sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing one
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vizra-ai/vizra-evals" rel="noopener noreferrer"&gt;Vizra Evals&lt;/a&gt; is an MIT-licensed package that does this as Pest tests. Install it as a dev dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require vizra/evals &lt;span class="nt"&gt;--dev&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An eval looks like a test, because it is one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Agents\SupportBot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'answers support questions from documented policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SupportBot&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toPassEval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$eval&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;base_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'evals/support.jsonl'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;costBelow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Answers using only documented store policy.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minScore&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxRegressions&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataset is one JSON object per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is your refund policy?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30 days"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can I return a sale item?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not eligible"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And crucially, this does not run in your normal suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/pest             &lt;span class="c"&gt;# evals skipped, zero tokens, zero cost&lt;/span&gt;
./vendor/bin/pest &lt;span class="nt"&gt;--evals&lt;/span&gt;     &lt;span class="c"&gt;# evals run against the real model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation matters more than it looks. Evals cost real money and take real time. A test suite that quietly spends $4 every time a junior runs &lt;code&gt;pest&lt;/code&gt; is a test suite people stop running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three details worth understanding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;samples(3)&lt;/code&gt;&lt;/strong&gt;. Every row runs three times. This is the whole premise. One sample proves nothing about a nondeterministic system, and three is the smallest number that gives you a mean and a sense of spread. Push it higher for rows you care about most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-&amp;gt;gate()&lt;/code&gt;&lt;/strong&gt;. If &lt;code&gt;notEmpty()&lt;/code&gt; fails, everything after it is skipped for that sample, including the judge. A response that came back empty is already broken; sending it to another model to be scored is spending tokens to confirm what you know. Order your cheap deterministic checks first and gate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;judge(...)&lt;/code&gt;&lt;/strong&gt; is for the things you cannot assert with string matching. "Did it stay within documented policy" is not a substring check. A judge is a second model scoring the first, returning a structured &lt;code&gt;{score, reasoning}&lt;/code&gt; rather than prose you have to parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The judge needs its own scepticism
&lt;/h2&gt;

&lt;p&gt;LLM-as-judge is the part people adopt fastest and check least. Three things to know before you trust a number it produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It grades its own family leniently.&lt;/strong&gt; Point the judge at a different model family than the agent under test. It is the cheapest correctness win available and it takes one argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It rewards length.&lt;/strong&gt; Longer answers score higher whether or not they are better. If concision matters to you, say so in the criteria, or you are quietly training your agent toward waffle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An uncalibrated judge is an opinion with a number attached.&lt;/strong&gt; Label fifty examples by hand, run the judge over them, and measure agreement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan evals:calibrate storage/labelled.jsonl &lt;span class="nt"&gt;--criteria&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Correctness"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it disagrees with you 30% of the time, its scores are not data. You will make decisions on those numbers for months, so it is worth an afternoon to find out whether they mean anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually catches regressions
&lt;/h2&gt;

&lt;p&gt;Everything above scores &lt;em&gt;today's&lt;/em&gt; run. On its own that is a report card, and a report card with no previous term is close to useless. You have no idea whether 71% is good.&lt;/p&gt;

&lt;p&gt;The point is comparison. Your first passing run becomes the suite's baseline. Rows are joined across runs by a hash of their content, so the framework knows that "What is your refund policy?" in today's run is the same row as in last month's. When a specific row drops, the build fails and names it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Eval [pest: answers support questions from documented policy] score 61.7%,
pass rate 33.3% across 6 samples.
Gate failed: 2 rows regressed against the reference run (allowed: 0).
  ↓ regressed: "What is your refund policy?" 96.7% → 51.7%
  ↓ regressed: "Can I return it?" 93.3% → 55.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "a test failed". Which inputs got worse, and by how much. That is the output you want in a pull request comment when someone has rewritten a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Pest's own evals plugin fits
&lt;/h2&gt;

&lt;p&gt;Pest ships an evals plugin, and it does the scoring well: &lt;code&gt;toBeCorrect()&lt;/code&gt;, &lt;code&gt;toBeRelevant()&lt;/code&gt;, &lt;code&gt;toBeSafe()&lt;/code&gt;, &lt;code&gt;toSatisfy()&lt;/code&gt;, &lt;code&gt;toHaveToolCalls()&lt;/code&gt;, &lt;code&gt;toFollowTrajectory()&lt;/code&gt;, and &lt;code&gt;repeat()&lt;/code&gt; to sample the same prompt several times. If what you need is to assert an agent is behaving right now, that is genuinely all you need, and you should use it.&lt;/p&gt;

&lt;p&gt;Vizra Evals sits on top of that rather than against it. The difference is what happens after the run finishes. Every sample is persisted, rows are joined across runs by content hash, and a baseline lets the build fail when one specific row drops. Pest scores a run. This keeps them and compares them.&lt;/p&gt;

&lt;p&gt;The two share the same &lt;code&gt;--evals&lt;/code&gt; flag and &lt;code&gt;PEST_EVALS&lt;/code&gt; environment variable, so both can live in one suite without fighting over the flag. &lt;a href="https://docs.vizra.ai/evals/pest" rel="noopener noreferrer"&gt;The docs cover running them together.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So which do you use
&lt;/h2&gt;

&lt;p&gt;Both, for different jobs, on different schedules.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fakes&lt;/th&gt;
&lt;th&gt;Evals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Question answered&lt;/td&gt;
&lt;td&gt;Does my code work?&lt;/td&gt;
&lt;td&gt;Is the answer still good?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;Real tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Milliseconds&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When&lt;/td&gt;
&lt;td&gt;Every commit&lt;/td&gt;
&lt;td&gt;Before a release, nightly, or when a prompt changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catches&lt;/td&gt;
&lt;td&gt;Broken wiring, wrong tool, missing auth&lt;/td&gt;
&lt;td&gt;Quality drift, regressions, cost creep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fakes belong in your normal suite and should run constantly. Evals belong on a slower loop: a nightly job, a pre-release gate, or a CI workflow triggered when anything under &lt;code&gt;app/Agents&lt;/code&gt; or your prompt files changes.&lt;/p&gt;

&lt;p&gt;If you only take one thing from this: &lt;strong&gt;the tests you have prove your plumbing works.&lt;/strong&gt; They were never designed to tell you whether the thing coming out of the pipe is any good, and no amount of adding more of them will change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Pick your most important agent. Write down ten inputs where you know what a good answer looks like. Real ones, from support tickets or logs, not invented ones. That file is your first dataset, and it is genuinely the hardest part of the whole exercise. Everything after it is mechanical.&lt;/p&gt;

&lt;p&gt;If you want to see what the output looks like before installing anything, there is a &lt;a href="https://vizra.ai/demo?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blog-beyond-fakes" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; with real history behind it. The &lt;a href="https://docs.vizra.ai/evals/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; takes about five minutes, and the source is on &lt;a href="https://github.com/vizra-ai/vizra-evals" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why I Built an AI Agent Framework for Laravel (And Why PHP Deserves AI Too) 🚀</title>
      <dc:creator>Aaron Lumsden</dc:creator>
      <pubDate>Thu, 14 Aug 2025 11:05:38 +0000</pubDate>
      <link>https://dev.to/aaronlumsden/why-i-built-an-ai-agent-framework-for-laravel-and-why-php-deserves-ai-too-3il3</link>
      <guid>https://dev.to/aaronlumsden/why-i-built-an-ai-agent-framework-for-laravel-and-why-php-deserves-ai-too-3il3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, August 2026: Vizra ADK is retired.&lt;/strong&gt; &lt;code&gt;v0.0.48&lt;/code&gt; is the final release and there will be no further updates. Laravel shipped an official AI SDK that does this job better than my package did, so I stopped. Use &lt;a href="https://laravel.com/docs/13.x/ai-sdk" rel="noopener noreferrer"&gt;laravel/ai&lt;/a&gt; to build agents. I now work on &lt;a href="https://vizra.ai/blog/i-retired-a-laravel-package-with-38000-installs-heres-what-i-built-instead" rel="noopener noreferrer"&gt;Vizra Evals&lt;/a&gt;, which tests them. It is not a replacement for the ADK: the ADK built agents, Evals tests them. This post is left up as history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For over a decade, I've been building with Laravel. It's been my go-to framework for everything from simple APIs to complex SaaS platforms. But lately, I've felt like a kid with my face pressed against the candy store window, watching the Python developers have all the AI fun.&lt;/p&gt;

&lt;p&gt;Every time I wanted to build an AI agent, I'd have to context-switch to Python. LangChain, AutoGPT, CrewAI – they're all amazing, but they're not PHP. They're not Laravel. And that constant switching between ecosystems was killing my productivity.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable developer would do after one too many late nights wrestling with Python virtual environments... &lt;strong&gt;I built my own AI agent framework for Laravel&lt;/strong&gt;. 😅&lt;/p&gt;

&lt;p&gt;Meet &lt;a href="https://vizra.ai" rel="noopener noreferrer"&gt;&lt;strong&gt;Vizra ADK&lt;/strong&gt;&lt;/a&gt; – the AI Agent Development Kit that brings true agentic capabilities home to Laravel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem That Kept Me Up at Night 🌙
&lt;/h2&gt;

&lt;p&gt;Picture this: You're building a Laravel app that needs an intelligent customer support agent. In the Python world, you'd spin up a separate service, set up API endpoints, manage different dependency managers, deployment pipelines, and pray that your PHP app and Python agent play nicely together.&lt;/p&gt;

&lt;p&gt;But what if your agent could just... &lt;em&gt;be&lt;/em&gt; Laravel? What if it could use Eloquent models directly? Queue jobs? Fire events? What if building an AI agent was as simple as extending a base class?&lt;/p&gt;

&lt;p&gt;That's the itch I needed to scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha!" Moment 💡
&lt;/h2&gt;

&lt;p&gt;The breakthrough came when I realized that AI agents don't need to be complicated. At their core, they're just:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A persona&lt;/strong&gt; (instructions and behavior)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; (things they can do)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; (context and history)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A brain&lt;/strong&gt; (the LLM)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. And Laravel already has amazing patterns for all of these!&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First Agent in Under 5 Minutes ⚡
&lt;/h2&gt;

&lt;p&gt;With Vizra ADK, you can have a working AI agent in literally three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require vizra/vizra-adk
php artisan vizra:install
php artisan vizra:make:agent CustomerSupportAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's look at what an actual agent looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Agents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Vizra\VizraADK\Agents\BaseLlmAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerSupportAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseLlmAgent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer_support'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Helps customers with their inquiries'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'You are a friendly customer support assistant.
        Always be helpful and provide accurate information.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;OrderLookupTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;RefundProcessorTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's a fully functional AI agent. No registration needed – Vizra ADK auto-discovers your agents. You can start using it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CustomerSupportAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'I need help with order #12345'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or chat with it via the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vizra:chat customer_support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vizra also has a bneautiful chat UI built in that you can use to chat with your agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Secret Sauce: Tools That Feel Like Laravel 🛠️
&lt;/h2&gt;

&lt;p&gt;This is where things get really exciting. In Python frameworks, giving your agent access to your database or business logic often means building complex bridges. With Vizra ADK, your tools can directly use Eloquent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Vizra\VizraADK\Contracts\ToolInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Vizra\VizraADK\System\AgentContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderLookupTool&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ToolInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'order_lookup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Look up order information by order ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'parameters'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The order ID to look up'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'required'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;AgentContext&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Just use Eloquent like you always do!&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Order not found'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Your agent now has access to your actual business logic&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shipping'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'customer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your existing Laravel code &lt;strong&gt;IS&lt;/strong&gt; the agent's toolkit. No API layers. No serialization headaches. Just PHP. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory That Persists (Because Context Matters) 🧠
&lt;/h2&gt;

&lt;p&gt;One of the coolest features is how Vizra ADK handles memory and context. Every conversation is automatically persisted, and agents can maintain state across interactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The agent remembers previous conversations&lt;/span&gt;
&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalShoppingAssistantAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Find me a gift for my mom'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'budget'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'occasion'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'birthday'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Later in the conversation...&lt;/span&gt;
&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalShoppingAssistantAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'She loves gardening'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Same session = continued context&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// The agent remembers the budget, occasion, AND now knows about gardening!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AgentContext&lt;/code&gt; class manages all of this elegantly, passing state between your agent and its tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartManagerTool&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ToolInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;AgentContext&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get current cart from context&lt;/span&gt;
        &lt;span class="nv"&gt;$cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
        &lt;span class="nv"&gt;$budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'budget'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Add item to cart&lt;/span&gt;
        &lt;span class="nv"&gt;$cart&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;uniqid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'item_'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'item_name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'item_price'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="c1"&gt;// Update context state&lt;/span&gt;
        &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total_spent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'cart_total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total_spent'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'remaining_budget'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$budget&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total_spent'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Model Support (Because Vendor Lock-in Sucks) 🔄
&lt;/h2&gt;

&lt;p&gt;Thanks to the amazing &lt;a href="https://github.com/prism-php/prism" rel="noopener noreferrer"&gt;Prism PHP&lt;/a&gt; library, Vizra ADK works with all major LLM providers out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use OpenAI&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'gpt-4o'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Or Claude&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'claude-3-opus'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Or Gemini&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'gemini-2.0-flash'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Or local Ollama&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'llama3:latest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch providers with just an environment variable. Your code doesn't change. I learned this lesson the hard way with payment gateways years ago – never again! 😂&lt;/p&gt;

&lt;h2&gt;
  
  
  The Laravel Advantage 🏆
&lt;/h2&gt;

&lt;p&gt;Here's what makes Vizra ADK special for Laravel developers:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Eloquent Integration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Your agents can directly query your database, update models, and leverage all your existing scopes and relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Queue Integration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Long-running agent tasks? Just queue them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;CustomerSupportAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'agents'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;strong&gt;Event System&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Agents fire Laravel events at key points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentExecutionFinished&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Agent completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'duration'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'tokens_used'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tokensUsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;strong&gt;Artisan Commands&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Everything is accessible via Artisan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vizra:agents          &lt;span class="c"&gt;# List all discovered agents&lt;/span&gt;
php artisan vizra:chat agent_name &lt;span class="c"&gt;# Interactive chat&lt;/span&gt;
php artisan vizra:trace abc-123   &lt;span class="c"&gt;# Debug execution traces&lt;/span&gt;
php artisan vizra:dashboard       &lt;span class="c"&gt;# Web UI for testing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. &lt;strong&gt;Evaluation Framework&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Test your agents at scale with the built-in evaluation system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vizra:make:eval CustomerSupportEval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define your test cases in CSV and run automated evaluations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerSupportEvaluation&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseEvaluation&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$csvPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app/Evaluations/data/customer_support_tests.csv'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;evaluateRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$csvRowData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$llmResponse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Validate the agent's response&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertResponseContains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$llmResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'help'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertResponseHasPositiveSentiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$llmResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Or use LLM-as-Judge for subjective evaluation&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$llmResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QualityJudgeAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expectMinimumScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;evaluateRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$csvRowData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$llmResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: Multi-Agent Workflows 🔄
&lt;/h2&gt;

&lt;p&gt;Where Vizra ADK really shines is when you need multiple agents working together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Vizra\VizraADK\Facades\Workflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Sequential workflow - each agent processes results from the previous&lt;/span&gt;
&lt;span class="nv"&gt;$workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Workflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidateOrderAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// Checks inventory, pricing&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentProcessingAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Handles payment&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FulfillmentAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// Creates shipping labels&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotificationAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// Emails customer&lt;/span&gt;

&lt;span class="c1"&gt;// Execute the entire workflow&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$workflow&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orderData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Or use parallel execution for independent tasks&lt;/span&gt;
&lt;span class="nv"&gt;$notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Workflow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;EmailAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SmsAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SlackAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;waitForAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent can access the results from previous agents, creating complex, intelligent workflows that would be a nightmare to orchestrate across language boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Debug Experience That Doesn't Suck 🐛
&lt;/h2&gt;

&lt;p&gt;I spent weeks debugging AI agents with just print statements before building proper tracing into Vizra ADK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vizra:trace abc-123-xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every LLM call with prompts and responses&lt;/li&gt;
&lt;li&gt;Tool executions with inputs and outputs&lt;/li&gt;
&lt;li&gt;Token usage and costs&lt;/li&gt;
&lt;li&gt;Execution timing for performance optimization&lt;/li&gt;
&lt;li&gt;Memory updates and context changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or use the built-in web dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vizra:dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you get a beautiful Livewire-powered interface where you can chat with your agents, see real-time traces, and monitor performance. Finding bottlenecks went from 2 hours to 2 minutes. 🎯&lt;/p&gt;

&lt;h2&gt;
  
  
  Early Success &amp;amp; Community Response 💙
&lt;/h2&gt;

&lt;p&gt;Since launching Vizra ADK:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;65 downloads&lt;/strong&gt; in the first week&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;49 stars&lt;/strong&gt; on GitHub&lt;/li&gt;
&lt;li&gt;Amazing feedback from the Laravel community&lt;/li&gt;
&lt;li&gt;Several production deployments already!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what excites me most is seeing what people are building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A legal document analyzer that uses existing Laravel permissions&lt;/li&gt;
&lt;li&gt;A code review bot that integrates with existing CI/CD pipelines
&lt;/li&gt;
&lt;li&gt;An intelligent inventory manager that predicts stock needs&lt;/li&gt;
&lt;li&gt;A customer service bot that actually understands context&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next? 🚀
&lt;/h2&gt;

&lt;p&gt;I'm actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud platform&lt;/strong&gt; for managed evaluations and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Join Me on This Journey 🤝
&lt;/h2&gt;

&lt;p&gt;Look, I get it. "Another framework" might make you roll your eyes. But if you're a Laravel developer who's been watching the AI revolution from the sidelines, or struggling with Python integration, give Vizra ADK a try.&lt;/p&gt;

&lt;p&gt;It's MIT licensed, fully open source, and built by someone who's been in the Laravel trenches for over a decade. I built this because I needed it, and I'm betting you do too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require vizra/vizra-adk
php artisan vizra:install
&lt;span class="c"&gt;# Join me on this journey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/vizra-ai/vizra-adk" rel="noopener noreferrer"&gt;https://github.com/vizra-ai/vizra-adk&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: &lt;a href="https://vizra.ai/docs" rel="noopener noreferrer"&gt;https://vizra.ai/docs&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;X&lt;/strong&gt;: &lt;a href="https://x.com/aaronlumsden" rel="noopener noreferrer"&gt;@aaronlumsden&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's show the world that PHP deserves a seat at the AI table. Because Laravel developers shouldn't have to learn Python just to build intelligent applications. &lt;/p&gt;

&lt;p&gt;We already have the best web framework. Now we have AI agents to match. 🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What are you building with AI? Have you felt the PHP/Python divide? I'd love to hear your thoughts in the comments!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Video courses to improve your web development skills</title>
      <dc:creator>Aaron Lumsden</dc:creator>
      <pubDate>Sat, 27 Apr 2019 09:58:41 +0000</pubDate>
      <link>https://dev.to/aarondo/video-courses-to-improve-your-web-development-skills-52i4</link>
      <guid>https://dev.to/aarondo/video-courses-to-improve-your-web-development-skills-52i4</guid>
      <description>&lt;p&gt;Hey there I've created a new website where any one can  learn web development for free. I'd love to hear your thoughts &amp;amp; feedback on it.&lt;/p&gt;

&lt;p&gt;Courses include Laravel, Vue.js, JavaScript, React.js, Angular.js, Node.js, Python and PHP. Let me know what you think? 👍&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codevideos.co/"&gt;https://codevideos.co/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>vue</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
