Skip to content

jayjfletcher/kaleidoscope

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kaleidoscope

Agent-based AI workflows for Laravel with streaming support, built on Prism PHP.

Features

  • Streaming Agent Execution - Real-time token-by-token responses
  • Tool/Function Calling - Agents can invoke tools during execution
  • Agent Handoffs - Agents can delegate to other specialized agents with nested streaming
  • Guardrails - Input/output validation before and after execution
  • Tracing - Database persistence of execution traces with a web UI

Requirements

Installation

composer require kaleidoscope/kaleidoscope

Publish the configuration:

php artisan vendor:publish --tag=kaleidoscope-config

Run the migrations:

php artisan migrate

Quick Start

Creating an Agent

use Kaleidoscope\Agent;

$agent = Agent::as('assistant')
    ->withInstructions('You are a helpful assistant that answers questions concisely.')
    ->using('anthropic', 'claude-sonnet-4-20250514')
    ->withMaxTokens(4096);

Running an Agent (Non-Streaming)

use Kaleidoscope\Facades\Kaleidoscope;

$result = Kaleidoscope::run($agent, 'What is the capital of France?');

echo $result->getOutput(); // "Paris is the capital of France."

Streaming an Agent

foreach (Kaleidoscope::stream($agent, 'Tell me a story') as $event) {
    match ($event->type) {
        'text_delta' => echo $event->delta,
        'tool_call' => handleToolCall($event),
        'tool_result' => handleToolResult($event),
        'stream_end' => break,
        default => null,
    };
}

SSE Response (for HTTP endpoints)

Route::get('/chat', function () {
    $agent = Agent::as('assistant')
        ->withInstructions('You are helpful.')
        ->using('anthropic', 'claude-sonnet-4-20250514');

    return Kaleidoscope::streamResponse($agent, request('message'));
});

JavaScript client:

const eventSource = new EventSource('/chat?message=Hello');

eventSource.addEventListener('text_delta', (event) => {
    const data = JSON.parse(event.data);
    document.getElementById('output').textContent += data.delta;
});

eventSource.addEventListener('stream_end', () => {
    eventSource.close();
});

Tools

Add tools to give your agent capabilities:

use Prism\Prism\Tool;

$calculator = Tool::as('calculator')
    ->for('Perform mathematical calculations')
    ->withStringParameter('expression', 'The math expression to evaluate')
    ->using(function (string $expression): string {
        return (string) eval("return {$expression};");
    });

$agent = Agent::as('math-assistant')
    ->withInstructions('You help with math problems. Use the calculator tool.')
    ->using('anthropic', 'claude-sonnet-4-20250514')
    ->withTools([$calculator]);

Agent Handoffs

Create specialized agents that can hand off to each other:

$researcher = Agent::as('researcher')
    ->withInstructions('You research topics thoroughly.')
    ->withHandoffDescription('Delegate research tasks to this agent')
    ->using('anthropic', 'claude-sonnet-4-20250514');

$writer = Agent::as('writer')
    ->withInstructions('You write engaging content based on research.')
    ->withHandoffDescription('Delegate writing tasks to this agent')
    ->using('anthropic', 'claude-sonnet-4-20250514');

$coordinator = Agent::as('coordinator')
    ->withInstructions('You coordinate between the researcher and writer.')
    ->using('anthropic', 'claude-sonnet-4-20250514')
    ->withTools([$researcher, $writer]); // Agents as tools!

// The coordinator can now delegate to researcher or writer as needed
$result = Kaleidoscope::run($coordinator, 'Write a blog post about AI agents');

When streaming, handoffs stream through the parent agent for a seamless experience:

foreach (Kaleidoscope::stream($coordinator, 'Write a blog post') as $event) {
    if ($event->type === 'agent_handoff') {
        echo "Handing off to: {$event->toAgent}\n";
    }
    // All text_delta events from sub-agents stream through
}

Guardrails

