Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ parameters:
path: src/Ingest.php
count: 1
reportUnmatched: false
# Laravel AI package is optional and unpublished - ignore all related errors
-
message: '#Laravel\\Ai\\#'
reportUnmatched: false
-
identifier: 'property.nonObject'
path: src/Sensors/AiEventSensor.php
reportUnmatched: false
8 changes: 8 additions & 0 deletions src/Compatibility.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ReflectionProperty;
use Symfony\Component\Console\Input\ArgvInput;

use function class_exists;
use function implode;
use function method_exists;
use function tap;
Expand Down Expand Up @@ -41,6 +42,8 @@ final class Compatibility

public static bool $queryConnectionTypeCapturable = false;

public static bool $aiPackageInstalled = false;

/**
* @var array{
* nightwatch_should_sample?: bool|null,
Expand Down Expand Up @@ -121,6 +124,11 @@ public static function boot(Application $app): void
* @see https://github.com/laravel/framework/releases/tag/v12.45.0
*/
self::$queryConnectionTypeCapturable = version_compare($version, '12.45.0', '>=');

/**
* @see https://github.com/laravel/ai
*/
self::$aiPackageInstalled = class_exists(\Laravel\Ai\Ai::class);
}

/**
Expand Down
40 changes: 40 additions & 0 deletions src/Concerns/CapturesState.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@
use Illuminate\Queue\Events\JobQueueing;
use Illuminate\Queue\Events\JobReleasedAfterException;
use Illuminate\Routing\Route;
use Laravel\Ai\Events\AgentPrompted;
use Laravel\Ai\Events\AgentStreamed;
use Laravel\Ai\Events\AudioGenerated;
use Laravel\Ai\Events\EmbeddingsGenerated;
use Laravel\Ai\Events\GeneratingAudio;
use Laravel\Ai\Events\GeneratingEmbeddings;
use Laravel\Ai\Events\GeneratingImage;
use Laravel\Ai\Events\GeneratingTranscription;
use Laravel\Ai\Events\ImageGenerated;
use Laravel\Ai\Events\InvokingTool;
use Laravel\Ai\Events\PromptingAgent;
use Laravel\Ai\Events\StreamingAgent;
use Laravel\Ai\Events\ToolInvoked;
use Laravel\Ai\Events\TranscriptionGenerated;
use Laravel\Nightwatch\Compatibility;
use Laravel\Nightwatch\Core;
use Laravel\Nightwatch\ExecutionStage;
Expand Down Expand Up @@ -453,6 +467,32 @@ public function cacheEvent(CacheEvent $event): void
$this->ingest->write($resolver());
}

/**
* @internal
*/
public function aiEvent(
PromptingAgent|StreamingAgent|AgentPrompted|AgentStreamed|
InvokingTool|ToolInvoked|
GeneratingAudio|AudioGenerated|
GeneratingEmbeddings|EmbeddingsGenerated|
GeneratingImage|ImageGenerated|
GeneratingTranscription|TranscriptionGenerated $event
): void {
if ($this->paused) {
return;
}

$aiEvent = $this->sensor->aiEvent($event);

if ($aiEvent === null) {
return;
}

[$record, $resolver] = $aiEvent;

$this->ingest->write($resolver());
}

/**
* @internal
*/
Expand Down
52 changes: 52 additions & 0 deletions src/Hooks/AiEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Laravel\Nightwatch\Hooks;

use Laravel\Ai\Events\AgentPrompted;
use Laravel\Ai\Events\AgentStreamed;
use Laravel\Ai\Events\AudioGenerated;
use Laravel\Ai\Events\EmbeddingsGenerated;
use Laravel\Ai\Events\GeneratingAudio;
use Laravel\Ai\Events\GeneratingEmbeddings;
use Laravel\Ai\Events\GeneratingImage;
use Laravel\Ai\Events\GeneratingTranscription;
use Laravel\Ai\Events\ImageGenerated;
use Laravel\Ai\Events\InvokingTool;
use Laravel\Ai\Events\PromptingAgent;
use Laravel\Ai\Events\StreamingAgent;
use Laravel\Ai\Events\ToolInvoked;
use Laravel\Ai\Events\TranscriptionGenerated;
use Laravel\Nightwatch\Core;
use Laravel\Nightwatch\State\CommandState;
use Laravel\Nightwatch\State\RequestState;
use Throwable;

/**
* @internal
*/
final class AiEventListener
{
/**
* @param Core<RequestState|CommandState> $nightwatch
*/
public function __construct(
private Core $nightwatch,
) {
//
}

public function __invoke(
PromptingAgent|StreamingAgent|AgentPrompted|AgentStreamed|
InvokingTool|ToolInvoked|
GeneratingAudio|AudioGenerated|
GeneratingEmbeddings|EmbeddingsGenerated|
GeneratingImage|ImageGenerated|
GeneratingTranscription|TranscriptionGenerated $event
): void {
try {
$this->nightwatch->aiEvent($event);
} catch (Throwable $e) {
$this->nightwatch->report($e, handled: true);
}
}
}
35 changes: 35 additions & 0 deletions src/NightwatchServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,23 @@
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\ServiceProvider;
use Laravel\Ai\Events\AgentStreamed;
use Laravel\Ai\Events\AudioGenerated;
use Laravel\Ai\Events\EmbeddingsGenerated;
use Laravel\Ai\Events\GeneratingAudio;
use Laravel\Ai\Events\GeneratingEmbeddings;
use Laravel\Ai\Events\GeneratingImage;
use Laravel\Ai\Events\GeneratingTranscription;
use Laravel\Ai\Events\ImageGenerated;
use Laravel\Ai\Events\InvokingTool;
use Laravel\Ai\Events\PromptingAgent;
use Laravel\Ai\Events\StreamingAgent;
use Laravel\Ai\Events\ToolInvoked;
use Laravel\Ai\Events\TranscriptionGenerated;
use Laravel\Nightwatch\Console\AgentCommand;
use Laravel\Nightwatch\Facades\Nightwatch;
use Laravel\Nightwatch\Factories\Logger;
use Laravel\Nightwatch\Hooks\AiEventListener;
use Laravel\Nightwatch\Hooks\ArtisanStartingListener;
use Laravel\Nightwatch\Hooks\CacheEventListener;
use Laravel\Nightwatch\Hooks\CommandBootedHandler;
Expand Down Expand Up @@ -366,6 +380,27 @@ private function registerHooks(): void

