<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Luis Güette: Prepare For Production]]></title><description><![CDATA[Here is where I keep track of all my experiences with software development.]]></description><link>https://quietmastery.me/s/prepare-for-production</link><image><url>https://substackcdn.com/image/fetch/$s_!HKmQ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9eb801a4-70b9-4de4-8eb4-98eaa0240695_430x430.png</url><title>Luis Güette: Prepare For Production</title><link>https://quietmastery.me/s/prepare-for-production</link></image><generator>Substack</generator><lastBuildDate>Sat, 11 Apr 2026 05:18:03 GMT</lastBuildDate><atom:link href="https://quietmastery.me/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Luis Güette]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[luisguette@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[luisguette@substack.com]]></itunes:email><itunes:name><![CDATA[Luis Güette]]></itunes:name></itunes:owner><itunes:author><![CDATA[Luis Güette]]></itunes:author><googleplay:owner><![CDATA[luisguette@substack.com]]></googleplay:owner><googleplay:email><![CDATA[luisguette@substack.com]]></googleplay:email><googleplay:author><![CDATA[Luis Güette]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Automatically Inject Controllers in Laravel]]></title><description><![CDATA[Let's say you have a route, and depending on certain conditions, the page changes drastically.]]></description><link>https://quietmastery.me/p/automatically-inject-controllers</link><guid isPermaLink="false">https://quietmastery.me/p/automatically-inject-controllers</guid><dc:creator><![CDATA[Luis Güette]]></dc:creator><pubDate>Tue, 23 Sep 2025 13:02:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HKmQ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9eb801a4-70b9-4de4-8eb4-98eaa0240695_430x430.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's say you have a route, and depending on certain conditions, the page changes drastically. We typically create multiple routes, one for each case, allowing us to split responsibilities. But what if you really want to keep everything under the same route?</p><p>The first solution that comes to mind is to have a bunch of `if/else` sentences in the controller, and based on that, we do different things, render different views, etc. Or maybe create some services that are dynamically injected into the controller based on certain conditions. But this doesn't sound like the Laravel way.</p><p>I always thought that controllers are kind of static entities; we just load them in the route as an entry point, and that's it. But what if I told you we could load them dynamically based on certain conditions, while maintaining the same route?</p><p>This is what we are going to talk about in this article.</p><h1>Let's bind controllers dynamically.</h1><p>It occurred to me that maybe we can set a contract instead of the controller in the route, something like this:</p><pre><code>Route::get('this-route/{resource}', ThisControllerContract::class)-&gt;name('this.route');</code></pre><p>It turns out this is possible! We need to do a couple of things to make it work. Let me explain it with an example from my previous article, "<a href="https://quietmastery.me/p/how-to-extend-mustverifyemail">How to Extend MustVerifyEmail.</a>".</p><p>In that article, I extended the email verification feature to add my own phone verification. For the verification page, I wanted to keep the same route, but change the way I handle it depending on whether your email or phone number is not verified.</p><p>So, I created a new interface and used it on the route like this:</p><pre><code> Route::get('verify', UserVerificationPromptControllerContract::class)
        -&gt;name('verification.notice');</code></pre><p>And I defined the interface like this:</p><pre><code>interface UserVerificationPromptController
{
    public function __invoke(): mixed;
}</code></pre><p>In this case, I don't want to bind any route item to a model, so I don't add any parameter to the `__invoke` method. But, if you have any route model binding in your case, you need to add them as parameters; this is only the case for route model binding.</p><p>Then, in a service provider, we bind the controllers like this:</p><pre><code>public function register(): void
{
  $this-&gt;app-&gt;bind(UserVerificationPromptController::class, function (Application $app) {
      /** @var ?User $user */
      $user = request()-&gt;user();

      if ($user &amp;&amp; ! $user-&gt;hasVerifiedPhoneNumber()) {
          return $this-&gt;app
                      -&gt;make(PhoneVerificationPromptController::class);
      }

      return $this-&gt;app-&gt;make(EmailVerificationPromptController::class);
  });
}</code></pre><p>See how I can get the request data, in this case the user, to define what controller I want to inject into the route.</p><p>And it's that simple! Now you can have multiple controllers that handle the same route. And I just have to say... <strong>This is the way.</strong></p>]]></content:encoded></item><item><title><![CDATA[How to Extend MustVerifyEmail ]]></title><description><![CDATA[Creating MustVerifyPhoneNumber in Laravel]]></description><link>https://quietmastery.me/p/how-to-extend-mustverifyemail</link><guid isPermaLink="false">https://quietmastery.me/p/how-to-extend-mustverifyemail</guid><dc:creator><![CDATA[Luis Güette]]></dc:creator><pubDate>Mon, 22 Sep 2025 12:03:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uTKk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uTKk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uTKk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 424w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 848w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 1272w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uTKk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png" width="677" height="613.499243570348" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:661,&quot;resizeWidth&quot;:677,&quot;bytes&quot;:70931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quietmastery.me/i/174120271?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uTKk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 424w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 848w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 1272w, https://substackcdn.com/image/fetch/$s_!uTKk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc45e3edc-afc9-456f-8a9f-a0a1bdec3064_661x599.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Every time I start a new project in Laravel with a starter kit, I enable the `MustVerifyEmail` feature. And I always take for granted that it works. The Laravel team does a great job making sure it works. But, recently, I asked myself, how does it actually work? That's what I'm going to explain in this article.</p><p>I'm going to explain to you by doing my own `MustVerifyPhoneNumber` feature. Let's get started.</p><p>The first thing is to create an interface called `MustVerifyPhone`. It is pretty similar to `MustVerifyEmail`. Below, you'll find the content of it:</p><pre><code>interface MustVerifyPhoneNumber

