LLM Agents is a PHP library for building and managing Language Model (LLM) based agents. It provides a framework for creating autonomous agents that can perform complex tasks, make decisions, and interact with various tools and APIs.
The library enables developers to integrate LLM capabilities into PHP applications efficiently, allowing for the creation of intelligent systems that can understand and respond to user inputs, process information, and carry out actions based on that processing.The library enables developers to integrate LLM capabilities into PHP applications efficiently.
For a comprehensive explanation of LLM agents and their applications, you can read the article: A PHP dev's dream: A PHP dev’s dream: An AI home that really gets you
For a complete example with sample agents and a CLI interface to interact with them, check out our sample application repository https://github.com/llm-agents-php/sample-app.
This sample app demonstrates practical implementations and usage patterns of the LLM Agents library.
The package does not include any specific LLM implementation. Instead, it provides a framework for creating agents that can interact with any LLM service or API.
- 🤖 Agent Creation: Create and configure LLM-based agents in PHP with customizable behaviors.
- 🔧 Tool Integration: Seamlessly integrate various tools and APIs for agent use in PHP applications.
- 🧠 Memory Management: Support for agent memory, enabling information retention and recall across interactions.
- 💡 Prompt Management: Efficient handling of prompts and instructions to guide agent behavior.
- 🔌 Extensible Architecture: Easily add new agent types, tools, and capabilities to your PHP projects.
- 🤝 Multi-Agent Support: Build systems with multiple interacting agents for complex problem-solving scenarios in PHP.
You can install the LLM Agents package via Composer:
composer require llm-agents/agentsTo create an agent, you'll need to define its behavior, tools, and configuration. Here's a basic example:
useLLM\Agents\Agent\AgentAggregate;
useLLM\Agents\Agent\Agent;
useLLM\Agents\Solution\Model;
useLLM\Agents\Solution\ToolLink;
useLLM\Agents\Solution\MetadataType;
useLLM\Agents\Solution\SolutionMetadata;
class SiteStatusCheckerAgent extends AgentAggregate
{
publicconstNAME = 'site_status_checker';
publicstaticfunctioncreate(): self
{
$agent = newAgent(
key: self::NAME,
name: 'Site Status Checker',
description: 'This agent checks the online status of websites.',
instruction: 'You are a website status checking assistant. Your goal is to help users determine if a website is online. Use the provided tool to check site availability. Give clear, concise responses about a site\'s status.',
);
$aggregate = newself($agent);
$aggregate->addMetadata(
newSolutionMetadata(
type: MetadataType::Memory,
key: 'check_availability',
content: 'Always check the site\'s availability using the provided tool.',
),
newSolutionMetadata(
type: MetadataType::Configuration,
key: 'max_tokens',
content: 500,
)
);
$model = newModel(model: 'gpt-4o-mini');
$aggregate->addAssociation($model);
$aggregate->addAssociation(newToolLink(name: CheckSiteAvailabilityTool::NAME));
return$aggregate;
}
}Now, let's implement the tool used by this agent:
useLLM\Agents\Tool\PhpTool;
useLLM\Agents\Tool\ToolLanguage;
class CheckSiteAvailabilityTool extends PhpTool
{
publicconstNAME = 'check_site_availability';
publicfunction__construct()
{
parent::__construct(
name: self::NAME,
inputSchema: CheckSiteAvailabilityInput::class,
description: 'This tool checks if a given URL is accessible and returns its HTTP status code and response time.',
);
}
publicfunctiongetLanguage(): ToolLanguage
{
return ToolLanguage::PHP;
}
publicfunctionexecute(object$input): string
{
$ch = curl_init($input->url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_NOBODY => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
]);
$startTime = microtime(true);
$response = curl_exec($ch);
$endTime = microtime(true);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$responseTime = round(($endTime - $startTime) * 1000, 2);
curl_close($ch);
$isOnline = $statusCode >= 200 && $statusCode < 400;
returnjson_encode([
'status_code' => $statusCode,
'response_time_ms' => $responseTime,
'is_online' => $isOnline,
]);
}
}And the input schema for the tool:
useSpiral\JsonSchemaGenerator\Attribute\Field;
class CheckSiteAvailabilityInput
{
publicfunction__construct(
#[Field(title: 'URL', description: 'The full URL of the website to check')]
publicreadonlystring$url,
) {}
}LLM Agents supports creating complex systems by linking multiple agents together. This allows you to build hierarchical or collaborative agent networks. Here's how you can link one agent to another:
To link one agent to another, you use the AgentLink class. Here's an example of how to modify our
SiteStatusCheckerAgent to include a link to another agent:
useLLM\Agents\Solution\AgentLink;
class SiteStatusCheckerAgent extends AgentAggregate
{
publicconstNAME = 'site_status_checker';
publicstaticfunctioncreate(): self
{
// ... [previous agent setup code] ...// Link to another agent$aggregate->addAssociation(
newAgentLink(
name: 'network_diagnostics_agent',
outputSchema: NetworkDiagnosticsOutput::class,
),
);
return$aggregate;
}
}In this example, we're linking a network_diagnostics_agent. The outputSchema parameter specifies the expected output
format from the linked agent. The output schema is used to standardize the data format that should be returned by the
linked agent.
We don't provide an implementation for the linked agent here, but you can use the linked agent in your agent's execution.
Here's an example of how you might call the linked agent:
useLLM\Agents\Tool\PhpTool;
useLLM\Agents\Agent\AgentExecutor;
useLLM\Agents\LLM\Prompt\Chat\ToolCallResultMessage;
useLLM\Agents\LLM\Response\ToolCalledResponse;
useLLM\Agents\Tool\ToolExecutor;
useLLM\Agents\Tool\ToolLanguage;
/** * @extends PhpTool<AskAgentInput> */finalclass AskAgentTool extends PhpTool
{
publicconstNAME = 'ask_agent';
publicfunction__construct(
privatereadonlyAgentExecutor$executor,
privatereadonlyToolExecutor$toolExecutor,
) {
parent::__construct(
name: self::NAME,
inputSchema: AskAgentInput::class,
description: 'Ask an agent with given name to execute a task.',
);
}
publicfunctiongetLanguage(): ToolLanguage
{
return ToolLanguage::PHP;
}
publicfunctionexecute(object$input): string|\Stringable
{
$prompt = \sprintf(
<<<'PROMPT'%sImportant rules:- Think before responding to the user.- Don not markup the content. Only JSON is allowed.- Don't write anything except the answer using JSON schema.- Answer in JSON using this schema:%s
PROMPT
,
$input->question,
$input->outputSchema,
);
while (true) {
$execution = $this->executor->execute($input->name, $prompt);
$result = $execution->result;
$prompt = $execution->prompt;
if ($resultinstanceof ToolCalledResponse) {
foreach ($result->toolsas$tool) {
$functionResult = $this->toolExecutor->execute($tool->name, $tool->arguments);
$prompt = $prompt->withAddedMessage(
newToolCallResultMessage(
id: $tool->id,
content: [$functionResult],
),
);
}
continue;
}
break;
}
return\json_encode($result->content);
}
}And the input schema for the tool:
useSpiral\JsonSchemaGenerator\Attribute\Field;
finalclass AskAgentInput
{
publicfunction__construct(
#[Field(title: 'Agent Name', description: 'The name of the agent to ask.')]
publicstring$name,
#[Field(title: 'Question', description: 'The question to ask the agent.')]
publicstring$question,
#[Field(title: 'Output Schema', description: 'The schema of the output.')]
publicstring$outputSchema,
) {}
}And just add the tool to the agent that has linked agents. When the agent is executed, it will call the linked agent if it decides to do so.
To execute an agent, you'll use the AgentExecutor class:
useLLM\Agents\AgentExecutor\ExecutorInterface;
useLLM\Agents\LLM\Prompt\Chat\Prompt;
useLLM\Agents\LLM\Prompt\Chat\MessagePrompt;
class AgentRunner
{
publicfunction__construct(
privateExecutorInterface$executor,
) {}
publicfunctionrun(string$input): string
{
$prompt = newPrompt([
MessagePrompt::user($input),
]);
$execution = $this->executor->execute(
agent: MyAgent::NAME,
prompt: $prompt,
);
return (string)$execution->result->content;
}
}
// Usage$agentRunner = newAgentRunner($executor);
$result = $agentRunner->run("Do something cool!");
echo$result;This example demonstrates how to create a simple agent that can perform a specific task using a custom tool.
Agents can use memory and predefined prompts to guide their behavior:
useLLM\Agents\Solution\SolutionMetadata;
useLLM\Agents\Solution\MetadataType;
// In your agent creation method:$aggregate->addMetadata(
newSolutionMetadata(
type: MetadataType::Memory,
key: 'user_preference',
content: 'The user prefers concise answers.',
),
newSolutionMetadata(
type: MetadataType::Prompt,
key: 'check_google',
content: 'Check the status of google.com.',
),
newSolutionMetadata(
type: MetadataType::Prompt,
key: 'check_yahoo',
content: 'Check the status of yahoo.com.',
),
//...
);The package includes a powerful interceptor system for the executor. This allows developers to inject data into prompts, modify execution options, and handle LLM responses at various stages of the execution process. Here's a detailed look at each available interceptor:
useLLM\Agents\AgentExecutor\ExecutorInterface;
useLLM\Agents\AgentExecutor\ExecutorPipeline;
useLLM\Agents\AgentExecutor\Interceptor\GeneratePromptInterceptor;
useLLM\Agents\AgentExecutor\Interceptor\InjectModelInterceptor;
useLLM\Agents\AgentExecutor\Interceptor\InjectOptionsInterceptor;
useLLM\Agents\AgentExecutor\Interceptor\InjectResponseIntoPromptInterceptor;
useLLM\Agents\AgentExecutor\Interceptor\InjectToolsInterceptor;
$executor = newExecutorPipeline(...);
$executor = $executor->withInterceptor(
newGeneratePromptInterceptor(...),
newInjectModelInterceptor(...),
newInjectToolsInterceptor(...),
newInjectOptionsInterceptor(...),
newInjectResponseIntoPromptInterceptor(...),
);
$executor->execute(...);GeneratePromptInterceptor
- Purpose: Generates the initial prompt for the agent.
- Functionality:
- Uses the
AgentPromptGeneratorInterfaceto create a comprehensive prompt. - Incorporates agent instructions, memory, and user input into the prompt.
- Uses the
- When to use: Always include this interceptor to ensure proper prompt generation.
InjectModelInterceptor
- Purpose: Injects the appropriate language model for the agent.
- Functionality:
- Retrieves the model associated with the agent.
- Adds the model information to the execution options.
- When to use: Include this interceptor when you want to ensure the correct model is used for each agent, especially in multi-agent systems.
InjectToolsInterceptor
- Purpose: Adds the agent's tools to the execution options.
- Functionality:
- Retrieves all tools associated with the agent.
- Converts tool schemas into a format understood by the LLM.
- Adds tool information to the execution options.
- When to use: Include this interceptor when your agent uses tools and you want them available during execution.
InjectOptionsInterceptor
- Purpose: Incorporates additional configuration options for the agent.
- Functionality:
- Retrieves any custom configuration options defined for the agent.
- Adds these options to the execution options.
- When to use: Include this interceptor when you have agent-specific configuration that should be applied during execution.
InjectResponseIntoPromptInterceptor
- Purpose: Adds the LLM's response back into the prompt for continuous conversation.
- Functionality:
- Takes the LLM's response from the previous execution.
- Appends this response to the existing prompt.
- When to use: Include this interceptor in conversational agents or when context from previous interactions is important.
You can create custom interceptors to add specialized behavior to your agent execution pipeline.
Here's an example of a custom interceptor that adds time-aware and user-specific context to the prompt:
useLLM\Agents\AgentExecutor\ExecutorInterceptorInterface;
useLLM\Agents\AgentExecutor\ExecutionInput;
useLLM\Agents\AgentExecutor\InterceptorHandler;
useLLM\Agents\Agent\Execution;
useLLM\Agents\LLM\Prompt\Chat\Prompt;
useLLM\Agents\LLM\Response\ChatResponse;
usePsr\Log\LoggerInterface;
class TokenCounterInterceptor implements ExecutorInterceptorInterface
{
publicfunction__construct(
privateTokenCounterInterface$tokenCounter,
privateLoggerInterface$logger,
) {}
publicfunctionexecute(ExecutionInput$input, InterceptorHandler$next): Execution
{
// Count tokens in the input prompt$promptTokens = $this->tokenCounter->count((string) $input->prompt);
// Execute the next interceptor in the chain$execution = $next($input);
// Count tokens in the response$responseTokens = 0;
if ($execution->resultinstanceof ChatResponse) {
$responseTokens = $this->tokenCounter->count((string) $execution->result->content);
}
// Log the token counts$this->logger->info('Token usage', [
'prompt_tokens' => $promptTokens,
'response_tokens' => $responseTokens,
'total_tokens' => $promptTokens + $responseTokens,
]);
return$execution;
}
}Then, you can add your custom interceptor to the executor:
usePsr\Log\LoggerInterface;
// Assume you have implementations of TokenCounterInterface and LoggerInterface$tokenCounter = newMyTokenCounter();
$logger = newMyLogger();
$executor = $executor->withInterceptor(
newTokenCounterInterceptor($tokenCounter, $logger),
);This example demonstrates how to create a more complex and useful interceptor. The token counting interceptor can be valuable for monitoring API usage, optimizing prompt length, or ensuring you stay within token limits of your LLM provider.
You can create various other types of interceptors to suit your specific needs, such as:
- Caching interceptors to store and retrieve responses for identical prompts
- Rate limiting interceptors to control the frequency of API calls
- Error handling interceptors to gracefully manage and log exceptions
- Analytics interceptors to gather data on agent performance and usage patterns
To use the LLM Agents package, you'll need to implement the required interfaces in your project.
It serves as a bridge between your application and LLM you're using, such as OpenAI, Claude, etc.
useLLM\Agents\LLM\ContextInterface;
useLLM\Agents\LLM\LLMInterface;
useLLM\Agents\LLM\OptionsInterface;
useLLM\Agents\LLM\Prompt\Chat\MessagePrompt;
useLLM\Agents\LLM\Prompt\Chat\PromptInterfaceasChatPromptInterface;
useLLM\Agents\LLM\Prompt\PromptInterface;
useLLM\Agents\LLM\Prompt\Tool;
useLLM\Agents\LLM\Response\Response;
useOpenAI\Client;
finalreadonlyclass OpenAILLM implements LLMInterface
{
publicfunction__construct(
privateClient$client,
privateMessageMapper$messageMapper,
privateStreamResponseParser$streamParser,
) {}
publicfunctiongenerate(
ContextInterface$context,
PromptInterface$prompt,
OptionsInterface$options,
): Response {
$request = $this->buildOptions($options);
$messages = $promptinstanceof ChatPromptInterface
? $prompt->format()
: [MessagePrompt::user($prompt)->toChatMessage()];
$request['messages'] = array_map(
fn($message) => $this->messageMapper->map($message),
$messages
);
if ($options->has('tools')) {
$request['tools'] = array_values(array_map(
fn(Tool$tool): array => $this->messageMapper->map($tool),
$options->get('tools')
));
}
$stream = $this->client->chat()->createStreamed($request);
return$this->streamParser->parse($stream);
}
privatefunctionbuildOptions(OptionsInterface$options): array
{
$defaultOptions = [
'temperature' => 0.8,
'max_tokens' => 120,
'model' => null,
// Add other default options as needed
];
$result = array_intersect_key($options->getIterator()->getArrayCopy(), $defaultOptions);
$result += array_diff_key($defaultOptions, $result);
if (!isset($result['model'])) {
thrownew \InvalidArgumentException('Model is required');
}
returnarray_filter($result, fn($value) => $value !== null);
}
}Here is an example of MessageMapper that converts messages to the format required by the LLM API:
useLLM\Agents\LLM\Prompt\Chat\ChatMessage;
useLLM\Agents\LLM\Prompt\Chat\Role;
useLLM\Agents\LLM\Prompt\Chat\ToolCalledPrompt;
useLLM\Agents\LLM\Prompt\Chat\ToolCallResultMessage;
useLLM\Agents\LLM\Prompt\Tool;
useLLM\Agents\LLM\Response\ToolCall;
finalreadonlyclass MessageMapper
{
publicfunctionmap(object$message): array
{
if ($messageinstanceof ChatMessage) {
return [
'content' => $message->content,
'role' => $message->role->value,
];
}
if ($messageinstanceof ToolCallResultMessage) {
return [
'content' => \is_array($message->content) ? \json_encode($message->content) : $message->content,
'tool_call_id' => $message->id,
'role' => $message->role->value,
];
}
if ($messageinstanceof ToolCalledPrompt) {
return [
'content' => null,
'role' => Role::Assistant->value,
'tool_calls' => \array_map(
staticfn(ToolCall$tool): array => [
'id' => $tool->id,
'type' => 'function',
'function' => [
'name' => $tool->name,
'arguments' => $tool->arguments,
],
],
$message->tools,
),
];
}
if ($messageinstanceof Tool) {
return [
'type' => 'function',
'function' => [
'name' => $message->name,
'description' => $message->description,
'parameters' => [
'type' => 'object',
'additionalProperties' => $message->additionalProperties,
] + $message->parameters,
'strict' => $message->strict,
],
];
}
if ($messageinstanceof \JsonSerializable) {
return$message->jsonSerialize();
}
thrownew \InvalidArgumentException('Invalid message type');
}
}It plays a vital role in preparing the context and instructions for an agent before it processes a user's request. It ensures that the agent has all necessary information, including its own instructions, memory, associated agents, and any relevant session context.
- System message with the agent's instruction and important rules.
- System message with the agent's memory (experiences).
- System message about associated agents (if any).
- System message with session context (if provided).
- User message with the actual prompt.
You can customize the prompt generation logic to suit your specific requirements.
Instead of implementing the AgentPromptGeneratorInterface yourself, you can use the llm-agents/prompt-generator
package as an implementation. This package provides a flexible and extensible system for generating chat prompts with
all required system and user messages for LLM agents.
Note: Read full documentation of the
llm-agents/prompt-generatorpackage here
To use it, first install the package:
composer require llm-agents/prompt-generatorThen, set it up in your project. Here's an example using Spiral Framework:
useLLM\Agents\PromptGenerator\Interceptors\AgentMemoryInjector;
useLLM\Agents\PromptGenerator\Interceptors\InstructionGenerator;
useLLM\Agents\PromptGenerator\Interceptors\LinkedAgentsInjector;
useLLM\Agents\PromptGenerator\Interceptors\UserPromptInjector;
useLLM\Agents\PromptGenerator\PromptGeneratorPipeline;
class PromptGeneratorBootloader extends Bootloader
{
publicfunctiondefineSingletons(): array
{
return [
PromptGeneratorPipeline::class => staticfunction (
LinkedAgentsInjector$linkedAgentsInjector,
): PromptGeneratorPipeline {
$pipeline = newPromptGeneratorPipeline();
return$pipeline->withInterceptor(
newInstructionGenerator(),
newAgentMemoryInjector(),
$linkedAgentsInjector,
newUserPromptInjector(),
// Add more interceptors as needed
);
},
];
}
}This class is responsible for handling conversions between JSON schemas and PHP objects.
We provide a schema mapper package that you can use to implement the SchemaMapperInterface in your project. This
package is a super handy JSON Schema Mapper for the LLM Agents project.
To install the package:
composer require llm-agents/json-schema-mapperNote: Read full documentation of the
llm-agents/json-schema-mapperpackage here
It provides a clean way to pass execution-specific data through the system without tightly coupling components or overly complicating method signatures.
useLLM\Agents\LLM\ContextFactoryInterface;
useLLM\Agents\LLM\ContextInterface;
finalclass ContextFactory implements ContextFactoryInterface
{
publicfunctioncreate(): ContextInterface
{
returnnewclassimplements ContextInterface {
// Implement any necessary methods or properties for your context
};
}
}The options is a simple key-value store that allows you to store and retrieve configuration options that can be passed to LLM clients and other components. For example, you can pass a model name, max tokens, and other configuration options to an LLM client.
useLLM\Agents\LLM\OptionsFactoryInterface;
useLLM\Agents\LLM\OptionsInterface;
finalclass OptionsFactory implements OptionsFactoryInterface
{
publicfunctioncreate(): OptionsInterface
{
returnnewclassimplements OptionsInterface {
privatearray$options = [];
publicfunctionhas(string$option): bool
{
returnisset($this->options[$option]);
}
publicfunctionget(string$option, mixed$default = null): mixed
{
return$this->options[$option] ?? $default;
}
publicfunctionwith(string$option, mixed$value): static
{
$clone = clone$this;
$clone->options[$option] = $value;
return$clone;
}
publicfunctiongetIterator(): \Traversable
{
returnnew \ArrayIterator($this->options);
}
};
}
}The LLM Agents package is built around several key components:
- AgentInterface: Defines the contract for all agents.
- AgentAggregate: Implements AgentInterface and aggregates an Agent instance with other Solution objects.
- Agent: Represents a single agent with its key, name, description, and instruction.
- Solution: Abstract base class for various components like Model and ToolLink.
- AgentExecutor: Responsible for executing agents and managing their interactions.
- Tool: Represents a capability that an agent can use to perform tasks.
For a visual representation of the architecture, refer to the class diagram in the documentation.
Here's a class diagram illustrating the key components of the LLM Agents PHP SDK:
classDiagram
class AgentInterface {
<<interface>>
+getKey() string
+getName() string
+getDescription() string
+getInstruction() string
+getTools() array
+getAgents() array
+getModel() Model
+getMemory() array
+getPrompts() array
+getConfiguration() array
}
class AgentAggregate {
-agent: Agent
-associations: array
+addAssociation(Solution)
+addMetadata(SolutionMetadata)
}
class Agent {
+key: string
+name: string
+description: string
+instruction: string
+isActive: bool
}
class Solution {
<<abstract>>
+name: string
+type: SolutionType
+description: string
-metadata: array
+addMetadata(SolutionMetadata)
+getMetadata() array
}
class SolutionMetadata {
+type: MetadataType
+key: string
+content: string|Stringable|int
}
class Model {
+model: string
}
class ToolLink {
+getName() string
}
class AgentLink {
+getName() string
+outputSchema: string
}
class ExecutorInterface {
<<interface>>
+execute(string, string|Stringable|Prompt, ContextInterface, OptionsInterface, PromptContextInterface) Execution
+withInterceptor(ExecutorInterceptorInterface) self
}
class ToolInterface {
<<interface>>
+getName() string
+getDescription() string
+getInputSchema() string
+getLanguage() ToolLanguage
+execute(object) string|Stringable
}
AgentAggregate ..|> AgentInterface
AgentAggregate o-- Agent
AgentAggregate o-- Solution
Agent --|> Solution
Model --|> Solution
ToolLink --|> Solution
AgentLink --|> Solution
ExecutorInterface --> AgentInterface
ExecutorInterface --> ToolInterface
Solution o-- SolutionMetadata
Thank you for considering contributing to the llm-agents-php community! We are open to all kinds of contributions. If you want to:
- 🤔 Suggest a feature
- 🐛 Report an issue
- 📖 Improve documentation
- 👨💻 Contribute to the code
You are more than welcome. Before contributing, kindly check our contribution guidelines.
LLM Agents is open-source software licensed under the MIT license.
