m-tech-stack/laravel-ai-engine
Laravel AI Engine provides a clean, configurable layer for adding AI features to Laravel apps, with a unified API for providers, prompt and chat workflows, embeddings, and tool calling—keeping AI logic testable and maintainable.
1. Install and publish.
composer require m-tech-stack/laravel-ai-engine
php artisan vendor:publish --tag=ai-engine-config # then check the actual tag list with `php artisan vendor:publish`
Two config files matter day-to-day: config/ai-engine.php (engines, keys, failover, credits, data_query.models, file analysis) and config/ai-agent.php (tools, resources, sub-agents, ai_native tool selection, memory). Read those two files before anything else — they are the real API surface.
2. Prove the wiring with ai:doctor before writing a line of code.
php artisan ai:doctor
This reports per-engine key health. Almost every "it doesn't work" issue in the first hour is an unset key, a keyless engine being silently skipped by failover, or a wrong default engine in ai-engine.default.
3. First use case: a single tool, not an agent. The smallest useful loop is one tool you register and one generate() call. Extend SimpleAgentTool, declare $name / $description / $parameters, implement handle(), then register in config/ai-agent.php:
'tools' => ['lookup_order' => App\AI\Tools\LookupOrderTool::class],
If you only need CRUD over a model, skip the tool entirely and use AiResource:
AiResource::for(Invoice::class)
->search(['number', 'status'])
->writable(['number', 'customer_id'])
->identity(['number'])
->register();
That generates find_ / create_ / show_ tools for you. It is the fastest path to a working assistant.
4. Where to look next, in order: config/ai-engine.php → config/ai-agent.php → docs/cookbook.mdx (the complete, source-grounded guide) → the actual class in src/. The skill map is a map, not a contract; the class is the contract.
Engine calls are one-shot and stateless. For anything that isn't agent-driven, the pattern is:
$response = app(AIEngineService::class)->generate(
new AIRequest(prompt: '...', engine: 'openai', model: 'gpt-4o')
);
Use generateDirect() when you want to bypass failover and fail loudly — good for tests and for latency-sensitive paths where a silent retry on a second provider is worse than an error.
Default to AiResource for entity CRUD, hand-write tools only for side effects. Resources cover search/write/detail with relations (->with(['items'])) and ->detailOnly() for read-only views. Reserve custom AgentTool subclasses for actions with real business logic, and implement requiresConfirmation() / getConfirmationMessage() on anything destructive.
Skills are for multi-step flows with a guaranteed terminal action. A skill declares $id, $triggers, $requiredData, $capabilities, and — critically — a final tool:
$skill->final(CreateInvoiceTool::class)->confirmTerms(['customer', 'line_items']);
The $requiredData + final-tool pairing is what makes skills reliable: the matcher routes on stripped keywords, then the skill blocks until its terminal tool has actually run. Keep $triggers write-specific. Adding a read-ambiguous keyword like "invoice products" will hijack read questions into a create flow.
Resolve relations deterministically, never via LLM guesses. Use the ResolvesAgentRelations trait, declare relations() with an identity field (usually email), and call resolveRelations($params, $ctx) inside execute(). The map array translates tool params to model columns; create lists what may be created on a miss. Treating identity as source of truth prevents duplicate customers from a hallucinated name variant.
Domain agents: use 'handler' => 'ai_native' when the sub-agent must plan. Only AiNativeSubAgentHandler runs a real planner over the sub-agent's declared tools, scoped to exactly that ToolRegistry. The tool and conversational handlers do not NL-plan tool calls — if you configure a sub-agent expecting reasoning and get a static dispatch, that's why.
Scale tool context incrementally. Start with ai-agent.ai_native.tool_selection.strategy = 'skill_scoped', add disclosure: 'progressive' (the find_tools meta-tool) when the registry grows past a dozen tools, and treat 'semantic' as a last resort — it costs an embedding round-trip per turn and adds a failure mode you can't debug from logs alone.
RAG follows a three-part shape: Vectorizable trait on the model (getVectorContent / getVectorMetadata) → VectorSearchService::index (or indexBatch) during writes, or php artisan ai:vector-index in a job → retrieval through the built-in search_knowledge tool. Index in a queued job; indexing inside a request handler is the most common source of slow admin saves.
Testing is deterministic, always. Mock AIEngineService in feature tests and use the fake embedding mode. Never let CI make a live model call — it makes suites flaky, slow, and billable. Assert on tool selection and ActionResult shape, not on prose.
Integrate with observability early. Listen to AIRequestStarted / AIRequestCompleted / AIFailoverTriggered and log them with a request ID. By the time a prompt regresses in production, you want the failover trail already recorded.
Fail-closed scope is the #1 source of "the agent says zero." data_query and aggregate_data scope by user_id / workspace_id / tenant_id from UnifiedActionContext unless the model is declared 'public' => true. A domain agent returning 0 for rows that clearly exist is almost never the planner — it's a missing public flag on a shared catalog model, or a context that carries no user ID (CLI, queue worker, webhook). Check the context before you touch the prompt.
Domain agents deliberately do not inherit global skills (EmptySkillRegistry). If you don't do this, an "invoice" persona matches the invoice-create skill, and that skill's final-tool guard blocks every read-only answer until a create tool runs — looping to exhaustion. Don't "fix" this by re-enabling skills on a domain agent; keep write flows as top-level skills and read flows as domain agents.
Know the convergence fallback exists. When the planner runs a tool successfully but exhausts steps before finalizing, AiNativeSubAgentHandler surfaces the last successful tool result and sets metadata.converged_via = 'tool_result_fallback'. That's a safety net, not success — alert on that metadata key in production, because it hides a planner that needs better tool descriptions.
Multi-turn confirmations belong at the top level. Do not build a confirmation conversation inside a delegated sub-agent. Use a skill, or use StructuredCollectionDefinition + StructuredCollectionSessionService when you need guided collection (addText, addEmail, list, summarize, confirm). The structured path also gives you package-rendered chat previews — publish with php artisan vendor:publish --tag=ai-engine-collection-ui and accept that you don't get AI-generated HTML.
Two different "not configured" failures look identical. A missing key silently removes an engine from fallback_engines, so requests succeed via a different provider with different quality and different cost. Run ai:doctor after every environment change, and log AIFailoverTriggered at warning level.
File analysis is sandboxed to base_path. analyze_file only extracts stored uploads under that root; paths outside it fail without a useful error. 'prefill' => true uses StructuredFileExtractor and derives pre-fill fields from the target create tool's own parameter definitions — so if pre-fill returns nothing, check that your create tool's getParameters() actually documents the fields.
Transcription is polymorphic with no trait. Add morphMany(Transcription::class, 'transcribable') to your model yourself; there is no Transcribable trait to use. Same class of trap for graph RAG: models need explicit getSourceNode() / getAccessScope() / toGraphObject(), and graph.require_access_scope is fail-closed.
Re-index after model changes. Changing getVectorContent() without re-running ai:vector-index leaves stale vectors that retrieve confidently and incorrectly. Make re-indexing part of the migration checklist for any model carrying the Vectorizable trait.
Pin the version and verify against src/. With a single star and zero dependents, you are the de facto QA. The repository is published as mabou7agar/laravel-ai-support while the Composer package is m-tech-stack/laravel-ai-engine — confirm the installed vendor path and read the class before trusting any snippet, including ones in this document. The "last release: 2026-08-13" date means the API is still moving; a composer.lock pin plus a short smoke test around AIEngineService::generate and ai:doctor will save you an afternoon on upgrade.
Credits are opt-in and easy to forget. CreditManager (hasCredits, deductCredits, calculateCredits) does nothing until ai-engine.credits.enabled is true, and per-model credit_index values are yours to calibrate. Turn it on before the first bill, not after.
How can I help you explore Laravel packages today?