{

    /**

     * Determine if the user has verified their phone number.

     */

    public function hasVerifiedPhoneNumber(): bool;

    /**

     * Mark the given user's phone number as verified.

     */

    public function markPhoneNumberAsVerified(): bool;

    /**

     * Send the phone number verification notification.

     */

    public function sendPhoneNumberVerificationNotification(): void;

    /**

     * Get the phone number that should be used for verification.

     */

    public function getPhoneNumberForVerification(): string;
}</code></pre><p>Now, we need to implement this interface in your model, in this case, the `User` model:</p><pre><code>class User extends Authenticatable implements MustVerifyEmail, MustVerifyPhoneNumber

{

&#9;...

}</code></pre><p>`MustVerifyEmail` actually implements the required methods in a trait. But wait, no trait implements the interface in the `User` model, right? Well, the trait is on the `Authenticatable` base class. If you go to the base class, you'll see a `MustVerifyEmail` trait. Let's do something similar.</p><p>I created a `MustVerifyPhoneNumber` trait with the following content:</p><pre><code>trait MustVerifyPhoneNumber

{

    public function hasVerifiedPhoneNumber(): bool

    {

        return ! is_null($this-&gt;phone_verified_at);

    }

    public function markPhoneNumberAsVerified(): bool

    {

        return $this-&gt;forceFill([

            'phone_verified_at' =&gt; $this-&gt;freshTimestamp(),

        ])-&gt;save();

    }

    public function sendPhoneNumberVerificationNotification(): void

    {

        $this-&gt;notify(new VerifyPhoneNumberNotification);

    }

    public function getPhoneNumberForVerification(): string

    {

        return $this-&gt;phone_number;

    }

}</code></pre><p>As you can see, there are two new columns we need to add to the `users` table: `phone_number` and `phone_verified_at`. We also need to create `VerifyPhoneNumberNotification`. Here is an example of the notification. Of course, you can create your own version:</p><pre><code>class VerifyPhoneNumberNotification extends Notification implements ShouldQueue