Guardrails provide input and output validation for agents. They run before the agent processes input (input guardrails) and after the agent generates output (output guardrails), allowing you to enforce safety policies, content filtering, and business rules.

Creating a Guardrail

Implement the Guardrail interface:

use Kaleidoscope\Contracts\Guardrail;
use Kaleidoscope\AgentContext;
use Kaleidoscope\GuardrailResult;

class ContentFilter implements Guardrail
{
    public function getName(): string
    {
        return 'content_filter';
    }

    public function validate(mixed $input, AgentContext $context): GuardrailResult
    {
        $blockedWords = ['inappropriate', 'harmful'];

        foreach ($blockedWords as $word) {
            if (str_contains(strtolower($input), $word)) {
                return GuardrailResult::fail("Content contains blocked word: {$word}");
            }
        }

        return GuardrailResult::pass();
    }
}

Attaching Guardrails to Agents

Use withInputGuardrails() and withOutputGuardrails() to attach guardrails:

$agent = Agent::as('safe-assistant')
    ->withInputGuardrails([new ContentFilter()])
    ->withOutputGuardrails([new ContentFilter()]);

GuardrailResult

Guardrails return a GuardrailResult to indicate pass or fail:

// Passing validation
return GuardrailResult::pass();

// Passing with an optional message
return GuardrailResult::pass('Content is appropriate');

// Failing validation
return GuardrailResult::fail('Content violates policy');

// With metadata for logging/debugging
return GuardrailResult::fail('Rate limit exceeded', [
    'current_rate' => 150,
    'max_rate' => 100,
]);

Accessing Context in Guardrails

Guardrails receive the AgentContext, allowing validation based on user data or session state:

class RateLimitGuardrail implements Guardrail
{
    public function getName(): string
    {
        return 'rate_limit';
    }

    public function validate(mixed $input, AgentContext $context): GuardrailResult
    {
        $userId = $context->get('user_id');
        $requestCount = Cache::get("requests:{$userId}", 0);

        if ($requestCount >= 100) {
            return GuardrailResult::fail('Rate limit exceeded');
        }

        Cache::increment("requests:{$userId}");

        return GuardrailResult::pass();
    }
}

// Pass context when running the agent
$context = AgentContext::make('session')
    ->set('user_id', auth()->id());

$result = Kaleidoscope::run($agent, $input, $context);

Multiple Guardrails

Multiple guardrails are checked in order. Execution stops at the first failure:

$agent = Agent::as('secure-assistant')
    ->withInputGuardrails([
        new RateLimitGuardrail(),
        new ContentFilter(),
        new ProfanityFilter(),
    ])
    ->withOutputGuardrails([
        new PIIRedactionGuardrail(),
        new ToxicityFilter(),
    ]);

Handling Guardrail Failures

When a guardrail fails, the result contains the error:

$result = Kaleidoscope::run($agent, 'blocked content');

if (! $result->isSuccess()) {
    // Error message includes guardrail name and failure reason
    echo $result->getError();
    // "Guardrail 'content_filter' failed: Content contains blocked word: blocked"
}

For more control, catch the GuardrailException:

use Kaleidoscope\Exceptions\GuardrailException;

try {
    $result = Kaleidoscope::run($agent, $input);
} catch (GuardrailException $e) {
    $guardrailName = $e->getGuardrailName();
    $failureResult = $e->getResult();

    Log::warning("Guardrail {$guardrailName} blocked request", [
        'message' => $failureResult->getMessage(),
        'metadata' => $failureResult->getMetadata(),
    ]);
}

Guardrails with Streaming

Guardrails work with streaming. Input guardrails run before streaming starts; if they fail, an error event is emitted:

foreach (Kaleidoscope::stream($agent, $input) as $event) {
    if ($event->type === 'error') {
        // Guardrail failure appears as an error event
        echo "Blocked: {$event->error}";
        break;
    }

    if ($event->type === 'text_delta') {
        echo $event->delta;
    }
}

