class Assistant extends \NeuronAI\Agent\Agent
{
/** Create from configuration */
public static function configure(AssistantConfig $config): self;
/** Chat with the assistant (returns AgentHandler for streaming/tool-calls) */
public function chat(Message|array $messages = [], ?InterruptRequest $interrupt = null): AgentHandler;
/**
* Get structured output from the assistant.
* Returns a filled instance of the output class with #[SchemaProperty] attributes.
*
* @param Message|Message[] $messages The user message(s)
* @param string|null $class FQCN of the output class (uses outputClass from config if null)
* @param int $maxRetries Override retry count (-1 = use config's structuredMaxRetries)
* @param InterruptRequest|null $interrupt Optional interrupt request
* @return object Filled instance of the output class
* @throws AgentException If no output class is configured and none provided
*/
public function structured(
Message|array $messages = [],
?string $class = null,
int $maxRetries = -1,
?InterruptRequest $interrupt = null,
): mixed;
/** Delegate to a sub-agent */
public function delegate(string $subAgentId, UserMessage $message): SubAgentResult;
/** Get the context condenser */
public function getContextCondenser(): ContextCondenserInterface;
/** Get the sub-agent registry */
public function getSubAgentRegistry(): SubAgentRegistry;
/** Get the skill registry */
public function getSkillRegistry(): SkillRegistry;
/** Get the default storage backend */
public function getStorage(): StorageInterface;
/** Get the storage backend for conversations */
public function getConversationStorage(): StorageInterface;
/** Get the storage backend for learning data */
public function getLearningStorage(): StorageInterface;
/** Get the storage backend for user memories */
public function getUserMemoryStorage(): StorageInterface;
/** Get the auto-learning engine */
public function getLearningEngine(): ?AutoLearningEngine;
/** Get the user memory store */
public function getUserMemoryStore(): ?UserMemoryStore;
}class AssistantConfig
{
public function __construct(
public readonly AIProviderInterface $provider,
public readonly StorageInterface $storage,
public readonly string $instructions = '',
public readonly int $contextWindow = 200000,
public readonly array $tools = [],
public readonly array $subAgents = [],
public readonly array $skills = [],
public readonly array $mcps = [],
public readonly ?string $skillsPath = null,
public readonly bool $autoLearn = false,
public readonly bool $autoDelegate = true,
public readonly bool $requireLearningCheck = true,
public readonly array $middleware = [],
public readonly ?LoggerInterface $logger = null,
public readonly ?string $userId = null,
public readonly ?float $requestTimeout = null,
public readonly ?string $outputClass = null,
public readonly int $structuredMaxRetries = 1,
public readonly ?StorageInterface $conversationStorage = null,
public readonly ?StorageInterface $learningStorage = null,
public readonly ?StorageInterface $userMemoryStorage = null,
) {}
}Each storage domain can use a different backend. When null, falls back to the main storage:
| Parameter | Domain | Fallback |
|---|---|---|
conversationStorage |
Conversations (threads) | $storage |
learningStorage |
Learning patterns, bugs, entries | $storage |
userMemoryStorage |
Per-user persistent memories | $storage |
new AssistantConfig(
provider: $provider,
storage: new FileStorage('/data/default'),
conversationStorage: new RedisStorage($redis),
learningStorage: new FileStorage('/data/learning'),
userMemoryStorage: new DatabaseStorage($pdo),
);contextWindowmust be at least 1000requestTimeoutmust be greater than 0 (when provided)structuredMaxRetriesmust be 0 or greater- All
subAgentsentries must beSubAgentConfiginstances outputClassmust be an existing class (when provided)
The assistant supports structured output via Neuron AI's #[SchemaProperty] system. Define a PHP class with typed properties and the SchemaProperty attribute, and the assistant will return a filled instance instead of plain text.
use NeuronAI\StructuredOutput\SchemaProperty;
class MapLayer
{
#[SchemaProperty(description: 'Layer name', required: true)]
public string $name;
#[SchemaProperty(description: 'Layer type: raster, vector, tile', required: true)]
public string $type;
#[SchemaProperty(description: 'Source URL or identifier', required: true)]
public string $source;
#[SchemaProperty(description: 'Default visibility', required: false)]
public bool $visible = true;
#[SchemaProperty(description: 'Opacity from 0 to 1', required: false)]
public float $opacity = 1.0;
}
class MapConfig
{
#[SchemaProperty(description: 'Map title', required: true)]
public string $title;
#[SchemaProperty(description: 'Map layers configuration', required: true, anyOf: [MapLayer::class])]
public array $layers;
#[SchemaProperty(description: 'Center coordinates [lat, lng]', required: false)]
public ?array $center = null;
#[SchemaProperty(description: 'Default zoom level', required: false)]
public ?int $zoom = null;
}Option A — Default output class via config:
$assistant = Assistant::configure(
new AssistantConfig(
provider: $provider,
storage: $storage,
instructions: 'You configure interactive maps based on user requirements.',
outputClass: MapConfig::class,
)
);
$config = $assistant->structured(
new UserMessage('Create a street map of São Paulo with satellite overlay')
);
// $config is a MapConfig instance
echo $config->title;
foreach ($config->layers as $layer) {
echo $layer->name . ': ' . $layer->type;
}Option B — Explicit class per call:
$assistant = Assistant::configure(
new AssistantConfig(
provider: $provider,
storage: $storage,
instructions: 'You are a data extraction assistant.',
)
);
$person = $assistant->structured(
new UserMessage('My name is Alice, I am 30 years old and live in Berlin.'),
PersonInfo::class,
);Add validation attributes to your output class properties. If validation fails, Neuron automatically retries the request up to structuredMaxRetries times:
use NeuronAI\StructuredOutput\SchemaProperty;
use NeuronAI\StructuredOutput\Validation\Rules\NotBlank;
use NeuronAI\StructuredOutput\Validation\Rules\Length;
use NeuronAI\StructuredOutput\Validation\Rules\Count;
class ReportConfig
{
#[SchemaProperty(description: 'Report title', required: true)]
#[NotBlank]
#[Length(min: 3, max: 255)]
public string $title;
#[SchemaProperty(description: 'Report sections', required: true, anyOf: [Section::class])]
#[Count(min: 1)]
public array $sections;
}| Rule | Description |
|---|---|
#[NotBlank] |
Value cannot be empty |
#[Length(min:, max:, exactly:)] |
String length constraints |
#[WordsCount(min:, max:, exactly:)] |
Word count constraints |
#[Count(min:, max:, exactly:)] |
Array size constraints |
#[EqualTo(reference:)] / #[NotEqualTo(reference:)] |
Exact value comparison |
#[GreaterThan(reference:)] / #[GreaterThanEqual(reference:)] |
Minimum value |
#[LowerThan(reference:)] / #[LowerThanEqual(reference:)] |
Maximum value |
#[OutOfRange(min:, max:)] |
Value must be outside range |
#[IsTrue] / #[IsFalse] |
Boolean value assertion |
#[IsNull] / #[IsNotNull] |
Nullability assertion |
#[Json] |
Must be valid JSON string |
#[Url] |
Must be valid URL |
#[Email] |
Must be valid email |
#[IpAddress] |
Must be valid IP address |
#[ArrayOf(class:)] |
Array must contain instances of class |
Output classes can reference other structured classes as properties or array items:
class Address
{
#[SchemaProperty(description: 'Street name', required: true)]
public string $street;
#[SchemaProperty(description: 'City', required: false)]
public ?string $city = null;
#[SchemaProperty(description: 'ZIP code', required: true)]
public string $zip;
}
class Contact
{
#[SchemaProperty(description: 'Full name', required: true)]
public string $name;
#[SchemaProperty(description: 'Address', required: true)]
public Address $address;
#[SchemaProperty(description: 'Tags', required: true, anyOf: [Tag::class])]
public array $tags;
}To use the structured output as application configuration (e.g., return as JSON to a frontend):
$config = $assistant->structured(
new UserMessage('Create a street map with satellite overlay')
);
header('Content-Type: application/json');
echo json_encode($config, JSON_PRETTY_PRINT);Output:
{
"title": "Street Map with Satellite Overlay",
"layers": [
{
"name": "Satellite Imagery",
"type": "raster",
"source": "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
"visible": true,
"opacity": 0.7
},
{
"name": "Street Network",
"type": "vector",
"source": "mapbox://mapbox.mapbox-streets-v8",
"visible": true,
"opacity": 1.0
}
],
"center": [-23.5505, -46.6333],
"zoom": 12
}#[SchemaProperty(
description: 'Property description for the LLM', // Required: helps the LLM understand what to fill
required: true, // Whether the property is required
minLength: 1, // String minimum length
maxLength: 255, // String maximum length
min: 0, // Numeric minimum
max: 100, // Numeric maximum
anyOf: [SomeClass::class], // Array item types (for array properties)
)]interface ContextCondenserInterface
{
public function condense(
array $messages,
string $taskDescription,
int $maxTokens,
?string $contextStrategy = null
): CondensedContext;
}
class CondensedContext
{
public function __construct(
public readonly array $messages,
public readonly ?string $summary,
public readonly array $keyFacts,
public readonly int $originalTokens,
public readonly int $condensedTokens,
public readonly string $strategy,
) {}
public function toMessages(): array;
}class SubAgentConfig
{
public function __construct(
public readonly string $id,
public readonly AIProviderInterface $provider,
public readonly string $instructions,
public readonly array $tools = [],
public readonly array $skills = [],
public readonly string $contextStrategy = 'default',
public readonly int $contextWindow = 150000,
public readonly array $mcps = [],
public readonly array $middleware = [],
) {}
}
class SubAgentRegistry
{
public function register(string $id, SubAgentConfig $config): void;
public function get(string $id): SubAgentConfig;
public function has(string $id): bool;
public function all(): array;
}
class SubAgentDispatcher
{
public function delegate(
string $subAgentId,
UserMessage $message,
array $currentMessages = []
): SubAgentResult;
}
class SubAgentResult
{
public function __construct(
public readonly Message $message,
public readonly WorkflowState $state,
public readonly CondensedContext $context,
public readonly array $toolCalls = [],
public readonly int $tokenUsage = 0,
public readonly float $duration = 0.0,
) {}
public function getContent(): string;
public function getSteps(): array;
}class Skill
{
public function __construct(
public readonly string $name,
public readonly string $description,
public readonly string $content,
public readonly array $tools = [],
public readonly ?string $contextStrategy = null,
public readonly array $categories = [],
public readonly ?string $version = null,
public readonly ?string $author = null,
public readonly ?string $sourceFile = null,
) {}
public function toSystemPrompt(): string;
}
class SkillRegistry
{
public function loadAll(): void;
public function register(Skill $skill): void;
public function get(string $name): ?Skill;
public function has(string $name): bool;
public function all(): array;
public function byCategory(string $category): array;
public function search(string $query): array;
}
class MarkdownSkillLoader
{
public function load(string $filePath): Skill;
public function loadDirectory(string $directory): array;
}class McpConfigBridge
{
public static function make(array $config, ?LoggerInterface $logger = null): McpConnector;
}Configuration formats:
// stdio — command is validated against an allowlist (npx, node, python3, python, uvx, docker, php)
['type' => 'stdio', 'command' => 'npx', 'args' => ['@modelcontextprotocol/server-github']]
// SSE — URL validated (no internal/metadata endpoints)
['type' => 'sse', 'url' => 'http://localhost:8080/sse', 'token' => 'optional']
// HTTP
['type' => 'http', 'url' => 'https://api.example.com/mcp']- Command allowlist for stdio (only known binaries allowed)
- Argument validation (no path traversal, no shell metacharacters)
- URL validation (http/https only, warns on internal addresses)
- PSR-3 logging of all connection attempts
class ToolLearner
{
public function record(ToolInterface $tool, array $arguments, mixed $result, ?\Throwable $error = null, array $context = []): void;
public function getSuccessRate(string $toolName): float;
public function findPatterns(string $taskDescription, int $limit = 5): array;
public function suggestTools(string $taskDescription): array;
}
class BugCollector
{
public function collect(\Throwable $error, array $context, ?string $resolution = null): string;
public function findSimilar(\Throwable $error, array $context): array;
public function resolve(string $bugId, string $resolution): void;
public function getUnresolved(): array;
}
class SuggestionEngine
{
public function suggestTools(string $taskDescription, array $availableTools): array;
public function getWarnings(string $toolName, array $arguments): array;
public function getTips(string $taskDescription): array;
}The GuardsAgainstPoisoning trait is applied to RecordLearningTool, RecordBugTool, and ForgetLearningTool. It detects instruction-like patterns (e.g., "never use X", "always skip Y") and refuses to record/delete them, preventing knowledge base poisoning through user manipulation.
class UserMemory
{
public function __construct(
public readonly string $id,
public readonly string $userId,
public readonly string $category,
public readonly string $content,
public readonly array $tags = [],
public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
public readonly ?\DateTimeImmutable $updatedAt = null,
) {}
public function matches(string $query): float;
public function toArray(): array;
public static function fromArray(array $data): self;
}
class UserMemoryStore
{
public function __construct(StorageInterface $storage);
public function save(UserMemory $memory): void;
public function get(string $userId, string $memoryId): ?UserMemory;
public function listForUser(string $userId, ?string $category = null): array;
public function search(string $userId, string $query): array;
public function delete(string $userId, string $memoryId): bool;
public function exists(string $userId, string $memoryId): bool;
}| Tool | Parameters | Description |
|---|---|---|
save_memory |
category, content, tags? | Save a memory (categories: preference, context, note, instruction) |
recall_memories |
query, category? | Search memories by query and optional category |
delete_memory |
memory_id | Delete a memory (ownership verified) |
userIdis injected viaAssistantConfigby the backend — never from user messages- Storage is partitioned by sanitized user ID via namespace (
memories/{userId}) delete_memoryverifiesmemory->userId === $this->userIdbefore deletion
interface StorageInterface
{
public function save(string $namespace, string $key, array $data): void;
public function load(string $namespace, string $key): ?array;
public function delete(string $namespace, string $key): bool;
public function exists(string $namespace, string $key): bool;
/**
* @return string[]
*/
public function list(string $namespace, string $pattern = '*'): array;
/**
* @return array{data: array, score: float}[]
*/
public function search(string $namespace, string $query, int $limit = 10): array;
/**
* @param array{max_age_days?: int, max_per_namespace?: int} $criteria
* @return int Number of entries removed
*/
public function cleanup(string $namespace, array $criteria = []): int;
}
class FileStorage implements StorageInterface
{
public function __construct(string $basePath);
}
class ConversationStore
{
public function __construct(StorageInterface $storage);
public function saveThread(string $threadId, array $messages): void;
public function loadThread(string $threadId): array;
public function appendToThread(string $threadId, array $messages): void;
public function listThreads(): array;
public function deleteThread(string $threadId): bool;
}
class HierarchicalChatHistory extends AbstractChatHistory
{
public function __construct(
int $contextWindow = 150000,
int $summaryThreshold = 10000,
int $recentMessages = 5,
?AIProviderInterface $summarizationProvider = null,
);
public function summarize(): void;
public function extractFacts(): void;
}class KnowledgeBase
{
public function __construct(StorageInterface $storage);
public function saveToolPattern(ToolPattern $pattern): void;
public function getToolPatterns(string $toolName): array;
public function getToolNames(): array;
public function searchPatterns(string $query): array;
public function saveBug(BugReport $bug): string;
public function getBug(string $id): ?BugReport;
public function searchBugs(array $criteria): array;
public function saveLearning(LearningEntry $entry): void;
public function getLearnings(string $context): array;
public function searchLearnings(string $query): array;
public function getContexts(): array;
public function deleteLearning(string $context, string $id): bool;
public function cleanup(): int;
}
class LearningEntry
{
public function __construct(
public readonly string $context,
public readonly string $observation,
public readonly bool $workedWell,
public readonly array $tags = [],
public readonly ?string $id = null,
public readonly \DateTimeImmutable $timestamp = new \DateTimeImmutable(),
) {}
public function matches(string $query): float;
public function toArray(): array;
public static function fromArray(array $data): self;
}
class ToolPattern
{
public function __construct(
public readonly string $toolName,
public readonly array $arguments,
public readonly mixed $result,
public readonly ?string $error = null,
public readonly array $context = [],
public readonly \DateTimeImmutable $timestamp = new \DateTimeImmutable(),
) {}
public function matches(string $taskDescription): float;
public function toArray(): array;
public static function fromArray(array $data): self;
}
class BugReport
{
public function __construct(
public readonly string $id,
public readonly string $errorType,
public readonly string $errorMessage,
public readonly string $stackTrace,
public readonly array $context,
public readonly \DateTimeImmutable $timestamp,
public readonly ?string $resolution = null,
public readonly bool $resolved = false,
) {}
public function toArray(): array;
public static function fromArray(array $data): self;
}class SensitiveDataRedactor
{
public function redact(string $text): string;
public function redactMessages(array $messages): array;
public static function redactString(string $text): string;
}
class TokenEstimator
{
public function estimate(string $text): int;
public function estimateMessages(array $messages): int;
}
class ConfigStorage
{
public function __construct(?string $path = null);
public function load(): array;
public function save(array $config): void;
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value): void;
public function exists(): bool;
public function isEncryptionAvailable(): bool;
}Config files are encrypted with sodium_crypto_secretbox() when the HL_AI_ENCRYPTION_KEY environment variable is set. Without the variable, config is stored in plaintext (with a warning in the CLI example).
class StderrLogger extends AbstractLogger
{
public function __construct(string $prefix = 'ai-assistant');
}The library uses PSR-3 LoggerInterface throughout. Pass any PSR-3 logger (Monolog, etc.) via AssistantConfig::$logger. Without one, a NullLogger is used (no output).
Reads and extracts text from local documents. Supports PDF, DOCX, TXT, CSV, Markdown, HTML, JSON, XML, and RTF.
use HackLab\AIAssistant\Tools\FileReader\FileReaderTool;
new AssistantConfig(
provider: $provider,
storage: $storage,
tools: [new FileReaderTool()],
);new FileReaderTool(
int $maxFileSizeBytes = 52428800, // 50MB
?array $readers = null, // Custom readers (null = defaults)
);| Property | Type | Required | Description |
|---|---|---|---|
file_path |
string | yes | Absolute path to the file |
max_length |
integer | no | Max characters to return (default: 100000) |
{
"success": true,
"file": "document.pdf",
"type": "pdf",
"size_bytes": 12345,
"content": "Extracted text...",
"truncated": false
}| Format | Extension | Reader |
|---|---|---|
.pdf |
PdfDocumentReader (smalot/pdfparser) |
|
| Word | .docx |
DocxDocumentReader (phpoffice/phpword) |
| Plain Text | .txt |
PlainTextDocumentReader |
| CSV | .csv |
PlainTextDocumentReader (formatted as table) |
| Markdown | .md |
PlainTextDocumentReader |
| HTML | .html, .htm |
PlainTextDocumentReader |
| JSON | .json |
PlainTextDocumentReader |
| XML | .xml |
PlainTextDocumentReader |
| RTF | .rtf |
PlainTextDocumentReader |
Implement DocumentReaderInterface to add support for new file types:
use HackLab\AIAssistant\Tools\FileReader\DocumentReaderInterface;
use HackLab\AIAssistant\Tools\FileReader\FileReaderTool;
class ExcelReader implements DocumentReaderInterface
{
public function supports(string $type): bool
{
return $type === 'xlsx';
}
public function read(string $filePath): string
{
// Extract text from Excel...
}
}
$tool = new FileReaderTool(readers: [
new ExcelReader(),
// Default readers are NOT included when you pass custom ones.
// Add them manually if needed:
new \HackLab\AIAssistant\Tools\FileReader\PdfDocumentReader(),
new \HackLab\AIAssistant\Tools\FileReader\DocxDocumentReader(),
new \HackLab\AIAssistant\Tools\FileReader\PlainTextDocumentReader(),
]);