{

    use Queueable;

    public function __construct() {}

    /**

     * @return string[]

     */

    public function via(User $notifiable): array

    {

        return [WhatsappChannel::class];

    }

    public function toWhatsapp(User $notifiable): TemplateMessage

    {

        $url = $this-&gt;verificationUrl($notifiable);

        return (new TemplateMessage)

            -&gt;name('verify_phone_number_v1')

            -&gt;addUrlButton($url);

    }

    protected function verificationUrl(User $notifiable): string

    {

        return URL::temporarySignedRoute(

            'verification.verify_phone',

            Carbon::now()-&gt;addMinutes(Config::get('auth.verification.expire', 60)),

            [

                'id' =&gt; $notifiable-&gt;getKey(),

                'hash' =&gt; sha1($notifiable-&gt;getPhoneNumberForVerification()),

            ]

        );

    }

}</code></pre><p>You can create your own custom channel to send the notification if you want. However, the part I want you to examine is the `verificationUrl` method. See how we generate a temporary signed route that we are going to send to the user. I took this from the `Illuminate\Auth\Notifications\VerifyEmail` notification. You can send the user to a controller to verify the phone number and finally redirect to your dashboard.</p><p>The next step is to override the `verified` middleware. This middleware ensures that the user is redirected to the verification page if the user is not verified. Let's create our own version of the middleware.</p><pre><code>class EnsureUserIsValidatedMiddleware

