prism-php/bedrock
Amazon Bedrock driver for Prism, bringing AWS-hosted foundation models like Claude, Llama, Titan, and Mistral into Laravel AI workflows. Configure credentials and use Prism’s unified API for text generation and embeddings.
Bedrock gives Prism-powered agents a ready-made toolkit: instead of hand-rolling every function schema, you attach Bedrock's tools (and your own Bedrock-style tool classes) to a Prism request and let the model call them.
Minimal setup:
composer require prism-php/bedrock
php artisan vendor:publish --tag="bedrock-config"
v1.8.0 bumps the pinned Prism version, so update both packages together — a stale prism-php/prism alongside Bedrock 1.8 is the most likely source of a confusing signature/type error on first boot:
composer update prism-php/bedrock prism-php/prism
Look first at config/bedrock.php (which tool groups are enabled and how they're scoped) and then at the tool classes themselves — that's where the schemas and handle() methods live, and it's the fastest way to understand what the model can actually do.
First use case: attach a single read-only tool and ask a question in natural language.
use Prism\Prism\Facades\Prism;
$response = Prism::text()
->using('anthropic', 'claude-sonnet-4-5')
->withTools([new DatabaseQueryTool(connection: 'readonly')])
->withPrompt('How many orders shipped last week, broken down by day?')
->asText();
If that works end to end, you understand the whole package: schema in, tool call out, result string back to the model.
Scope tools per agent, not per app. The single biggest design decision is which tool set a given agent gets. A support bot gets search + read-only lookups; an internal ops agent gets the wider set. Build named toolkits in config or a small factory class so the set is reviewable in one place rather than assembled ad hoc in every controller.
Parameterless tools are now first-class (v1.8.0). Tools that take no arguments — "list available reports", "get current server time", "flush the cache" — used to be awkward: you'd either add a dummy parameter the model had to fill in, or fight schema validation when the model sent {}. The improvement in #42 means you can declare a genuinely empty schema and trust the model to invoke it with no arguments. If you were carrying dummy _unused parameters as a workaround, remove them — they cost tokens on every request and occasionally get hallucinated into real values.
class ListReportsTool extends Tool
{
public function schema(PrismRequest $request): array
{
return []; // no parameters — supported cleanly as of v1.8.0
}
public function handle(PrismRequest $request): string
{
return Report::pluck('name')->implode(', ');
}
}
Treat handle() as an API boundary. Whatever you return is fed straight back into the model's context. Return compact, structured strings (a short table or JSON), not dd()-style dumps or full Eloquent collections. Truncate aggressively. A tool that returns 40KB of rows will blow your context window and degrade the answer far more than a tool that returns the top 20 results plus a count.
Compose built-in tools with your own. The pragmatic pattern is Bedrock's shipped tools for the generic work (HTTP fetch, file read, DB query) plus a handful of domain tools that encode your business logic (RefundOrderTool, LookupCustomerTool). Domain tools beat giving the model raw SQL access for anything that mutates state.
Guard destructive tools explicitly. Anything that writes, sends, or deletes should be behind an authorization check inside handle() and, ideally, a two-step confirm pattern: the tool returns "about to refund $340 to order #9921 — call ConfirmRefund with this token to proceed." Models are enthusiastic; the guardrail belongs in PHP, not in the prompt.
Test with Prism's fakes. Assert on which tools the model is offered and what your tools return, not on model output. Bedrock tools are plain classes with a handle() method, so most of your coverage is ordinary unit tests that never touch an API.
Every tool schema is billed on every request. Twenty tools means twenty JSON schemas in the system prompt for a question that needs one. Prune aggressively, and keep descriptions short but unambiguous about when to call — the description is the model's only routing signal.
v1.8.0's Prism bump is the sharp edge. If you pin Prism in composer.json, confirm your constraint allows the version Bedrock 1.8 requires. Symptoms of a mismatch are usually a TypeError on a method signature or a silently ignored tool, not a clean "version conflict" message.
Empty schemas can still trip strict providers. Some providers dislike parameters: {} and want the key omitted entirely. If a parameterless tool works on one provider and errors on another after upgrading, that's the first place to look — and it's worth having one parameterless tool in your smoke test suite precisely to catch it.
Log every tool call. Wrap handle() so the tool name, arguments, duration, and a truncated result land in a log channel. When an agent misbehaves, the tool call trace explains 90% of it — you'll see the model calling the wrong tool, passing a plausible-but-wrong ID, or looping on the same failing call.
Never let a tool throw raw exceptions at the model. Catch, log, and return a human-readable failure string ("No customer found with ID 4412"). An uncaught exception kills the whole turn; a returned error message lets the model retry or explain.
Point database tools at a read-only connection. A dedicated MySQL/Postgres user with SELECT only is cheap insurance and makes a prompt-injection-via-tool-result far less catastrophic.
Run php artisan config:clear after editing config/bedrock.php — a cached config is the usual reason a newly enabled tool group appears not to exist.
Extension point: the base tool class is the seam. Subclass it, add shared concerns (audit logging, rate limiting, permission checks) in a parent, and every tool in your app inherits them — much better than repeating the same guard in fifteen handle() methods.
How can I help you explore Laravel packages today?