agentphp / sdk
A comprehensive PHP SDK for building AI conversational agents with support for multiple LLM providers (OpenAI, Anthropic, Google), tools, streaming, and human-in-the-loop workflows
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/agentphp/sdk
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.0
- psr/http-client: ^1.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- illuminate/support: ^10.0 || ^11.0
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^10.0
- symfony/config: ^6.0 || ^7.0
- symfony/dependency-injection: ^6.0 || ^7.0
- symfony/http-kernel: ^6.0 || ^7.0
Suggests
- illuminate/support: Required for Laravel integration (^10.0 || ^11.0)
- symfony/config: Required for Symfony configuration (^6.0 || ^7.0)
- symfony/dependency-injection: Required for Symfony DI (^6.0 || ^7.0)
- symfony/http-kernel: Required for Symfony integration (^6.0 || ^7.0)
README
A comprehensive PHP SDK for building AI conversational agents with support for multiple LLM providers (OpenAI, Anthropic, Google), tools, streaming, and human-in-the-loop workflows.
Features
- Multiple LLM Providers: OpenAI (GPT-4), Anthropic (Claude), Google (Gemini)
- Tool System: Define and execute custom tools with automatic validation
- Streaming: Real-time token streaming for responsive UIs
- Middleware Pipeline: Logging, retry, rate limiting, caching
- Memory Management: Conversation and sliding window memory
- Human-in-the-Loop: Approval workflows for sensitive operations
- Framework Integration: Laravel and Symfony support
Requirements
- PHP 8.0 or higher
- Composer
- Guzzle HTTP client
Installation
composer require agentphp/sdk
Quick Start
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt('You are a helpful assistant.') ->build(); $response = $agent->chat('Hello, how are you?'); echo $response->getContent();
Table of Contents
- Standalone Usage (Legacy PHP)
- Laravel Integration
- Symfony Integration
- Providers
- System Prompts
- Tools
- Streaming
- Middleware
- Human-in-the-Loop
- Error Handling
Standalone Usage (Legacy PHP)
Basic Chat
<?php require 'vendor/autoload.php'; use AgentPHP\Core\AgentBuilder; // Create an agent with OpenAI $agent = AgentBuilder::create() ->withOpenAI('your-openai-api-key', 'gpt-4') ->withSystemPrompt('You are a helpful assistant.') ->withName('MyAgent') ->build(); // Simple chat $response = $agent->chat('What is the capital of France?'); echo $response->getContent(); // "The capital of France is Paris." // Get usage information $usage = $response->getUsage(); echo "Tokens used: " . $usage->getTotalTokens();
Conversation Memory
use AgentPHP\Core\AgentBuilder; // With conversation memory (default) $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withConversationMemory() ->build(); // The agent remembers previous messages $agent->chat('My name is John'); $response = $agent->chat('What is my name?'); echo $response->getContent(); // "Your name is John." // With sliding window memory (limits context size) $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSlidingWindowMemory(4000) // Max 4000 tokens ->build();
Configuration Options
use AgentPHP\Core\AgentBuilder; use AgentPHP\Core\AgentConfig; // Using AgentBuilder (recommended) $agent = AgentBuilder::create() ->withOpenAI('your-api-key', 'gpt-4') ->withName('MyAssistant') ->withSystemPrompt('You are a coding expert.') ->withTemperature(0.7) ->withMaxTokens(2000) ->withMaxIterations(10) ->withTimeout(30) ->withStreaming(false) ->build(); // Using AgentConfig directly $config = AgentConfig::fromArray([ 'name' => 'MyAssistant', 'system_prompt' => 'You are a coding expert.', 'temperature' => 0.7, 'max_tokens' => 2000, 'max_iterations' => 10, 'timeout' => 30, 'streaming' => false, ]); $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withConfig($config) ->build();
Complete Standalone Example
<?php require 'vendor/autoload.php'; use AgentPHP\Core\AgentBuilder; use AgentPHP\Tools\AbstractTool; use AgentPHP\Tools\ToolSchemaBuilder; // Define a custom tool class WeatherTool extends AbstractTool { protected string $name = 'get_weather'; protected string $description = 'Get the current weather for a location'; public function getParameters(): array { return ToolSchemaBuilder::create() ->addString('location', 'City name', required: true) ->build(); } protected function handle(array $arguments) { $location = $arguments['location']; // Simulate weather API return [ 'temperature' => 22, 'condition' => 'sunny' ]; } } // Create agent with tools and middleware $agent = AgentBuilder::create() ->withOpenAI(getenv('OPENAI_API_KEY'), 'gpt-4') ->withSystemPrompt('You are a helpful assistant with weather capabilities.') ->withTool(new WeatherTool()) ->withRetry(3, 1000, 2.0) ->withConversationMemory() ->build(); // Chat $response = $agent->chat('What is the weather in Paris?'); echo $response->getContent();
Laravel Integration
Installation
composer require agentphp/sdk
The service provider is auto-discovered. For manual registration:
// config/app.php 'providers' => [ AgentPHP\Framework\Laravel\AgentServiceProvider::class, ], 'aliases' => [ 'Agent' => AgentPHP\Framework\Laravel\Facades\Agent::class, ],
Publish Configuration
php artisan vendor:publish --tag=agent-config
Configuration
// config/agent.php return [ 'default' => env('AGENT_PROVIDER', 'openai'), 'name' => env('AGENT_NAME', 'AgentPHP'), 'system_prompt' => env('AGENT_SYSTEM_PROMPT', 'You are a helpful assistant.'), 'providers' => [ 'openai' => [ 'api_key' => env('OPENAI_API_KEY'), 'model' => env('OPENAI_MODEL', 'gpt-4'), 'timeout' => 30, ], 'anthropic' => [ 'api_key' => env('ANTHROPIC_API_KEY'), 'model' => env('ANTHROPIC_MODEL', 'claude-3-sonnet-20240229'), ], 'google' => [ 'api_key' => env('GOOGLE_API_KEY'), 'model' => env('GOOGLE_MODEL', 'gemini-pro'), ], ], 'memory' => [ 'driver' => 'conversation', // or 'sliding_window' 'max_tokens' => 4000, ], 'middleware' => [ 'logging' => ['enabled' => true, 'level' => 'debug'], 'retry' => ['enabled' => true, 'max_retries' => 3], 'rate_limit' => ['enabled' => false, 'max_requests' => 60], ], 'human_loop' => [ 'enabled' => false, 'timeout' => 300, ], 'tools' => [ // \App\Tools\SearchTool::class, ], ];
Environment Variables
AGENT_PROVIDER=openai AGENT_NAME=MyAgent OPENAI_API_KEY=your-openai-api-key OPENAI_MODEL=gpt-4
Using the Facade
use AgentPHP\Framework\Laravel\Facades\Agent; // Simple chat $response = Agent::chat('Hello!'); echo $response->getContent(); // With streaming $stream = Agent::chatStream('Tell me a story'); foreach ($stream->getChunks() as $chunk) { echo $chunk->getContent(); } // Get conversation history $history = Agent::getHistory(); // Reset conversation Agent::reset();
Using Dependency Injection
use AgentPHP\Core\Agent; class ChatController extends Controller { public function __construct( private Agent $agent ) {} public function chat(Request $request) { $response = $this->agent->chat($request->input('message')); return response()->json([ 'message' => $response->getContent(), 'usage' => $response->getUsage()->toArray(), ]); } }
Using AgentBuilder in Laravel
use AgentPHP\Core\AgentBuilder; class CustomAgentController extends Controller { public function chat(Request $request) { // Create a custom agent instance $agent = AgentBuilder::create() ->withOpenAI(config('agent.providers.openai.api_key')) ->withSystemPrompt('You are a customer service agent.') ->withTemperature(0.5) ->withTool(new SearchTool()) ->withLogging(app('log')) ->build(); return response()->json([ 'response' => $agent->chat($request->input('message'))->getContent(), ]); } }
Registering Tools in Laravel
You can register tools directly on the Agent instance:
use AgentPHP\Core\Agent; class ChatController extends Controller { public function __construct( private Agent $agent ) { // Register tools directly on the agent $this->agent->addTools([ new \App\Tools\WeatherTool(), new \App\Tools\CalculatorTool(), new \App\Tools\SearchTool(), ]); } public function chat(Request $request) { $response = $this->agent->chat($request->input('message')); return response()->json(['message' => $response->getContent()]); } }
Or register a single tool:
$this->agent->addTool(new WeatherTool());
Alternatively, you can register tools globally in the ServiceProvider (less recommended):
// In AppServiceProvider use AgentPHP\Tools\ToolRegistry; public function boot(ToolRegistry $registry) { $registry->register(new \App\Tools\SearchTool()); }
Laravel Streaming Response
use AgentPHP\Core\Agent; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\StreamedResponse; public function stream(Request $request): StreamedResponse { $message = $request->input('message'); return new StreamedResponse(function () use ($message) { $agent = app(Agent::class); $stream = $agent->chatStream($message); foreach ($stream->getChunks() as $chunk) { if ($chunk->hasContent()) { echo "data: " . json_encode(['content' => $chunk->getContent()]) . "\n\n"; ob_flush(); flush(); } if ($chunk->isDone()) { break; } } echo "data: [DONE]\n\n"; ob_flush(); flush(); }, 200, [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', 'X-Accel-Buffering' => 'no', ]); }
Symfony Integration
Installation
composer require agentphp/sdk
Enable the Bundle
// config/bundles.php return [ // ... AgentPHP\Framework\Symfony\AgentBundle::class => ['all' => true], ];
Configuration
# config/packages/agent.yaml agent: provider: openai name: MyAgent system_prompt: 'You are a helpful assistant.' providers: openai: api_key: '%env(OPENAI_API_KEY)%' model: gpt-4 timeout: 30 anthropic: api_key: '%env(ANTHROPIC_API_KEY)%' model: claude-3-sonnet-20240229 google: api_key: '%env(GOOGLE_API_KEY)%' model: gemini-pro memory: driver: conversation # or sliding_window max_tokens: 4000 max_iterations: 10 temperature: 0.7 max_tokens: ~ # null for provider default timeout: 30 streaming: false human_loop: enabled: false timeout: 300 auto_approve: false middleware: logging: enabled: true level: debug retry: enabled: true max_retries: 3 delay: 1000 multiplier: 2.0 rate_limit: enabled: false max_requests: 60 window: 60 cache: enabled: false ttl: 3600 tools: []
Environment Variables
OPENAI_API_KEY=your-openai-api-key ANTHROPIC_API_KEY=your-anthropic-api-key
Using in Controllers
use AgentPHP\Core\Agent; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; class ChatController extends AbstractController { public function __construct( private Agent $agent ) {} public function chat(Request $request): JsonResponse { $message = $request->request->get('message'); $response = $this->agent->chat($message); return $this->json([ 'message' => $response->getContent(), 'usage' => $response->getUsage()->toArray(), ]); } }
Symfony Streaming Response
use AgentPHP\Core\Agent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; public function stream(Request $request): StreamedResponse { $message = $request->request->get('message'); return new StreamedResponse(function () use ($message) { $stream = $this->agent->chatStream($message); foreach ($stream->getChunks() as $chunk) { if ($chunk->hasContent()) { echo "data: " . json_encode(['content' => $chunk->getContent()]) . "\n\n"; ob_flush(); flush(); } if ($chunk->isDone()) { break; } } echo "data: [DONE]\n\n"; ob_flush(); flush(); }, 200, [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', 'X-Accel-Buffering' => 'no', ]); }
Using AgentBuilder in Symfony
use AgentPHP\Core\AgentBuilder; class CustomAgentService { public function createSpecializedAgent(): Agent { return AgentBuilder::create() ->withAnthropic($_ENV['ANTHROPIC_API_KEY'], 'claude-3-opus-20240229') ->withSystemPrompt('You are an expert programmer.') ->withTemperature(0.3) ->withMaxTokens(4000) ->build(); } }
Registering Tools as Services
# config/services.yaml services: App\Tools\SearchTool: tags: ['agent.tool'] App\Tools\CalculatorTool: tags: ['agent.tool']
Providers
OpenAI
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withOpenAI( apiKey: 'your-api-key', model: 'gpt-4', // or gpt-4-turbo, gpt-3.5-turbo baseUrl: null, // Custom base URL (optional) organization: null // Organization ID (optional) ) ->build(); // Or with ProviderConfig use AgentPHP\Providers\OpenAIProvider; use AgentPHP\Providers\ProviderConfig; $config = new ProviderConfig([ 'api_key' => 'your-api-key', 'model' => 'gpt-4-turbo', 'base_url' => 'https://api.openai.com/v1', 'organization' => 'org-xxx', 'timeout' => 30, ]); $provider = new OpenAIProvider($config); $agent = AgentBuilder::create() ->withCustomProvider($provider) ->build();
Anthropic (Claude)
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withAnthropic( apiKey: 'your-api-key', model: 'claude-3-opus-20240229' // or claude-3-sonnet, claude-3-haiku ) ->build(); // Available models: // - claude-3-opus-20240229 (most capable) // - claude-3-sonnet-20240229 (balanced) // - claude-3-haiku-20240307 (fastest)
Google (Gemini)
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withGoogle( apiKey: 'your-api-key', model: 'gemini-pro' // or gemini-pro-vision ) ->build();
Custom Provider
use AgentPHP\Contracts\ProviderInterface; use AgentPHP\Core\AgentBuilder; use AgentPHP\Core\ChatResponse; class MyCustomProvider implements ProviderInterface { public function getName(): string { return 'custom'; } public function chat(array $messages, array $options = []): ChatResponse { // Your implementation } public function chatStream(array $messages, callable $callback, array $options = []): ChatResponse { // Your streaming implementation } // ... other interface methods } $agent = AgentBuilder::create() ->withCustomProvider(new MyCustomProvider()) ->build();
Provider Factory
use AgentPHP\Providers\ProviderFactory; // Create provider by name $provider = ProviderFactory::create('openai', [ 'api_key' => 'your-key', 'model' => 'gpt-4', ]); $provider = ProviderFactory::create('anthropic', [ 'api_key' => 'your-key', 'model' => 'claude-3-sonnet-20240229', ]);
System Prompts
System prompts define the behavior and personality of your agent.
Basic System Prompt
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt('You are a helpful assistant that speaks formally.') ->build();
Detailed System Prompt
$systemPrompt = <<<PROMPT You are an expert customer service agent for TechCorp. Your responsibilities: - Answer questions about our products - Help troubleshoot technical issues - Process refund requests when appropriate Guidelines: - Always be polite and professional - If you don't know something, say so - Never share confidential information - Escalate to a human agent for complex issues Available products: - TechWidget Pro (\$99) - TechGadget Plus (\$149) - TechService Premium (monthly subscription) PROMPT; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt($systemPrompt) ->build();
Dynamic System Prompts
$userName = 'John'; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt("You are helping user: {$userName}") ->build(); // Or update at runtime $agent->setSystemPrompt("New context: User is a premium member.");
Multi-Language Support
$userLanguage = 'es'; $systemPrompt = match($userLanguage) { 'es' => 'Eres un asistente util. Responde siempre en espanol.', 'fr' => 'Vous etes un assistant utile. Repondez toujours en francais.', default => 'You are a helpful assistant. Always respond in English.', }; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt($systemPrompt) ->build();
Role-Based System Prompts
function getSystemPromptForRole(string $role): string { return match($role) { 'developer' => 'You are a senior software developer. Provide code examples and technical explanations.', 'teacher' => 'You are a patient teacher. Explain concepts simply and use analogies.', 'analyst' => 'You are a data analyst. Focus on facts, statistics, and data-driven insights.', default => 'You are a helpful assistant.', }; } $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt(getSystemPromptForRole('developer')) ->build();
Tools
Tools allow your agent to perform actions and access external data.
Creating a Tool
Extend AbstractTool and implement the handle() method:
use AgentPHP\Tools\AbstractTool; use AgentPHP\Tools\ToolSchemaBuilder; class WeatherTool extends AbstractTool { protected string $name = 'get_weather'; protected string $description = 'Get the current weather for a location'; public function getParameters(): array { return ToolSchemaBuilder::create() ->addString('location', 'The city and country', required: true) ->addEnum('unit', 'Temperature unit', ['celsius', 'fahrenheit'], required: false) ->build(); } /** * Handle the tool execution. * * @param array $arguments The validated arguments from the LLM * @return mixed The result (will be wrapped in ToolResult automatically) */ protected function handle(array $arguments) { $location = $arguments['location']; $unit = $arguments['unit'] ?? 'celsius'; // Call weather API $weather = $this->fetchWeather($location, $unit); return [ 'temperature' => $weather['temp'], 'condition' => $weather['condition'], 'humidity' => $weather['humidity'], ]; } private function fetchWeather(string $location, string $unit): array { // Your API implementation return [ 'temp' => 22, 'condition' => 'sunny', 'humidity' => 45, ]; } }
The AbstractTool base class:
- Provides
execute(array $arguments): ToolResultwhich wraps yourhandle()result - Automatically converts your return value to a
ToolResult::success() - Catches exceptions and converts them to
ToolResult::failure() - Sets
requiresApproval()tofalseby default (override if needed)
Registering Tools
use AgentPHP\Core\AgentBuilder; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withTool(new WeatherTool()) ->withTool(new SearchTool()) ->withTool(new CalculatorTool()) ->build(); // Or register multiple at once $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withTools([ new WeatherTool(), new SearchTool(), new CalculatorTool(), ]) ->build();
Tool Schema Builder
use AgentPHP\Tools\ToolSchemaBuilder; $schema = ToolSchemaBuilder::create() // String with validation ->addString('email', 'User email address', required: true) // Enum (select from options) ->addEnum('priority', 'Task priority', ['low', 'medium', 'high']) // Integer with range ->addInteger('age', 'User age', required: true) // Number (float) ->addNumber('price', 'Product price') // Boolean ->addBoolean('active', 'Is active') // Array of strings ->addArray('tags', 'List of tags', itemType: 'string') // Nested object ->addObject('address', 'Shipping address', [ 'street' => ['type' => 'string', 'description' => 'Street address'], 'city' => ['type' => 'string', 'description' => 'City'], 'zip' => ['type' => 'string', 'description' => 'ZIP code'], ]) ->build();
Tool with Approval Required
For sensitive operations, set $requiresApproval = true:
class DeleteUserTool extends AbstractTool { protected string $name = 'delete_user'; protected string $description = 'Permanently delete a user account'; protected bool $requiresApproval = true; // Requires human approval public function getParameters(): array { return ToolSchemaBuilder::create() ->addString('user_id', 'The user ID to delete', required: true) ->addString('reason', 'Reason for deletion', required: true) ->build(); } protected function handle(array $arguments) { $userId = $arguments['user_id']; // Delete user logic $this->userService->delete($userId); return [ 'deleted' => true, 'user_id' => $userId, ]; } }
Using Tools with Agent
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withSystemPrompt('You can help users check the weather.') ->withTool(new WeatherTool()) ->build(); // The agent will automatically use the tool when appropriate $response = $agent->chat('What is the weather in Paris?'); echo $response->getContent(); // "The current weather in Paris is 22C and sunny with 45% humidity." // Check if tools were used if ($response->hasToolCalls()) { foreach ($response->getToolCalls() as $call) { echo "Tool used: " . $call->getName(); } }
Tool Registry
use AgentPHP\Tools\ToolRegistry; $registry = new ToolRegistry(); // Register tools $registry->register(new WeatherTool()); $registry->register(new SearchTool()); // Check if tool exists if ($registry->has('get_weather')) { $tool = $registry->get('get_weather'); } // Get all tool names $names = $registry->names(); // ['get_weather', 'search'] // Get tools requiring approval $sensitiveTools = $registry->getRequiringApproval(); // Filter tools $readOnlyTools = $registry->filter(fn($tool) => !$tool->requiresApproval());
Manual Tool Execution
use AgentPHP\Tools\ToolExecutor; $executor = new ToolExecutor($registry); // Execute a specific tool $result = $executor->executeByName('get_weather', [ 'location' => 'Tokyo', 'unit' => 'celsius', ]); if ($result->isSuccess()) { echo $result->getResultAsString(); } else { echo "Error: " . $result->getError(); }
Streaming
Streaming allows you to receive tokens in real-time as they are generated. All providers (OpenAI, Anthropic, Google) support streaming.
How Streaming Works
When you call $agent->chatStream(), it returns a StreamInterface object. This object provides methods to iterate over chunks of text as they arrive from the LLM provider, instead of waiting for the complete response.
Legacy PHP - Terminal/CLI Streaming
Complete example for running in terminal:
<?php // File: cli_stream.php require 'vendor/autoload.php'; use AgentPHP\Core\AgentBuilder; // Create agent $agent = AgentBuilder::create() ->withOpenAI(getenv('OPENAI_API_KEY'), 'gpt-4') ->withSystemPrompt('You are a helpful assistant.') ->build(); echo "Assistant: "; // Get the stream $stream = $agent->chatStream('Tell me a short story about a robot'); // Process each chunk as it arrives foreach ($stream->getChunks() as $chunk) { // Check if chunk has text content if ($chunk->hasContent()) { echo $chunk->getContent(); // Force output to display immediately if (function_exists('ob_flush')) { @ob_flush(); } flush(); } } echo "\n\n--- Stream complete ---\n";
Run it:
php cli_stream.php
Legacy PHP - Web Browser SSE Streaming
Complete example for streaming to a web browser using Server-Sent Events (SSE):
Backend (stream.php):
<?php // File: stream.php require 'vendor/autoload.php'; use AgentPHP\Core\AgentBuilder; // Disable output buffering if (ob_get_level()) { ob_end_clean(); } // Set SSE headers header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); // Required for nginx // Get message from request $message = $_GET['message'] ?? $_POST['message'] ?? 'Hello'; // Create agent $agent = AgentBuilder::create() ->withOpenAI(getenv('OPENAI_API_KEY'), 'gpt-4') ->withSystemPrompt('You are a helpful assistant.') ->build(); try { $stream = $agent->chatStream($message); foreach ($stream->getChunks() as $chunk) { if ($chunk->hasContent()) { // Send SSE data echo "data: " . json_encode([ 'type' => 'content', 'content' => $chunk->getContent() ]) . "\n\n"; flush(); } // Check for errors if ($chunk->isError()) { echo "data: " . json_encode([ 'type' => 'error', 'message' => $chunk->getContent() ]) . "\n\n"; flush(); break; } } // Send completion signal echo "data: " . json_encode(['type' => 'done']) . "\n\n"; flush(); } catch (\Exception $e) { echo "data: " . json_encode([ 'type' => 'error', 'message' => $e->getMessage() ]) . "\n\n"; flush(); }
Frontend (index.html):
<!DOCTYPE html> <html> <head> <title>Chat Streaming</title> <style> #chat { max-width: 600px; margin: 20px auto; } #response { min-height: 200px; border: 1px solid #ccc; padding: 15px; white-space: pre-wrap; font-family: sans-serif; } #message { width: 80%; padding: 10px; } button { padding: 10px 20px; } </style> </head> <body> <div id="chat"> <h2>Chat with AI</h2> <input type="text" id="message" placeholder="Type your message..."> <button onclick="sendMessage()">Send</button> <h3>Response:</h3> <div id="response"></div> </div> <script> let eventSource = null; function sendMessage() { const message = document.getElementById('message').value; const responseDiv = document.getElementById('response'); // Clear previous response responseDiv.textContent = ''; // Close existing connection if (eventSource) { eventSource.close(); } // Create new SSE connection eventSource = new EventSource('stream.php?message=' + encodeURIComponent(message)); eventSource.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === 'content') { responseDiv.textContent += data.content; } else if (data.type === 'done') { eventSource.close(); console.log('Stream complete'); } else if (data.type === 'error') { responseDiv.textContent += '\n\nError: ' + data.message; eventSource.close(); } }; eventSource.onerror = function() { eventSource.close(); console.log('Connection closed'); }; } </script> </body> </html>
Legacy PHP - AJAX POST Streaming
For POST requests with the Fetch API:
Backend (api_stream.php):
<?php // File: api_stream.php require 'vendor/autoload.php'; use AgentPHP\Core\AgentBuilder; // Handle CORS if needed header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Content-Type'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); } // Disable output buffering while (ob_get_level()) { ob_end_clean(); } // Set SSE headers header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); // Get JSON body $input = json_decode(file_get_contents('php://input'), true); $message = $input['message'] ?? 'Hello'; $agent = AgentBuilder::create() ->withOpenAI(getenv('OPENAI_API_KEY'), 'gpt-4') ->build(); $stream = $agent->chatStream($message); foreach ($stream->getChunks() as $chunk) { if ($chunk->hasContent()) { echo "data: " . json_encode(['content' => $chunk->getContent()]) . "\n\n"; flush(); } } echo "data: [DONE]\n\n"; flush();
Frontend JavaScript (Fetch API):
async function streamChat(message) { const responseDiv = document.getElementById('response'); responseDiv.textContent = ''; const response = await fetch('api_stream.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); const lines = text.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.substring(6).trim(); if (data === '[DONE]') { console.log('Stream complete'); return; } try { const parsed = JSON.parse(data); if (parsed.content) { responseDiv.textContent += parsed.content; } } catch (e) { // Skip invalid JSON } } } } }
StreamChunk Methods
Each chunk in the stream provides these methods:
foreach ($stream->getChunks() as $chunk) { // Get text content $text = $chunk->getContent(); $text = $chunk->getText(); // Alias // Check what type of chunk this is $chunk->hasContent(); // Has text content? $chunk->isContent(); // Is a content chunk? $chunk->isDone(); // Is the final chunk? $chunk->isError(); // Is an error chunk? $chunk->isFinal(); // Is done, stop, or error? // For tool calls during streaming $chunk->hasToolCall(); // Has complete tool call? $chunk->hasToolCallDelta(); // Has partial tool call data? $chunk->getToolCall(); // Get ToolCall object $chunk->getToolCallDelta(); // Get partial tool call array // Metadata $chunk->getType(); // 'content', 'tool_call', 'done', 'error', etc. $chunk->getIndex(); // Chunk index number $chunk->getFinishReason(); // 'stop', 'length', 'tool_calls', etc. $chunk->getModel(); // Model name (usually in first/last chunk) $chunk->getUsage(); // Usage stats (usually in last chunk) }
Getting the Complete Response After Streaming
$stream = $agent->chatStream('Tell me about PHP'); $fullText = ''; // Collect all chunks foreach ($stream->getChunks() as $chunk) { if ($chunk->hasContent()) { $content = $chunk->getContent(); echo $content; // Stream to output $fullText .= $content; // Collect for later } } // After streaming, you have the complete text echo "\n\nComplete response length: " . strlen($fullText) . " characters\n";
Streaming with Different Providers
All providers use the same streaming interface:
// OpenAI $agent = AgentBuilder::create() ->withOpenAI(getenv('OPENAI_API_KEY'), 'gpt-4') ->build(); // Anthropic $agent = AgentBuilder::create() ->withAnthropic(getenv('ANTHROPIC_API_KEY'), 'claude-3-sonnet-20240229') ->build(); // Google $agent = AgentBuilder::create() ->withGoogle(getenv('GOOGLE_API_KEY'), 'gemini-pro') ->build(); // Same streaming code works for all providers $stream = $agent->chatStream('Hello!'); foreach ($stream->getChunks() as $chunk) { echo $chunk->getContent(); }
Error Handling in Streams
try { $stream = $agent->chatStream('Tell me a story'); foreach ($stream->getChunks() as $chunk) { if ($chunk->isError()) { throw new \Exception('Stream error: ' . $chunk->getContent()); } echo $chunk->getContent(); } } catch (\AgentPHP\Exceptions\ProviderException $e) { echo "Provider error: " . $e->getMessage(); } catch (\AgentPHP\Exceptions\RateLimitException $e) { echo "Rate limited. Retry after: " . $e->getRetryAfter() . " seconds"; } catch (\Exception $e) { echo "Error: " . $e->getMessage(); }
Nginx Configuration for SSE
If using nginx, add this to your location block:
location /stream.php { proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; # If using FastCGI fastcgi_buffering off; fastcgi_keep_conn on; }
Apache Configuration for SSE
For Apache with mod_php, streaming should work out of the box. If using PHP-FPM:
<Location /stream.php> SetEnv no-gzip 1 SetEnv proxy-nokeepalive 1 </Location>
Middleware
Middleware allows you to intercept and modify requests/responses.
Built-in Middleware
Logging Middleware
use AgentPHP\Core\AgentBuilder; use Psr\Log\LoggerInterface; $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withLogging($logger, 'info') // PSR-3 logger ->build(); // Or with Laravel $agent = AgentBuilder::create() ->withOpenAI(config('agent.providers.openai.api_key')) ->withLogging(app('log'), 'debug') ->build();
Retry Middleware
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withRetry( maxRetries: 3, // Number of retry attempts delay: 1000, // Initial delay in milliseconds multiplier: 2.0 // Exponential backoff multiplier ) ->build();
Rate Limit Middleware
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withRateLimit( maxRequests: 60, // Maximum requests window: 60 // Time window in seconds ) ->build();
Timeout Middleware
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withTimeout(30) // 30 seconds timeout ->build();
Combining Middleware
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withLogging($logger) ->withRetry(3, 1000, 2.0) ->withRateLimit(60, 60) ->withTimeout(30) ->build();
Custom Middleware
use AgentPHP\Contracts\MiddlewareInterface; use AgentPHP\Core\ChatRequest; use AgentPHP\Core\ChatResponse; class MetricsMiddleware implements MiddlewareInterface { public function __construct( private MetricsService $metrics ) {} public function process(ChatRequest $request, callable $next): ChatResponse { $startTime = microtime(true); try { // Call next middleware/handler $response = $next($request); // Record success metrics $duration = microtime(true) - $startTime; $this->metrics->record('chat.success', $duration); $this->metrics->increment('chat.tokens', $response->getUsage()->getTotalTokens()); return $response; } catch (\Exception $e) { // Record error metrics $this->metrics->increment('chat.errors'); throw $e; } } } // Use the middleware $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withMiddleware(new MetricsMiddleware($metricsService)) ->build();
Authentication Middleware
class AuthenticationMiddleware implements MiddlewareInterface { public function process(ChatRequest $request, callable $next): ChatResponse { // Check if user is authenticated if (!$this->auth->check()) { throw new UnauthorizedException('User must be authenticated'); } // Check rate limits for user $userId = $this->auth->id(); if ($this->rateLimiter->tooManyAttempts($userId, 100)) { throw new RateLimitException('Too many requests'); } $this->rateLimiter->hit($userId); return $next($request); } }
Content Filter Middleware
class ContentFilterMiddleware implements MiddlewareInterface { private array $blockedWords = ['spam', 'inappropriate']; public function process(ChatRequest $request, callable $next): ChatResponse { // Filter input $messages = $request->getMessages(); foreach ($messages as $message) { if ($this->containsBlockedContent($message['content'] ?? '')) { throw new ContentPolicyException('Message contains blocked content'); } } $response = $next($request); // Filter output $content = $response->getContent(); if ($this->containsBlockedContent($content)) { throw new ContentPolicyException('Response contains blocked content'); } return $response; } private function containsBlockedContent(string $text): bool { foreach ($this->blockedWords as $word) { if (stripos($text, $word) !== false) { return true; } } return false; } }
Human-in-the-Loop
Enable approval workflows for sensitive operations.
Basic Setup
use AgentPHP\Core\AgentBuilder; use AgentPHP\HumanLoop\ApprovalHandler; $approvalHandler = ApprovalHandler::create(); $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withHumanApproval($approvalHandler) ->withTool(new DeleteUserTool()) // requiresApproval() returns true ->build();
Custom Approval Handler
use AgentPHP\Contracts\HumanLoopInterface; use AgentPHP\Tools\ToolCall; class SlackApprovalHandler implements HumanLoopInterface { public function requestApproval(ToolCall $call): bool { // Send Slack notification $this->slack->send([ 'channel' => '#approvals', 'text' => "Approval needed for: {$call->getName()}", 'data' => $call->getArguments(), ]); // Wait for response (implement your logic) return $this->waitForApproval($call->getId()); } public function onApproved(ToolCall $call): void { $this->slack->send([ 'channel' => '#approvals', 'text' => "Approved: {$call->getName()}", ]); } public function onDenied(ToolCall $call, string $reason): void { $this->slack->send([ 'channel' => '#approvals', 'text' => "Denied: {$call->getName()} - {$reason}", ]); } }
Auto-Approval for Development
$agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->withAutoApproval() // Skip approval in development ->build(); // Or skip approval for specific tools $agent = AgentBuilder::create() ->withOpenAI('your-api-key') ->skipApproval() // Skip all approvals ->build();
Error Handling
use AgentPHP\Exceptions\AgentException; use AgentPHP\Exceptions\ProviderException; use AgentPHP\Exceptions\RateLimitException; use AgentPHP\Exceptions\TimeoutException; use AgentPHP\Exceptions\ToolException; use AgentPHP\Exceptions\ValidationException; try { $response = $agent->chat('Hello'); } catch (RateLimitException $e) { // Handle rate limiting $retryAfter = $e->getRetryAfter(); sleep($retryAfter); } catch (TimeoutException $e) { // Handle timeout echo "Request timed out after {$e->getTimeout()} seconds"; } catch (ToolException $e) { // Handle tool errors echo "Tool '{$e->getToolName()}' failed: {$e->getMessage()}"; } catch (ValidationException $e) { // Handle validation errors foreach ($e->getErrors() as $field => $error) { echo "{$field}: {$error}"; } } catch (ProviderException $e) { // Handle provider-specific errors echo "Provider error: {$e->getMessage()}"; } catch (AgentException $e) { // Handle all other agent errors echo "Agent error: {$e->getMessage()}"; }
Development
Setup
# Clone the repository git clone https://github.com/agentphp/sdk.git cd sdk # Install dependencies composer install # Run tests composer test # Run code style check composer cs:check # Run static analysis composer analyse
Testing
# Run all tests composer test # Run unit tests only composer test:unit # Run integration tests only composer test:integration # Run tests with coverage composer test:coverage
Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
The MIT License (MIT). Please see License File for more information.
Security
If you discover any security-related issues, please email security@agentphp.dev instead of using the issue tracker.