{

    /**

     * Handle an incoming request.

     */

    public function handle(Request $request, Closure $next, ?string $redirectToRoute = null): Response|RedirectResponse

    {

        if (

&#9;        $this-&gt;emailNotVerified(request()-&gt;user()) 

&#9;        || $this-&gt;phoneNumberNotVerified(request()-&gt;user())

        ) {

            return Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));

        }

        return $next($request);

    }

    private function emailNotVerified(?User $user): bool

    {

        return ! $user?-&gt;hasVerifiedEmail();

    }

    private function phoneNumberNotVerified(?User $user): bool

    {

        return ! $user?-&gt;hasVerifiedPhoneNumber();

    }

}</code></pre><p>In my case, I verify that both the email and phone number are verified; if not, we redirect to the `verification.notice` route. You can find this route in the `auth.php` routes file. Also, you can check the default `verified` middleware in `Illuminate\Auth\Middleware\EnsureEmailIsVerified`.</p><p>Finally, we just need to override the `verified` middleware in `bootstrap/app.php` like this:</p><pre><code>return Application::configure(basePath: dirname(__DIR__))

    -&gt;withMiddleware(function (Middleware $middleware) {

        $middleware-&gt;alias([

            'verified' =&gt; EnsureUserIsValidatedMiddleware::class,

        ]);

&#9;&#9;...

    })-&gt;create();</code></pre><p>And this is how you can implement your own verification feature based on the `MustVerifyEmail` feature from Laravel.</p>]]></content:encoded></item><item><title><![CDATA[The Difference Between "collect" and "wrap" in Laravel Collections]]></title><description><![CDATA[Today, I learned that Laravel collections have a &#8220;wrap&#8221; method, which performs a similar function to the &#8220;collect&#8221; method.]]></description><link>https://quietmastery.me/p/the-difference-between-collect-and</link><guid isPermaLink="false">https://quietmastery.me/p/the-difference-between-collect-and</guid><dc:creator><![CDATA[Luis Güette]]></dc:creator><pubDate>Tue, 16 Sep 2025 13:02:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Jz0G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jz0G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jz0G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 424w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 848w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jz0G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png" width="1400" height="1400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1400,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:234056,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quietmastery.me/i/173703087?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Jz0G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 424w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 848w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!Jz0G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d2cec64-1dfe-4674-a1ab-6a67c8bf8dcf_1400x1400.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Today, I learned that Laravel collections have a &#8220;wrap&#8221; method, which performs a similar function to the &#8220;collect&#8221; method. So, let me explain what the key differences are.</p><h1>collect method</h1><p>The &#8220;collect()&#8221; function creates an instance of a Collection from the value provided. If the input is already a collection, it simply returns the collection untouched.</p><pre><code>php

$collection = collect([1, 2, 3]); // Returns Collection([1, 2, 3])

$existing = collect([4, 5, 6]);
$collection = collect($existing); // Returns original Collection([4, 5, 6])</code></pre><h1>wrap Method</h1><p>The &#8220;Collection::wrap()&#8221; method always guarantees a Collection output, regardless of the input type or existing structure. If the input is not an array or a Collection, it wraps it in an array before creating a Collection.</p><pre><code>php

Collection::wrap('Alice'); // Returns Collection(['Alice'])
Collection::wrap(['Alice', 'Bob']); // Returns Collection(['Alice', 'Bob'])
Collection::wrap(collect(['Alice', 'Bob'])); // Returns Collection(['Alice', 'Bob'])</code></pre><p>There is now a helper function for &#8220;Collection::wrap&#8221;. Should we have one?</p><p></p>]]></content:encoded></item><item><title><![CDATA[Create Agentic Apps With NodeGraph And Laravel]]></title><description><![CDATA[In my last article, I shared with you this idea about how to build a Laravel application with agents.]]></description><link>https://quietmastery.me/p/create-agentic-apps-with-nodegraph</link><guid isPermaLink="false">https://quietmastery.me/p/create-agentic-apps-with-nodegraph</guid><dc:creator><![CDATA[Luis Güette]]></dc:creator><pubDate>Thu, 11 Sep 2025 23:35:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8e340ea5-5aad-4a58-ae6b-fd8af77032fe_1820x1822.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my last article, I shared with you this idea about  how to build a Laravel application with agents. </p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;63e0c253-9a28-4c18-9135-f34ad99a9254&quot;,&quot;caption&quot;:&quot;I began thinking about creating an app with Laravel that allows users to interact primarily with AI agents. I&#8217;ve been exploring various design ideas, and recently, an interesting concept has come to mind. Let me share it with you.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;The new method of building apps with AI agents&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:42351071,&quot;name&quot;:&quot;Luis G&#252;ette&quot;,&quot;bio&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9eb801a4-70b9-4de4-8eb4-98eaa0240695_430x430.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-09-07T22:53:14.023Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!h-Sv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://substack.com/home/post/p-173035617&quot;,&quot;section_name&quot;:&quot;Prepare For Production&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:173035617,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Luis G&#252;ette&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HKmQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9eb801a4-70b9-4de4-8eb4-98eaa0240695_430x430.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>However, I then had the idea of creating a <a href="https://laravel.com/">Laravel</a> package to make it easier. So, I created <a href="https://github.com/taecontrol/nodegraph">NodeGraph</a>.</p><p>NodeGraph is a compact state-graph runtime designed for Laravel, making it easy to create controlled flows. In these flows, nodes can be AI agents or custom code that executes when the flow reaches a particular state, much like <a href="https://www.langchain.com/langgraph">LangGraph</a>. </p><p>You would ask, Why don't you just use LangGraph? And the reason is that they built it for Python and JavaScript, and I really like to work with PHP and Laravel.</p><p>Also, the key difference is that <a href="https://github.com/taecontrol/nodegraph">NodeGraph</a> is fully integrated with Laravel. We store the State and Checkpoints in the database, which allows us to have more flexibility on when and how to run our flows, while keeping track of everything that's happening. This package can be helpful when building complex state machines, and we need to keep track of what&#8217;s happening on each run.</p><h1>How NodeGraph Works</h1><p>The first thing you need to create is a State, which is a PHP enum that holds all the possible states:</p><pre><code>use Taecontrol\NodeGraph\Contracts\HasNode;

enum OrderState: string implements HasNode
{
    case Start = 'start';
    case Charge = 'charge';
    case Done = 'done';

    public function node(): string
    {
        return match ($this) {
            self::Start =&gt; \App\Nodes\StartNode::class,
            self::Charge =&gt; \App\Nodes\ChargeNode::class,
            self::Done =&gt; \App\Nodes\DoneNode::class,
        };
    }
}</code></pre><p>Notice that there is a <code>node()</code> method where you map each State to a Node. The Nodes look like this:</p><pre><code>namespace App\Nodes;

use App\Decisions\SimpleDecision;
use App\Enums\OrderState;
use App\Events\OrderEvent; // extends Taecontrol\NodeGraph\Event
use Taecontrol\NodeGraph\Node;

class StartNode extends Node
{
    public function handle($context): SimpleDecision
    {
        $d = new SimpleDecision(OrderState::Charge);
        $d-&gt;addMetadata('from', 'start');
        $d-&gt;addEvent(new OrderEvent('start'));
        return $d;
    }
}

class ChargeNode extends Node
{
    public function handle($context): SimpleDecision
    {
        // ... charge logic ...
        $d = new SimpleDecision(OrderState::Done);
        $d-&gt;addMetadata('from', 'charge');
        $d-&gt;addEvent(new OrderEvent('charged'));
        return $d;
    }
}

class DoneNode extends Node
{
    public function handle($context): SimpleDecision
    {
        $d = new SimpleDecision(null); // stay in terminal state
        $d-&gt;addMetadata('from', 'done');
        $d-&gt;addEvent(new OrderEvent('done'));
        return $d;
    }
}</code></pre><p>Each Node has a centralized Context:</p><pre><code>use Taecontrol\NodeGraph\Context;
use Taecontrol\NodeGraph\Models\Thread;

class OrderContext extends Context
{
    public function __construct(protected Thread $thread) {}

    public function thread(): Thread
    {
        return $this-&gt;thread;
    }
}</code></pre><p>We can use Context to pass whatever we need to our Nodes. But, it is required that you pass a Thread as a parameter. Nodes return a Decision:</p><pre><code>namespace App\Decisions;

use Taecontrol\NodeGraph\Decision;

class SimpleDecision extends Decision {}</code></pre><p>As you saw in the Node definitions&#8217; code, you can add events, set a new state, or update some metadata in a Decision. When the flow runs in that Node, the Thread's current State is updated to what you specified, the events are dispatched, metadata is stored in the Thread, and a new checkpoint is created. </p><p>The next step is to put everything together in a Graph.</p><pre><code>use Taecontrol\NodeGraph\Graph;
use App\Enums\OrderState;

class OrderGraph extends Graph
{
    public function define(): void
    {
        $this-&gt;addEdge(OrderState::Start, OrderState::Charge);
        $this-&gt;addEdge(OrderState::Charge, OrderState::Done);
        // Done has no outgoing edges, so it's terminal
    }

    public function initialState(): OrderState
    {
        return OrderState::Start;
    }
}</code></pre><p>Finally, you can run it like this:</p><pre><code>use Taecontrol\NodeGraph\Models\Thread;

$thread = Thread::create([
    'threadable_type' =&gt; \App\Models\Order::class, // anything morphable
    'threadable_id' =&gt; (string) \Illuminate\Support\Str::ulid(),
    'metadata' =&gt; [],
]);

$context = new \App\Contexts\OrderContext($thread);
$graph = app(\App\Graphs\OrderGraph::class);

$graph-&gt;run($context); // Start -&gt; Charge
$graph-&gt;run($context); // Charge -&gt; Done
$graph-&gt;run($context); // Done is terminal; finished_at will be set on this run</code></pre><p>See how we first create a Thread, which is a model with a polymorphic relation to the model we want to connect it to. We make a Context and pass the Thread as a parameter. Then, we instantiate the Graph. Finally, we run the Graph.</p><p>And it&#8217;s that simple. The package is pretty much in beta for now, and you can get more information about how to install and use it in the&nbsp;<a href="https://github.com/taecontrol/nodegraph">NodeGraph repository.</a>&nbsp;Let me know what you think!</p>]]></content:encoded></item><item><title><![CDATA[The new method of building apps with AI agents]]></title><description><![CDATA[I began thinking about creating an app with Laravel that allows users to interact primarily with AI agents.]]></description><link>https://quietmastery.me/p/the-new-method-of-building-apps-with</link><guid isPermaLink="false">https://quietmastery.me/p/the-new-method-of-building-apps-with</guid><dc:creator><![CDATA[Luis Güette]]></dc:creator><pubDate>Sun, 07 Sep 2025 22:53:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!h-Sv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I began thinking about creating an app with Laravel that allows users to interact primarily with AI agents. I&#8217;ve been exploring various design ideas, and recently, an interesting concept has come to mind. Let me share it with you.</p><h1>Paga F&#225;cil</h1><p>The idea is that users can make payments in Venezuela without ever leaving the WhatsApp app. Compared to the current method, it should be easier and faster. </p><p>The user should simply write in natural language that they want to make a new payment, and the service will guide them through the payment step by step without leaving WhatsApp. </p><p>Under the hood, we use <a href="https://cobrafacil.app">Cobra F&#225;cil</a> API, which allows us to make transfers from one account to another.</p><p>We use passwordless login; every time users log in to the platform, they receive a 6-digit code via email. Users must be authenticated on the platform to perform actions such as creating a payment method or updating their profile. </p><p>For now, we only support mobile payment, which is the allowed option by <a href="https://cobrafacil.app">Cobra F&#225;cil</a>.</p><p>So, let&#8217;s start designing this app.</p><h1>General Architecture</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!h-Sv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!h-Sv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 424w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 848w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 1272w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!h-Sv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png" width="1456" height="920" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:920,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:113567,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://luisguette.substack.com/i/173035617?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!h-Sv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 424w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 848w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 1272w, https://substackcdn.com/image/fetch/$s_!h-Sv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31cdee9-8b47-4b96-be1f-868118758d92_1520x960.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>From the graph, this is the basic flow:</p><ol><li><p>Users send a message to WhatsApp, and WhatsApp sends that message to our Laravel app via a webhook call.</p></li><li><p>We process the inbound message and send it out to a &#8220;Routing System&#8221;. The routing system defines to which &#8220;Agent&#8221; the message is sent.</p></li><li><p>The &#8220;Agent&#8221; processes the message; it may interact with other agents in the process.  Then, it sends a response back to the WhatsApp service and the users.</p></li></ol><p>We need a way to store the application&#8217;s state. For this, we are going to create a &#8220;conversations&#8221; and &#8220;conversation_messages&#8221; tables. Let&#8217;s see the data architecture now.</p><h1>Data Architecture</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!y52C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!y52C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 424w, https://substackcdn.com/image/fetch/$s_!y52C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 848w, https://substackcdn.com/image/fetch/$s_!y52C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 1272w, https://substackcdn.com/image/fetch/$s_!y52C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!y52C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png" width="458" height="458" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:968,&quot;width&quot;:968,&quot;resizeWidth&quot;:458,&quot;bytes&quot;:66376,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://luisguette.substack.com/i/173035617?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!y52C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 424w, https://substackcdn.com/image/fetch/$s_!y52C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 848w, https://substackcdn.com/image/fetch/$s_!y52C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 1272w, https://substackcdn.com/image/fetch/$s_!y52C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa67980c0-fa3f-40c2-bcec-d6c9fb3b5ae1_968x968.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol><li><p><strong>User: </strong>This is the basic user, defined by Laravel.</p></li><li><p><strong>Conversation: </strong>The conversation model has</p><ul><li><p><strong>User ID</strong>: Because it <strong>belongs to</strong> a user.</p></li><li><p><strong>State</strong>: This is the current state of the conversation. A determined agent handles the conversation based on this state.</p></li><li><p><strong>Is Active</strong>: To know if the conversation is active. Given we have only one chat, there should be only one active conversation.</p></li><li><p><strong>Expires At</strong>: If the conversation has been active for more than a particular time, we should expire it.</p></li><li><p><strong>Metadata</strong>: This column stores all relevant data in JSON format.</p></li></ul></li><li><p><strong>Conversation Message:</strong> This model has</p><ul><li><p><strong>Conversation ID</strong>: Because it <strong>belongs to</strong> a conversation.</p></li><li><p><strong>Role</strong>: This is to define whether it is a message from the user or the assistant.</p></li><li><p><strong>Direction:</strong> This defines whether the message is inbound or outbound.</p></li><li><p><strong>Content:</strong> Content of the message.</p></li></ul></li><li><p><strong>Payment Method:</strong> This model specifies the bank account from which the user transfers the money.</p></li></ol><p>Now, let&#8217;s take a closer look at the conversation&#8217;s flow.</p><h1>Conversation Flow</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eEZ2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eEZ2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 424w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 848w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 1272w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eEZ2!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png" width="1200" height="1200" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:113566,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://luisguette.substack.com/i/173035617?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eEZ2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 424w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 848w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 1272w, https://substackcdn.com/image/fetch/$s_!eEZ2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85e1e689-521a-4ab6-a38a-4c8b984a80c9_1520x1520.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>When users start a new conversation, we direct them to&nbsp;<strong>Intent Detection,</strong>&nbsp;and the&nbsp;<strong>Intent Detection Agent&nbsp;</strong>manages this state.</p><h2>Intent Detection Agent</h2><p>This agent ensures we understand the user&#8217;s intent, and based on that, we route them to either <strong>Payment Data Collection</strong> or <strong>Customer Service</strong> state.</p><p>Here, we get the inbound message, and we pass it to an LLM to decipher the user intent. When the LLM knows what the user wants, it calls a function to transition to one of the&nbsp;<strong>allowed</strong>&nbsp;states mentioned earlier.</p><p>How does the LLM know what the allowed states are?</p><p>We create the <strong>StateGraph</strong> class to define all relations between the states. We call those &#8220;edges&#8221;. In this class, we can determine things like what the initial state of a conversation is, what state is terminal, what the neighbors of a specific state are, and if a state can transition to another state.</p><p>In this case, we pass the neighbor states Intent Detection state (Payment Data Collection and Customer Service).</p><p>If one of the intents is detected, we transition to the corresponding state. Since we want the next agent to respond to the user, we call the next agent directly.</p><p>Otherwise, the LLM will continue asking questions to understand the user&#8217;s intent.</p><p>Let&#8217;s say the user wants to make a payment. We&#8217;ll call the <strong>Payment Data Collection Agent</strong> then.</p><h2>Payment Data Collection Agent</h2><p>This agent gathers all the necessary information to make a payment. It asks questions like What amount is, Recipient name, Recipient phone number, Document ID, among other things.</p><p>Once it has all the information, it calls a function to store it in the conversation metadata and calls the next agent.</p><h2>OTP Capture Agent</h2><p>This agent retrieves the data from the previous agent and makes the first API call to&nbsp;<a href="https://cobrafacil.app">Cobra F&#225;cil</a>, allowing the user&#8217;s bank to send a verification code to the user, ensuring they actually want to initiate a transfer from their bank account. Additionally, it sends a message to the user requesting that they provide this code. Then, we update the conversation state, but we <strong>don&#8217;t call the next agent</strong>. We need to wait for the user to give the OTP code so we can call the next agent.</p><h2>Payment Execution Agent</h2><p>With the OTP code and the payment information, this agent makes a second API call to <a href="https://cobrafacil.app">Cobra F&#225;cil</a> to execute the payment. We update the conversation state and tell the user that the payment is in progress.</p><h2>Payment Status Monitoring</h2><p>We don&#8217;t handle this state like the others. In this case, we monitor the pending payment transaction from <a href="https://cobrafacil.app">Cobra F&#225;cil</a> in a Laravel command. Once the transaction is processed, we update the conversation state and send back a message to the user with the successful payment details. Hence, the last two states are &#8216;Payment Failed&#8217; and &#8216;Payment Success&#8217;.</p><h2>Customer Service Agent</h2><p>On the other hand, if the user asks questions about the service, this agent handles them. It simply responds to any questions the user has about the service.</p><p>The conversation automatically expires once the user completes a payment or a specified amount of time has passed without any interaction with the user.</p><p>And that&#8217;s it! As you can see, we can handle flows by designing a &#8220;state machine&#8221; where each node is an agent. This approach allows us to create highly flexible conversation flows.</p><p>Should I create a package to make these types of applications easier to build?</p><div class="poll-embed" data-attrs="{&quot;id&quot;:372420}" data-component-name="PollToDOM"></div><p></p><p></p><p></p>]]></content:encoded></item></channel></rss>