From b25394f3962c298149e4dfbe1eaf9ce5036a485c Mon Sep 17 00:00:00 2001 From: Sandro Gehri <sandrogehri@gmail.com> Date: Tue, 5 Dec 2023 09:11:01 +0100 Subject: [PATCH 1/5] WIP: add pulse card --- composer.json | 2 +- src/Facades/OpenAI.php | 1 + src/ServiceProvider.php | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ba9c259..c136f75 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^8.1.0", "guzzlehttp/guzzle": "^7.7.0", "laravel/framework": "^9.46.0|^10.14.1", - "openai-php/client": "^v0.8.0" + "openai-php/client": "dev-add-events as v0.8.0" }, "require-dev": { "laravel/pint": "^1.13.6", diff --git a/src/Facades/OpenAI.php b/src/Facades/OpenAI.php index ffd0294..1d7ab62 100644 --- a/src/Facades/OpenAI.php +++ b/src/Facades/OpenAI.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Facade; use OpenAI\Contracts\ResponseContract; use OpenAI\Laravel\Testing\OpenAIFake; +use OpenAI\Resources\Assistants; use OpenAI\Responses\StreamResponse; /** diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 976fcee..4005fa7 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,6 +4,7 @@ namespace OpenAI\Laravel; +use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use OpenAI; @@ -35,6 +36,7 @@ public function register(): void ->withOrganization($organization) ->withHttpHeader('OpenAI-Beta', 'assistants=v1') ->withHttpClient(new \GuzzleHttp\Client(['timeout' => config('openai.request_timeout', 30)])) + ->withEventDispatcher(resolve(DispatcherContract::class)) // @phpstan-ignore-line ->make(); }); From 4b9f108830901cbf3db7f7402f9e03e1f149f309 Mon Sep 17 00:00:00 2001 From: Sandro Gehri <sandrogehri@gmail.com> Date: Tue, 5 Dec 2023 13:46:43 +0100 Subject: [PATCH 2/5] Add OpenAI requests card for Laravel Pulse --- composer.json | 1 + .../views/livewire/openai-requests.blade.php | 104 ++++++++++++++++ src/Facades/OpenAI.php | 1 - src/Pulse/Livewire/OpenAIRequestsCard.php | 112 ++++++++++++++++++ src/Pulse/Recorders/OpenAIRequests.php | 69 +++++++++++ src/ServiceProvider.php | 16 ++- tests/Arch.php | 3 + tests/Facades/OpenAI.php | 9 ++ tests/ServiceProvider.php | 20 ++++ 9 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 resources/views/livewire/openai-requests.blade.php create mode 100644 src/Pulse/Livewire/OpenAIRequestsCard.php create mode 100644 src/Pulse/Recorders/OpenAIRequests.php diff --git a/composer.json b/composer.json index c136f75..cd9ebfb 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ }, "require-dev": { "laravel/pint": "^1.13.6", + "laravel/pulse": "^1.0.0", "pestphp/pest": "^2.8.2", "pestphp/pest-plugin-arch": "^2.2.2", "pestphp/pest-plugin-mock": "^2.0.0", diff --git a/resources/views/livewire/openai-requests.blade.php b/resources/views/livewire/openai-requests.blade.php new file mode 100644 index 0000000..3c61f32 --- /dev/null +++ b/resources/views/livewire/openai-requests.blade.php @@ -0,0 +1,104 @@ +<x-pulse::card :cols="$cols" :rows="$rows" :class="$class"> + <x-pulse::card-header + name="{{ $this->label }}" + title="Time: {{ number_format($time) }}ms; Run at: {{ $runAt }};" + details="past {{ $this->periodForHumans() }}" + > + <x-slot:icon> + <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" + xmlns="http://www.w3.org/2000/svg"> + <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/> + </svg> + </x-slot:icon> + <x-slot:actions> + @if(!$this->type) + <x-pulse::select + wire:model.live="openaiRequests" + label="By" + :options="[ + 'user' => 'Users', + 'endpoint' => 'API endpoint', + ]" + class="flex-1" + @change="loading = true" + /> + @endif + </x-slot:actions> + </x-pulse::card-header> + + <x-pulse::scroll :expand="$expand" wire:poll.5s=""> + @if ($requests->isEmpty()) + <x-pulse::no-results/> + @else + @if($aggregate === 'user') + <div class="grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4 gap-2"> + @foreach ($requests as $requestCount) + <x-pulse::user-card wire:key="{{ $requestCount->user->id.$this->period }}" + :name="$requestCount->user->name" :extra="$requestCount->user->extra"> + @if ($requestCount->user->avatar ?? false) + <x-slot:avatar> + <img height="32" width="32" src="{{ $requestCount->user->avatar }}" loading="lazy" + class="rounded-full"> + </x-slot:avatar> + @endif + + <x-slot:stats> + @php + $sampleRate = $config['sample_rate']; + @endphp + + @if ($sampleRate < 1) + <span title="Sample rate: {{ $sampleRate }}, Raw value: {{ number_format($requestCount->count) }}">~{{ number_format($requestCount->count * (1 / $sampleRate)) }}</span> + @else + {{ number_format($requestCount->count) }} + @endif + </x-slot:stats> + </x-pulse::user-card> + @endforeach + </div> + @else + <x-pulse::table> + <colgroup> + <col width="0%"/> + <col width="100%"/> + <col width="0%"/> + </colgroup> + <x-pulse::thead> + <tr> + <x-pulse::th>Method</x-pulse::th> + <x-pulse::th>Uri</x-pulse::th> + <x-pulse::th class="text-right">Count</x-pulse::th> + </tr> + </x-pulse::thead> + <tbody> + @foreach ($requests->take(10) as $request) + <tr class="h-2 first:h-0"></tr> + <tr wire:key="{{ $request->method.$request->uri.$this->period }}"> + <x-pulse::td> + <x-pulse::http-method-badge :method="$request->method"/> + </x-pulse::td> + <x-pulse::td class="overflow-hidden max-w-[1px]"> + <code class="block text-xs text-gray-900 dark:text-gray-100 truncate" + title="{{ $request->uri }}"> + /{{ $request->uri }} + </code> + </x-pulse::td> + <x-pulse::td numeric class="text-gray-700 dark:text-gray-300 font-bold"> + @if ($config['sample_rate'] < 1) + <span title="Sample rate: {{ $config['sample_rate'] }}, Raw value: {{ number_format($request->count) }}">~{{ number_format($request->count * (1 / $config['sample_rate'])) }}</span> + @else + {{ number_format($request->count) }} + @endif + </x-pulse::td> + </tr> + @endforeach + </tbody> + </x-pulse::table> + + @if ($requests->count() > 10) + <div class="mt-2 text-xs text-gray-400 text-center">Limited to 10 entries</div> + @endif + @endif + @endif + </x-pulse::scroll> +</x-pulse::card> diff --git a/src/Facades/OpenAI.php b/src/Facades/OpenAI.php index 1d7ab62..ffd0294 100644 --- a/src/Facades/OpenAI.php +++ b/src/Facades/OpenAI.php @@ -7,7 +7,6 @@ use Illuminate\Support\Facades\Facade; use OpenAI\Contracts\ResponseContract; use OpenAI\Laravel\Testing\OpenAIFake; -use OpenAI\Resources\Assistants; use OpenAI\Responses\StreamResponse; /** diff --git a/src/Pulse/Livewire/OpenAIRequestsCard.php b/src/Pulse/Livewire/OpenAIRequestsCard.php new file mode 100644 index 0000000..b53b722 --- /dev/null +++ b/src/Pulse/Livewire/OpenAIRequestsCard.php @@ -0,0 +1,112 @@ +<?php + +namespace OpenAI\Laravel\Pulse\Livewire; + +use Illuminate\Contracts\Support\Renderable; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\View; +use Laravel\Pulse\Facades\Pulse; +use Laravel\Pulse\Livewire\Card; +use Laravel\Pulse\Livewire\Concerns\HasPeriod; +use Laravel\Pulse\Livewire\Concerns\RemembersQueries; +use Livewire\Attributes\Computed; +use Livewire\Attributes\Lazy; +use Livewire\Attributes\Url; +use OpenAI\Laravel\Pulse\Recorders\OpenAIRequests; + +/** + * @internal + */ +#[Lazy] +class OpenAIRequestsCard extends Card +{ + use HasPeriod, RemembersQueries; + + /** + * The type of request aggregation to show. + * + * @var 'user'|'endpoint'|null + */ + public ?string $type = null; + + /** + * The openai requests type. + * + * @var 'user'|'endpoint' + */ + #[Url] + public string $openaiRequests = 'user'; + + #[Computed] + public function label(): string + { + return match ($this->type ?? $this->openaiRequests) { + 'user' => 'Top 10 OpenAI Users', + 'endpoint' => 'Top 10 OpenAI Endpoints', + }; + } + + /** + * Render the component. + */ + public function render(): Renderable + { + $aggregate = $this->type ?? $this->openaiRequests; + + [$requests, $time, $runAt] = $this->remember( + function () use ($aggregate) { + /** @var Collection<int, object{key: string, count: int}> $counts */ + $counts = Pulse::aggregate( + match ($aggregate) { + 'user' => 'openai_request_handled_per_user', + 'endpoint' => 'openai_request_handled_per_endpoint', + }, + 'count', // @phpstan-ignore-line + $this->periodAsInterval(), + limit: 10, + ); + + if ($aggregate === 'user') { + /** @var Collection<int, array{id: string|int, name: string, email?: ?string, avatar?: ?string, extra?: ?string}> $users */ + $users = Pulse::resolveUsers($counts->pluck('key')); + + return $counts->map(function ($row) use ($users) { + $user = $users->firstWhere('id', $row->key); + + return (object) [ + 'user' => (object) [ + 'id' => $row->key, + 'name' => $user['name'] ?? ($row->key === 'null' ? 'Guest' : 'Unknown'), + 'extra' => $user['extra'] ?? $user['email'] ?? '', + 'avatar' => $user['avatar'] ?? (($user['email'] ?? false) + ? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user['email'])))) + : null), + ], + 'count' => (int) $row->count, + ]; + }); + } + + return $counts->map(function ($row) { + [$method, $uri] = json_decode($row->key, flags: JSON_THROW_ON_ERROR); // @phpstan-ignore-line + + return (object) [ + 'uri' => $uri, + 'method' => $method, + 'count' => (int) $row->count, + ]; + }); + }, + $aggregate + ); + + return View::make('openai-php::livewire.openai-requests', [ + 'time' => $time, + 'runAt' => $runAt, + 'config' => Config::get('pulse.recorders.'.OpenAIRequests::class), + 'requests' => $requests, + 'aggregate' => $aggregate, + ]); + } +} diff --git a/src/Pulse/Recorders/OpenAIRequests.php b/src/Pulse/Recorders/OpenAIRequests.php new file mode 100644 index 0000000..7b07118 --- /dev/null +++ b/src/Pulse/Recorders/OpenAIRequests.php @@ -0,0 +1,69 @@ +<?php + +namespace OpenAI\Laravel\Pulse\Recorders; + +use Carbon\CarbonImmutable; +use Illuminate\Config\Repository; +use Laravel\Pulse\Pulse; +use Laravel\Pulse\Recorders\Concerns\Groups; +use Laravel\Pulse\Recorders\Concerns\Ignores; +use Laravel\Pulse\Recorders\Concerns\Sampling; +use OpenAI\Events\RequestHandled; + +/** + * @internal + */ +class OpenAIRequests +{ + use Groups, Ignores, Sampling; + + /** + * The events to listen for. + * + * @var list<class-string> + */ + public array $listen = [ + RequestHandled::class, + ]; + + /** + * Create a new recorder instance. + */ + public function __construct( + protected Pulse $pulse, + protected Repository $config, + ) { + // + } + + /** + * Record the request. + */ + public function record(RequestHandled $event): void + { + [$timestamp, $method, $uri, $userId] = [ + CarbonImmutable::now()->getTimestamp(), + $event->payload->method->value, + $event->payload->uri->toString(), + $this->pulse->resolveAuthenticatedUserId(), + ]; + + $this->pulse->lazy(function () use ($timestamp, $method, $uri, $userId) { + if (! $this->shouldSample() || $this->shouldIgnore($uri)) { + return; + } + + $this->pulse->record( + type: 'openai_request_handled_per_user', + key: json_encode($userId, flags: JSON_THROW_ON_ERROR), + timestamp: $timestamp, + )->count(); + + $this->pulse->record( + type: 'openai_request_handled_per_endpoint', + key: json_encode([$method, $this->group($uri)], flags: JSON_THROW_ON_ERROR), + timestamp: $timestamp, + )->count(); + }); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 4005fa7..e8f3c8f 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,26 +4,28 @@ namespace OpenAI\Laravel; +use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; -use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider as BaseServiceProvider; +use Livewire\Livewire; use OpenAI; use OpenAI\Client; use OpenAI\Contracts\ClientContract; use OpenAI\Laravel\Commands\InstallCommand; use OpenAI\Laravel\Exceptions\ApiKeyIsMissing; +use OpenAI\Laravel\Pulse\Livewire\OpenAIRequestsCard; /** * @internal */ -final class ServiceProvider extends BaseServiceProvider implements DeferrableProvider +final class ServiceProvider extends BaseServiceProvider { /** * Register any application services. */ public function register(): void { - $this->app->singleton(ClientContract::class, static function (): Client { + $this->app->singleton(ClientContract::class, static function (Container $container): Client { $apiKey = config('openai.api_key'); $organization = config('openai.organization'); @@ -36,7 +38,7 @@ public function register(): void ->withOrganization($organization) ->withHttpHeader('OpenAI-Beta', 'assistants=v1') ->withHttpClient(new \GuzzleHttp\Client(['timeout' => config('openai.request_timeout', 30)])) - ->withEventDispatcher(resolve(DispatcherContract::class)) // @phpstan-ignore-line + ->withEventDispatcher($container->make(DispatcherContract::class)) // @phpstan-ignore-line ->make(); }); @@ -58,6 +60,12 @@ public function boot(): void InstallCommand::class, ]); } + + $this->loadViewsFrom(__DIR__.'/../resources/views', 'openai-php'); + + if (class_exists(Livewire::class)) { + Livewire::component('openai.pulse.requests', OpenAIRequestsCard::class); + } } /** diff --git a/tests/Arch.php b/tests/Arch.php index e1ca9cc..9556939 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -17,9 +17,12 @@ ->expect('OpenAI\Laravel\ServiceProvider') ->toOnlyUse([ 'GuzzleHttp\Client', + 'Illuminate\Container\Container', 'Illuminate\Support\ServiceProvider', + 'Livewire\Livewire', 'OpenAI\Laravel', 'OpenAI', + 'Illuminate\Contracts\Events\Dispatcher', 'Illuminate\Contracts\Support\DeferrableProvider', // helpers... diff --git a/tests/Facades/OpenAI.php b/tests/Facades/OpenAI.php index a30f161..cea7a19 100644 --- a/tests/Facades/OpenAI.php +++ b/tests/Facades/OpenAI.php @@ -1,11 +1,13 @@ <?php use Illuminate\Config\Repository; +use Illuminate\Contracts\Events\Dispatcher; use OpenAI\Laravel\Facades\OpenAI; use OpenAI\Laravel\ServiceProvider; use OpenAI\Resources\Completions; use OpenAI\Responses\Completions\CreateResponse; use PHPUnit\Framework\ExpectationFailedException; +use Psr\EventDispatcher\EventDispatcherInterface; it('resolves resources', function () { $app = app(); @@ -16,6 +18,13 @@ ], ])); + $app->bind(Dispatcher::class, fn () => new class implements EventDispatcherInterface + { + public function dispatch(object $event) + { + } + }); + (new ServiceProvider($app))->register(); OpenAI::setFacadeApplication($app); diff --git a/tests/ServiceProvider.php b/tests/ServiceProvider.php index cae4dd5..f31020f 100644 --- a/tests/ServiceProvider.php +++ b/tests/ServiceProvider.php @@ -1,10 +1,23 @@ <?php use Illuminate\Config\Repository; +use Illuminate\Contracts\Events\Dispatcher; use OpenAI\Client; use OpenAI\Contracts\ClientContract; use OpenAI\Laravel\Exceptions\ApiKeyIsMissing; use OpenAI\Laravel\ServiceProvider; +use Psr\EventDispatcher\EventDispatcherInterface; + +beforeEach(function () { + $app = app(); + + $app->bind(Dispatcher::class, fn () => new class implements EventDispatcherInterface + { + public function dispatch(object $event) + { + } + }); +}); it('binds the client on the container', function () { $app = app(); @@ -15,6 +28,13 @@ ], ])); + $app->bind(DispatcherContract::class, fn () => new class implements DispatcherContract + { + public function dispatch(object $event): void + { + } + }); + (new ServiceProvider($app))->register(); expect($app->get(Client::class))->toBeInstanceOf(Client::class); From b0e076af8555afcfebbd65cc7468f2295d9efb81 Mon Sep 17 00:00:00 2001 From: Sandro Gehri <sandrogehri@gmail.com> Date: Tue, 5 Dec 2023 13:55:54 +0100 Subject: [PATCH 3/5] Update README --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 93917b8..8e312ab 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,51 @@ OpenAI::assertSent(Completions::class, function (string $method, array $paramete For more testing examples, take a look at the [openai-php/client](https://github.com/openai-php/client#testing) repository. +## Laravel Pulse + +This package provides a [Laravel Pulse](https://pulse.laravel.com) card to show statistics about your OpenAI usage. + +The card supports two metrics: +- **Requests per user**: Shows the number of requests per user. +- **Requests per endpoint**: Shows the number of requests per endpoint. + +### Installation + +First, make sure Laravel Pulse is [installed](https://laravel.com/docs/10.x/pulse#installation). + +Next, you need to register the recorder in your `config/pulse.php` file: + +```php +'recorders' => [ + // ... + + \OpenAI\Laravel\Pulse\Recorders\OpenAIRequests::class => [ + 'enabled' => env('PULSE_OPENAI_REQUESTS_ENABLED', true), + 'sample_rate' => env('PULSE_OPENAI_REQUESTS_SAMPLE_RATE', 1), + 'ignore' => [], + 'groups' => [ + '/(.*)\/(asst_|file-|ft-|msg_|run_|step_|thread_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', + ], + ], +], +``` + +### Usage + +Finally, add the card to your pulse `dashboard.blade.php` or any other Blade file. + +```blade +<livewire:openai.pulse.requests /> +``` + +If you want to be specific about the metric to show, you can pass it as `type`: + +```blade +<livewire:openai.pulse.requests type="endpoint" /> + +<livewire:openai.pulse.requests type="user" /> +``` + --- OpenAI PHP for Laravel is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. From a6006cfdcb40c89814586742d90daa276f4bb9ef Mon Sep 17 00:00:00 2001 From: Sandro Gehri <sandrogehri@gmail.com> Date: Tue, 5 Dec 2023 15:09:02 +0100 Subject: [PATCH 4/5] Add DispatcherDecorator --- composer.json | 2 +- src/Events/DispatcherDecorator.php | 19 +++++++++++ src/ServiceProvider.php | 3 +- tests/Facades/OpenAI.php | 9 ++---- tests/Fixtures/NullEventDispatcher.php | 44 ++++++++++++++++++++++++++ tests/ServiceProvider.php | 16 ++-------- 6 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 src/Events/DispatcherDecorator.php create mode 100644 tests/Fixtures/NullEventDispatcher.php diff --git a/composer.json b/composer.json index cd9ebfb..318ffe0 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "require-dev": { "laravel/pint": "^1.13.6", - "laravel/pulse": "^1.0.0", + "laravel/pulse": "^v1.0.0-beta3", "pestphp/pest": "^2.8.2", "pestphp/pest-plugin-arch": "^2.2.2", "pestphp/pest-plugin-mock": "^2.0.0", diff --git a/src/Events/DispatcherDecorator.php b/src/Events/DispatcherDecorator.php new file mode 100644 index 0000000..3585ebd --- /dev/null +++ b/src/Events/DispatcherDecorator.php @@ -0,0 +1,19 @@ +<?php + +namespace OpenAI\Laravel\Events; + +use Illuminate\Contracts\Events\Dispatcher; +use Psr\EventDispatcher\EventDispatcherInterface; + +class DispatcherDecorator implements EventDispatcherInterface +{ + public function __construct( + private readonly Dispatcher $events + ) { + } + + public function dispatch(object $event) + { + return (object) $this->events->dispatch($event); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index e8f3c8f..eb115e8 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -12,6 +12,7 @@ use OpenAI\Client; use OpenAI\Contracts\ClientContract; use OpenAI\Laravel\Commands\InstallCommand; +use OpenAI\Laravel\Events\DispatcherDecorator; use OpenAI\Laravel\Exceptions\ApiKeyIsMissing; use OpenAI\Laravel\Pulse\Livewire\OpenAIRequestsCard; @@ -38,7 +39,7 @@ public function register(): void ->withOrganization($organization) ->withHttpHeader('OpenAI-Beta', 'assistants=v1') ->withHttpClient(new \GuzzleHttp\Client(['timeout' => config('openai.request_timeout', 30)])) - ->withEventDispatcher($container->make(DispatcherContract::class)) // @phpstan-ignore-line + ->withEventDispatcher(new DispatcherDecorator($container->make(DispatcherContract::class))) // @phpstan-ignore-line ->make(); }); diff --git a/tests/Facades/OpenAI.php b/tests/Facades/OpenAI.php index cea7a19..19b8af4 100644 --- a/tests/Facades/OpenAI.php +++ b/tests/Facades/OpenAI.php @@ -7,7 +7,7 @@ use OpenAI\Resources\Completions; use OpenAI\Responses\Completions\CreateResponse; use PHPUnit\Framework\ExpectationFailedException; -use Psr\EventDispatcher\EventDispatcherInterface; +use Tests\Fixtures\NullEventDispatcher; it('resolves resources', function () { $app = app(); @@ -18,12 +18,7 @@ ], ])); - $app->bind(Dispatcher::class, fn () => new class implements EventDispatcherInterface - { - public function dispatch(object $event) - { - } - }); + $app->bind(Dispatcher::class, fn () => new NullEventDispatcher()); (new ServiceProvider($app))->register(); diff --git a/tests/Fixtures/NullEventDispatcher.php b/tests/Fixtures/NullEventDispatcher.php new file mode 100644 index 0000000..464be9e --- /dev/null +++ b/tests/Fixtures/NullEventDispatcher.php @@ -0,0 +1,44 @@ +<?php + +namespace Tests\Fixtures; + +use Illuminate\Contracts\Events\Dispatcher; + +class NullEventDispatcher implements Dispatcher +{ + public function listen($events, $listener = null) + { + } + + public function hasListeners($eventName) + { + } + + public function subscribe($subscriber) + { + } + + public function until($event, $payload = []) + { + } + + public function dispatch($event, $payload = [], $halt = false) + { + } + + public function push($event, $payload = []) + { + } + + public function flush($event) + { + } + + public function forget($event) + { + } + + public function forgetPushed() + { + } +} diff --git a/tests/ServiceProvider.php b/tests/ServiceProvider.php index f31020f..080840b 100644 --- a/tests/ServiceProvider.php +++ b/tests/ServiceProvider.php @@ -6,17 +6,12 @@ use OpenAI\Contracts\ClientContract; use OpenAI\Laravel\Exceptions\ApiKeyIsMissing; use OpenAI\Laravel\ServiceProvider; -use Psr\EventDispatcher\EventDispatcherInterface; +use Tests\Fixtures\NullEventDispatcher; beforeEach(function () { $app = app(); - $app->bind(Dispatcher::class, fn () => new class implements EventDispatcherInterface - { - public function dispatch(object $event) - { - } - }); + $app->bind(Dispatcher::class, fn () => new NullEventDispatcher()); }); it('binds the client on the container', function () { @@ -28,13 +23,6 @@ public function dispatch(object $event) ], ])); - $app->bind(DispatcherContract::class, fn () => new class implements DispatcherContract - { - public function dispatch(object $event): void - { - } - }); - (new ServiceProvider($app))->register(); expect($app->get(Client::class))->toBeInstanceOf(Client::class); From ff5c7c1b26e4c034a6fe53e18526a842070bd04c Mon Sep 17 00:00:00 2001 From: Sandro Gehri <sandrogehri@gmail.com> Date: Tue, 12 Dec 2023 15:05:51 +0100 Subject: [PATCH 5/5] Improve grouping example in README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e312ab..368120a 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,13 @@ Next, you need to register the recorder in your `config/pulse.php` file: 'sample_rate' => env('PULSE_OPENAI_REQUESTS_SAMPLE_RATE', 1), 'ignore' => [], 'groups' => [ - '/(.*)\/(asst_|file-|ft-|msg_|run_|step_|thread_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', + '/(.*)\/(asst_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', + '/(.*)\/(file-)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', + '/(.*)\/(ft-)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', + '/(.*)\/(thread_)[0-9a-zA-Z]*(.*)\/(run_)[0-9a-zA-Z]*(.*)\/(step_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3/\4*\5/\6*\7', + '/(.*)\/(thread_)[0-9a-zA-Z]*(.*)\/(run_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3/\4*\5', + '/(.*)\/(thread_)[0-9a-zA-Z]*(.*)\/(msg_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3/\4*\5', + '/(.*)\/(thread_)[0-9a-zA-Z]*(.*)/' => '\1/\2*\3', ], ], ],