An AI chat over your own database. The engine runs an Anthropic Managed Agents conversation whose tools are safe, read-only SQL over the anonymized views of adt/doctrine-anonymization - so the model can analyze your data without ever seeing personal details.
composer require adt/ai-chatThe package is the conversation engine: session handling, the polling loop, tool execution, agent provisioning, prompt building and output redaction. It ships no HTTP client, no queue and no frontend - those live in your project behind two small interfaces, because every project already has its own.
ManagedAgentsClient (you implement) ──► AgentTurnRunner ──► TurnResult
│
ToolHandler (SQL / charts / export) ◄──────┘
│
ReadOnlyQueryExecutor (adt/doctrine-anonymization, read-only account, anon schema)
The examples/ directory is a full reference integration you can copy
into a project and adjust:
| File | Shows |
|---|---|
GuzzleManagedAgentsClient.php |
the transport - raw HTTP with the beta header, over Guzzle |
Entity/Conversation.php, Entity/Agent.php |
entities built from the package traits, ownership on your side |
Entity/Message.php |
a minimal transcript entity (the package has no message contract on purpose) |
ChatService.php |
the coordinator - web request stores + dispatches, worker runs the turn, redacts and persists |
QueueMessageDispatcher.php |
the background hand-off (here adt/background-queue) incl. the consumer wiring |
config.neon |
complete DI registration of everything above |
ui/ChatControl.php + .latte |
the frontend as a Nette component: AJAX signals, polling, CSV download endpoint, tool-data whitelisting |
ui/ChatPresenter.php + .latte |
a page hosting the control incl. the CDN includes (Chart.js, marked, DOMPurify) |
ui/assets/aiChatControl.js, ui/assets/ai-chat.css |
the complete browser side: conversations, markdown rendering, charts, tables, context gauge, CSV buttons |
The examples are not autoloaded by the package - they are a starting point, not an API.
ManagedAgentsClient maps 1:1 to the Managed Agents REST endpoints (beta). Use
your existing HTTP client with its logging and error handling:
class MyAiClient implements ADT\AiChat\Client\ManagedAgentsClient { ... }A turn takes minutes, so it must run in a worker. MessageDispatcher::dispatch()
hands the message to your queue; the worker then runs the turn:
class QueueMessageDispatcher implements ADT\AiChat\Dispatch\MessageDispatcher
{
public function dispatch(int|string $conversationId, string $userMessage): void
{
$this->queue->publish('aiChatProcessMessage', compact('conversationId', 'userMessage'));
}
}Conversation and agent mapping are your Doctrine entities implementing the package interfaces; the traits carry the default mapping, your side adds the id and the owner relation:
#[ORM\Entity]
class AiChatConversation implements ADT\AiChat\Entity\ConversationInterface
{
use ADT\AiChat\Entity\ConversationTrait;
#[ORM\ManyToOne(targetEntity: User::class)]
public User $user; // ownership stays on your side
...
}
#[ORM\Entity]
class AiChatAgent implements ADT\AiChat\Entity\AgentInterface
{
use ADT\AiChat\Entity\AgentTrait;
...
}Run right after regenerating the anonymized views (same deploy step). Idempotent: an existing agent is updated in place (same id, new version) with the current system prompt and tool definitions:
[$mapping, $created] = $provisioner->ensureProvisioned($schemaName);$runner = new AgentTurnRunner($client, $toolHandler, $provisioner->resolver($schemaName));
$result = $runner->run($conversation, $userMessage, function (?string $sessionId) use ($em): void {
$em->flush(); // persist the session id as soon as it changes
});
$text = $piiFilter->filter($result->text, $piiValueCollector->collect($result->toolData));
// store $text + $result->toolData + $result->tokensInput/Output as a messageThe runner handles: ignoring pre-existing session events, executing custom tools
and sending their results back, token accounting (window usage = last request incl.
cache counters; output summed), a retry with a fresh session when a stale one
refuses the message, and hard stops (maxPolls, maxToolCalls) that surface as
TurnTimeoutException - which is never retried, so a message cannot be duplicated.
Users can attach files (images, PDFs, text/CSV) to a message. Upload each file to
the Anthropic Files API via ManagedAgentsClient::uploadFile() in the web request,
pass only the returned file ids through your queue, and hand them to the runner as
Attachment objects - it sends them as image/document content blocks of the same
user.message, so the model reads the files directly from its context:
$file = $client->uploadFile('export.csv', $contents, 'text/plain');
$result = $runner->run($conversation, $userMessage, $persist, [
new Attachment($file['id'], 'text/plain', 'export.csv'),
]);Media types follow the content-block rules: image/jpeg|png|gif|webp become image
blocks, application/pdf and text/plain document blocks. Upload other text
formats (CSV, Markdown, logs) as text/plain - the filename keeps the extension.
ToolHandler is the default ToolExecutor:
| Tool | Purpose |
|---|---|
get_database_schema |
tables of the anon schema; columns incl. #[Description] texts |
execute_sql |
read-only SELECT, row-capped, over the read-only account |
render_chart / render_table |
displayable results for your frontend |
export_csv |
validates the SQL and returns a download token; your endpoint streams the rows later via ReadOnlyQueryExecutor::streamQuery() (never through the model) |
Different tool set? Implement ToolExecutor yourself.
PiiResponseFilter redacts e-mails, phone numbers, bank accounts and secrets from
the final reply by pattern, and - via PiiValueCollector - the literal values that
appeared in personal-data columns of SQL results (names, birth dates), which no
regex can recognise. Replacement labels and the phone prefix are configurable for
localized chats.
SystemPromptBuilder renders a template with a {{tableList}} placeholder; only
table names go into the prompt (columns are fetched on demand by the schema tool),
which keeps every inference cheap and clear of the hard system length limit.
A generic default template ships in src/resources/system-prompt.md - copy it and
tune the wording to your domain.
composer install
vendor/bin/codecept run unitThe suite drives the full turn loop against a scriptable fake of the Managed Agents API (tool round-trips, history isolation, timeouts, stale-session retry, token accounting) plus the tool handler, prompt builder and redaction - no network and no database.