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

v1.0.5 2025-12-20 17:17 UTC

This package is auto-updated.

Last update: 2025-12-20 17:27:07 UTC


README

CI Latest Stable Version Total Downloads License PHP Version

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)

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): ToolResult which wraps your handle() result
  • Automatically converts your return value to a ToolResult::success()
  • Catches exceptions and converts them to ToolResult::failure()
  • Sets requiresApproval() to false by 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.