Over the previous few weeks I’ve posted a few instances about There There, the helpdesk we’re constructing at Spatie. After 20 years of operating our personal buyer assist, we determined to cease compromising on instruments and construct the one we really need to use.
Numerous the enjoyable is on the AI facet. We’re not utilizing it to exchange assist brokers, we’re utilizing it to make them quicker. The human reads the ticket, thinks, and clicks a button. The mannequin pulls up related historical past, drafts a reply, tags the ticket, closes a reproduction. There There may be in personal beta proper now, and you may apply for early entry at there-there.app.
The entire AI layer is constructed on the brand new Laravel AI SDK. It has been a pleasure to work with, and I might prefer to stroll by means of how we use it.
Utilizing the Laravel AI SDK
Every little thing begins with an agent class. Ours is known as ThereThereAgent. It declares which mannequin to make use of, what the system immediate seems like, which instruments the mannequin can name, and what dialog historical past to replay.
This is a trimmed model.
use LaravelAiAttributesMaxSteps; use LaravelAiAttributesTemperature; use LaravelAiContractsAgent; use LaravelAiContractsConversational; use LaravelAiContractsHasTools; use LaravelAiPromptable; #[Temperature(0.3)] #[MaxSteps(10)] class ThereThereAgent implements Agent, Conversational, HasTools { use Promptable; public perform __construct( protected Workspace $workspace, protected Consumer $consumer, protected ?Ticket $contextTicket = null, protected array $conversationHistory = [], ) {} public perform mannequin(): string { return config('ai-pricing.default.identify'); } public perform directions(): string { return view('prompts.ai.there-there-agent-system', [ 'workspace' => $this->workspace, 'user' => $this->user, 'contextTicket' => $this->contextTicket, ])->render(); } public perform instruments(): iterable { return [ new ChangeTicketStatusTool($this->workspace), new AssignTicketTool($this->workspace), new AddNoteToTicketTool($this->workspace), new ManageTicketTagsTool($this->workspace), ]; } }
Just a few issues I like right here. The #[Temperature] and #[MaxSteps] attributes are declarative. No wiring by means of constructor parameters, no config file. The directions() technique renders a Blade view, which suggests our system immediate is an everyday template with contains and partials. And instruments() returns an iterable, so we will flip instruments on and off relying on context.
Instruments are the place it will get enjoyable. Each is a category the mannequin can resolve to name, with a typed JSON schema so arguments are validated earlier than they hit our code. This is the device that adjustments a ticket’s standing.
class ChangeTicketStatusTool implements Instrument { use FindsTicketsInWorkspace; public perform __construct(protected Workspace $workspace) {} public perform description(): string { return 'Change the standing of a ticket. Use this when the agent asks to shut, reopen, or mark a ticket as spam.'; } public perform schema(JsonSchema $schema): array { return [ 'ticket_subject' => $schema->string()->description('The subject, title, or ULID of the ticket.')->required(), 'contact_email' => $schema->string()->description('The contact email on the ticket.')->required(), 'new_status' => $schema->string()->description('The new status: open, closed, or spam.')->required(), ]; } public perform deal with(Request $request): string { $standing = TicketStatus::tryFrom($request['new_status'] ?? ''); if (! $standing) { return 'Invalid standing. Have to be one in all: open, closed, spam.'; } $ticket = $this->findTicket($request['ticket_subject'], $request['contact_email']); if (! $ticket) { return 'Couldn't discover a matching ticket.'; } app(ChangeTicketStatusAction::class)->execute($ticket, $standing); return json_encode([ 'result' => 'success', 'ticket' => $ticket->displayTitle(), 'new_status' => $status->label(), ]); } }
description() is what the mannequin reads when deciding whether or not to name this device. schema() returns the typed JSON schema the mannequin has to match. deal with() runs the device and returns a string that goes again into the mannequin’s context. We return JSON on success so the mannequin has structured information to check with when it writes its ultimate reply.
Discover we do not do heavy lifting contained in the device. The precise work is delegated to ChangeTicketStatusAction, an everyday area motion. The device is simply an AI-facing adapter over code we already had.
Calling the agent from an motion is a few strains.
$agent = new ThereThereAgent( workspace: $workspace, consumer: $consumer, contextTicket: $agentChat->contextTicket, conversationHistory: $agentChat->getConversationHistoryExcludingLatest(), ); return $this->streamingService->stream($agent, $messageForAi, $agentChat);
We hand the agent off to our streaming service, which turns the SDK’s response stream into occasions the frontend can eat. That is one other put up.
TODO: quick video of the Ask There There agent choosing up a message, calling a device like altering a ticket standing, and replying.
In closing
The Laravel AI SDK has hit a candy spot for our use case. The agent and power abstractions are easy, the Blade-backed immediate is a pleasure to edit, and the typed schema means the mannequin hardly ever mangles its arguments. We have barely scratched the floor of what it might do.
You’ll be able to learn extra in the Laravel AI SDK repo. And if you would like to attempt There There your self, we’re in personal beta proper now and you may apply for early entry at there-there.app.