$events->listen(RequestReceived::class, (new OctaneListener($core))(...)); // @phpstan-ignore class.notFound

/**
* @see \Laravel\Nightwatch\Records\AiEvent
*/
if (Compatibility::$aiPackageInstalled) {
$events->listen([
PromptingAgent::class,
StreamingAgent::class,
AgentStreamed::class,
InvokingTool::class,
ToolInvoked::class,
GeneratingAudio::class,
AudioGenerated::class,
GeneratingEmbeddings::class,
EmbeddingsGenerated::class,
GeneratingImage::class,
ImageGenerated::class,
GeneratingTranscription::class,
TranscriptionGenerated::class,
], (new AiEventListener($core))(...));
}

Queue::createPayloadUsing(new CreateQueuePayloadHandler($core));

if (Compatibility::$contextExists) {
Expand Down
28 changes: 28 additions & 0 deletions src/Records/AiEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Laravel\Nightwatch\Records;

/**
* @internal
*/
final class AiEvent
{
public function __construct(
public readonly string $operation,
public readonly string $invocationId,
public readonly string $toolInvocationId,
public readonly string $provider,
public readonly string $model,
public readonly string $agent,
public readonly string $tool,
public readonly int $promptTokens,
public readonly int $completionTokens,
public readonly int $cacheReadTokens,
public readonly int $cacheWriteTokens,
public readonly int $reasoningTokens,
public readonly int $duration,
public readonly bool $failed,
) {
//
}
}
41 changes: 41 additions & 0 deletions src/SensorManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
use Illuminate\Queue\Events\JobQueued;
use Illuminate\Queue\Events\JobQueueing;
use Illuminate\Queue\Events\JobReleasedAfterException;
use Laravel\Ai\Events\AgentPrompted;
use Laravel\Ai\Events\AgentStreamed;
use Laravel\Ai\Events\AudioGenerated;
use Laravel\Ai\Events\EmbeddingsGenerated;
use Laravel\Ai\Events\GeneratingAudio;
use Laravel\Ai\Events\GeneratingEmbeddings;
use Laravel\Ai\Events\GeneratingImage;
use Laravel\Ai\Events\GeneratingTranscription;
use Laravel\Ai\Events\ImageGenerated;
use Laravel\Ai\Events\InvokingTool;
use Laravel\Ai\Events\PromptingAgent;
use Laravel\Ai\Events\StreamingAgent;
use Laravel\Ai\Events\ToolInvoked;
use Laravel\Ai\Events\TranscriptionGenerated;
use Laravel\Nightwatch\Records\AiEvent;
use Laravel\Nightwatch\Records\CacheEvent as CacheEventRecord;
use Laravel\Nightwatch\Records\Command;
use Laravel\Nightwatch\Records\Exception;
Expand All @@ -27,6 +42,7 @@
use Laravel\Nightwatch\Records\Query;
use Laravel\Nightwatch\Records\QueuedJob;
use Laravel\Nightwatch\Records\Request as RequestRecord;
use Laravel\Nightwatch\Sensors\AiEventSensor;
use Laravel\Nightwatch\Sensors\CacheEventSensor;
use Laravel\Nightwatch\Sensors\CommandSensor;
use Laravel\Nightwatch\Sensors\ExceptionSensor;
Expand Down Expand Up @@ -128,6 +144,11 @@ final class SensorManager
*/
public $commandSensor;

/**
* @var (callable(PromptingAgent|StreamingAgent|AgentPrompted|AgentStreamed|InvokingTool|ToolInvoked|GeneratingAudio|AudioGenerated|GeneratingEmbeddings|EmbeddingsGenerated|GeneratingImage|ImageGenerated|GeneratingTranscription|TranscriptionGenerated): ?array{0: AiEvent, 1: callable(): array<mixed>})|null
*/
public $aiEventSensor;

/**
* @param list<string> $redactPayloadFields
* @param list<string> $redactHeaders
Expand Down Expand Up @@ -361,6 +382,25 @@ public function user(): ?array
return $sensor();
}

/**
* @return ?array{0: AiEvent, 1: callable(): array<mixed>}
*/
public function aiEvent(
PromptingAgent|StreamingAgent|AgentPrompted|AgentStreamed|
InvokingTool|ToolInvoked|
GeneratingAudio|AudioGenerated|
GeneratingEmbeddings|EmbeddingsGenerated|
GeneratingImage|ImageGenerated|
GeneratingTranscription|TranscriptionGenerated $event
): ?array {
$sensor = $this->aiEventSensor ??= new AiEventSensor(
executionState: $this->executionState,
clock: $this->clock,
);

return $sensor($event);
}

public function flush(): void
{
$this->cacheEventSensor = null;
Expand All @@ -377,5 +417,6 @@ public function flush(): void
$this->scheduledTaskSensor = null;
$this->requestSensor = null;
$this->commandSensor = null;
$this->aiEventSensor = null;
}
}
Loading
Loading