Common Guardrail Patterns

PII Detection:

class PIIGuardrail implements Guardrail
{
    public function getName(): string
    {
        return 'pii_detection';
    }

    public function validate(mixed $input, AgentContext $context): GuardrailResult
    {
        // Check for SSN pattern
        if (preg_match('/\b\d{3}-\d{2}-\d{4}\b/', $input)) {
            return GuardrailResult::fail('Input contains SSN');
        }

        // Check for credit card pattern
        if (preg_match('/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/', $input)) {
            return GuardrailResult::fail('Input contains credit card number');
        }

        return GuardrailResult::pass();
    }
}

Length Validation:

class MaxLengthGuardrail implements Guardrail
{
    public function __construct(private int $maxLength = 10000) {}

    public function getName(): string
    {
        return 'max_length';
    }

    public function validate(mixed $input, AgentContext $context): GuardrailResult
    {
        if (strlen($input) > $this->maxLength) {
            return GuardrailResult::fail(
                "Input exceeds maximum length of {$this->maxLength} characters"
            );
        }

        return GuardrailResult::pass();
    }
}

Topic Restriction:

class TopicGuardrail implements Guardrail
{
    public function __construct(private array $blockedTopics) {}

    public function getName(): string
    {
        return 'topic_restriction';
    }

    public function validate(mixed $input, AgentContext $context): GuardrailResult
    {
        $lowerInput = strtolower($input);

        foreach ($this->blockedTopics as $topic) {
            if (str_contains($lowerInput, strtolower($topic))) {
                return GuardrailResult::fail("Discussion of '{$topic}' is not permitted");
            }
        }

        return GuardrailResult::pass();
    }
}

$agent = Agent::as('assistant')
    ->withInputGuardrails([
        new TopicGuardrail(['politics', 'religion']),
    ]);

Context

Share data between agent executions:

use Kaleidoscope\AgentContext;

$context = AgentContext::make('user-session')
    ->set('user_id', 123)
    ->set('preferences', ['language' => 'en']);

$result = Kaleidoscope::run($agent, 'What are my settings?', $context);

// Child contexts inherit from parents
$childContext = $context->createChild(['additional' => 'data']);

Tracing

All agent executions are automatically traced when enabled. View traces at:

/kaleidoscope

Configure tracing in config/kaleidoscope.php:

'tracing' => [
    'enabled' => env('KALEIDOSCOPE_TRACING_ENABLED', true),
    'connection' => env('KALEIDOSCOPE_TRACING_CONNECTION', null),
    'retention_days' => env('KALEIDOSCOPE_TRACING_RETENTION', 30),
],

'ui' => [
    'enabled' => env('KALEIDOSCOPE_UI_ENABLED', true),
    'path' => env('KALEIDOSCOPE_UI_PATH', 'kaleidoscope'),
    'middleware' => ['web'],
],

Stream Events

When streaming, you'll receive these event types:

Event Description
stream_start Stream has started, includes agent/model info
text_delta A chunk of generated text
tool_call Agent is calling a tool
tool_result Tool has returned a result
agent_handoff Agent is handing off to another agent
error An error occurred
stream_end Stream has ended, includes usage stats

Configuration

// config/kaleidoscope.php

return [
    'default_provider' => env('KALEIDOSCOPE_PROVIDER', 'anthropic'),
    'default_model' => env('KALEIDOSCOPE_MODEL', 'claude-sonnet-4-20250514'),

    'defaults' => [
        'max_tokens' => 4096,
        'temperature' => 1.0,
        'max_steps' => 10, // Max tool call iterations
    ],

    'tracing' => [
        'enabled' => true,
        'connection' => null, // Use default database
        'retention_days' => 30,
    ],

    'ui' => [
        'enabled' => true,
        'path' => 'kaleidoscope',
        'middleware' => ['web'],
    ],
];

Testing

composer test

License

MIT

About

AWS Bedrock provider for Prism

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published