From 1cc5b26015856f84821141743fa4df273f265ab2 Mon Sep 17 00:00:00 2001 From: Allan Tatter Date: Thu, 4 Jul 2024 11:12:15 +0300 Subject: [PATCH] Refactor streaming support and add test coverage --- composer.json | 5 +- phpunit.xml.dist | 34 ++++++++ src/Cache/CacheInterface.php | 11 +++ src/Cache/EmbeddingsCache.php | 8 +- .../Assistants/CreateAssistant.php | 10 ++- .../Assistants/CreateAssistantFile.php | 10 ++- .../Assistants/DeleteAssistant.php | 10 ++- .../Assistants/DeleteAssistantFile.php | 7 +- .../Assistants/ListAssistantFiles.php | 7 +- .../Assistants/ModifyAssistant.php | 10 ++- src/Capabilities/Capability.php | 2 + src/Capabilities/CapabilityClient.php | 28 ++++--- src/Capabilities/Chat/CreateChat.php | 39 +++++++-- .../Chat/Responses/StreamedChatResponse.php | 2 +- .../Embeddings/CreateEmbedding.php | 9 ++- src/Capabilities/Embeddings/Embeddings.php | 2 - src/Capabilities/Files/DeleteFile.php | 16 ++-- src/Capabilities/Files/ListFiles.php | 7 +- .../Files/Responses/FileContentResponse.php | 4 +- ...eteResponse.php => FileDeleteResponse.php} | 2 +- src/Capabilities/Files/RetrieveFile.php | 7 +- .../Files/RetrieveFileContent.php | 7 +- src/Capabilities/Files/UploadFile.php | 19 +++-- src/Capabilities/Responses/Response.php | 8 +- src/Capabilities/Responses/StreamResponse.php | 2 +- src/Capabilities/Threads/CreateMessage.php | 10 ++- src/Capabilities/Threads/CreateRun.php | 10 ++- src/Capabilities/Threads/CreateThread.php | 12 ++- src/Capabilities/Threads/DeleteThread.php | 10 ++- src/Capabilities/Threads/ListMessageFiles.php | 7 +- src/Capabilities/Threads/ListMessages.php | 7 +- src/Capabilities/Threads/ModifyMessage.php | 10 ++- src/Capabilities/Threads/ModifyThread.php | 10 ++- src/Capabilities/Threads/RetrieveMessage.php | 7 +- .../Threads/RetrieveMessageFile.php | 7 +- src/Capabilities/Threads/RetrieveRun.php | 7 +- src/Capabilities/Threads/RetrieveThread.php | 3 +- src/Facades/OpenAI.php | 2 +- src/OpenAI.php | 11 ++- src/StreamHandler.php | 5 +- tests/Feature/ExampleTest.php | 15 ++++ tests/Unit/AssistantTest.php | 80 +++++++++++++++++++ tests/Unit/ChatTest.php | 55 +++++++++++++ tests/Unit/EmbeddingsTest.php | 41 ++++++++++ tests/Unit/FilesTest.php | 54 +++++++++++++ tests/Unit/ThreadsTest.php | 65 +++++++++++++++ 46 files changed, 574 insertions(+), 120 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 src/Cache/CacheInterface.php rename src/Capabilities/Files/Responses/{DeleteResponse.php => FileDeleteResponse.php} (91%) create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Unit/AssistantTest.php create mode 100644 tests/Unit/ChatTest.php create mode 100644 tests/Unit/EmbeddingsTest.php create mode 100644 tests/Unit/FilesTest.php create mode 100644 tests/Unit/ThreadsTest.php diff --git a/composer.json b/composer.json index a738fbe..6c1820f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "outl1ne/nova-translations-loader": "^5.0" }, "require-dev": { - "nova-kit/nova-devtool": "^1.5" + "nova-kit/nova-devtool": "^1.5", + "phpunit/phpunit": "^11.1" }, "autoload": { "psr-4": { @@ -65,4 +66,4 @@ "@php vendor/bin/phpstan analyse" ] } -} \ No newline at end of file +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..057b347 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + tests/Unit + + + tests/Feature + + + + + src + + + + + + + + + + + + + + + + + diff --git a/src/Cache/CacheInterface.php b/src/Cache/CacheInterface.php new file mode 100644 index 0000000..c97234e --- /dev/null +++ b/src/Cache/CacheInterface.php @@ -0,0 +1,11 @@ +enabled) return; Cache::put($this->cacheKey($arguments['model'], $arguments['input'], $arguments), [ diff --git a/src/Capabilities/Assistants/CreateAssistant.php b/src/Capabilities/Assistants/CreateAssistant.php index b544eb2..4c457a5 100644 --- a/src/Capabilities/Assistants/CreateAssistant.php +++ b/src/Capabilities/Assistants/CreateAssistant.php @@ -30,10 +30,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("assistants", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new AssistantResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Assistants/CreateAssistantFile.php b/src/Capabilities/Assistants/CreateAssistantFile.php index 8fd95d6..fd438b2 100644 --- a/src/Capabilities/Assistants/CreateAssistantFile.php +++ b/src/Capabilities/Assistants/CreateAssistantFile.php @@ -19,10 +19,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants/{$assistantId}/files", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("assistants/{$assistantId}/files", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new AssistantFileResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Assistants/DeleteAssistant.php b/src/Capabilities/Assistants/DeleteAssistant.php index ffc5416..afdf8d7 100644 --- a/src/Capabilities/Assistants/DeleteAssistant.php +++ b/src/Capabilities/Assistants/DeleteAssistant.php @@ -16,10 +16,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("assistants/{$assistantId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->delete("assistants/{$assistantId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new DeleteResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Assistants/DeleteAssistantFile.php b/src/Capabilities/Assistants/DeleteAssistantFile.php index 57795eb..e48bc09 100644 --- a/src/Capabilities/Assistants/DeleteAssistantFile.php +++ b/src/Capabilities/Assistants/DeleteAssistantFile.php @@ -17,8 +17,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("assistants/{$assistantId}/files/{$fileId}"); - $response->throw(); + $response = $this->openAI->http()->delete("assistants/{$assistantId}/files/{$fileId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ]); return $this->handleResponse(new DeleteResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Assistants/ListAssistantFiles.php b/src/Capabilities/Assistants/ListAssistantFiles.php index ca149e7..71f9b61 100644 --- a/src/Capabilities/Assistants/ListAssistantFiles.php +++ b/src/Capabilities/Assistants/ListAssistantFiles.php @@ -26,10 +26,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("assistants/{$assistantId}/files", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("assistants/{$assistantId}/files", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new AssistantFileListResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Assistants/ModifyAssistant.php b/src/Capabilities/Assistants/ModifyAssistant.php index 39a3428..ede2ec9 100644 --- a/src/Capabilities/Assistants/ModifyAssistant.php +++ b/src/Capabilities/Assistants/ModifyAssistant.php @@ -31,10 +31,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants/{$assistantId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("assistants/{$assistantId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new AssistantResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Capability.php b/src/Capabilities/Capability.php index c6b787c..a1bf67a 100644 --- a/src/Capabilities/Capability.php +++ b/src/Capabilities/Capability.php @@ -4,6 +4,7 @@ use Closure; use Outl1ne\NovaOpenAI\OpenAI; +use Outl1ne\NovaOpenAI\Cache\CacheInterface; class Capability { @@ -12,6 +13,7 @@ class Capability public Closure $shouldStoreErrorsCallback; public Closure $storingCallback; public ?Closure $streamCallback = null; + public ?CacheInterface $cache; public function __construct( public readonly OpenAI $openAI, diff --git a/src/Capabilities/CapabilityClient.php b/src/Capabilities/CapabilityClient.php index 55f6675..486c9e7 100644 --- a/src/Capabilities/CapabilityClient.php +++ b/src/Capabilities/CapabilityClient.php @@ -5,13 +5,15 @@ use Closure; use Exception; use Outl1ne\NovaOpenAI\OpenAI; +use GuzzleHttp\Promise\Promise; use Outl1ne\NovaOpenAI\StreamHandler; +use Psr\Http\Message\ResponseInterface; use Outl1ne\NovaOpenAI\Pricing\Calculator; use Outl1ne\NovaOpenAI\Models\OpenAIRequest; -use Illuminate\Http\Client\Response as HttpResponse; +use GuzzleHttp\Psr7\Response as HttpResponse; use Outl1ne\NovaOpenAI\Capabilities\Responses\Response; -use Outl1ne\NovaOpenAI\Capabilities\Responses\CachedResponse; use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamChunk; +use Outl1ne\NovaOpenAI\Capabilities\Responses\CachedResponse; abstract class CapabilityClient { @@ -80,20 +82,24 @@ protected function isStreamedResponse(HttpResponse $response) return strpos($response->getHeaderLine('Content-Type'), 'text/event-stream') !== false; } - protected function handleStreamedResponse(HttpResponse $httpResponse, ?callable $handleResponse = null) + protected function handleStreamedResponse(Promise $promise, ?callable $handleResponse = null) { if (!$this->capability->streamCallback instanceof Closure) { throw new Exception('Response is a stream but stream callback is not defined.'); } - $response = (new StreamHandler($httpResponse, $this->capability->streamCallback, function (StreamChunk $streamChunk) { - $this->request->status = 'streaming'; - $this->request->meta = $streamChunk?->meta; - $this->request->model_used = $streamChunk?->model; - if (($this->capability->shouldStoreCallback)($streamChunk)) { - $this->request->save(); - } - }))->handle(); + $chainedPromise = $promise->then(function (ResponseInterface $stream) { + $response = (new StreamHandler($stream, $this->capability->streamCallback, function (StreamChunk $streamChunk) { + $this->request->status = 'streaming'; + $this->request->meta = $streamChunk?->meta; + $this->request->model_used = $streamChunk?->model; + if (($this->capability->shouldStoreCallback)($streamChunk)) { + $this->request->save(); + } + }))->handle(); + return $response; + }); + $response = $chainedPromise->wait(); $this->request->cost = $this->calculateCost($response); $this->request->time_sec = $this->measure(); diff --git a/src/Capabilities/Chat/CreateChat.php b/src/Capabilities/Chat/CreateChat.php index 3cc44a0..d54ca07 100644 --- a/src/Capabilities/Chat/CreateChat.php +++ b/src/Capabilities/Chat/CreateChat.php @@ -3,7 +3,12 @@ namespace Outl1ne\NovaOpenAI\Capabilities\Chat; use Exception; -use Illuminate\Http\Client\Response; +use GuzzleHttp\Promise\Promise; +use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Response as GuzzleResponse; use Outl1ne\NovaOpenAI\Capabilities\CapabilityClient; use Outl1ne\NovaOpenAI\Capabilities\Chat\Parameters\Messages; use Outl1ne\NovaOpenAI\Capabilities\Chat\Responses\ChatResponse; @@ -54,14 +59,32 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post('chat/completions', [ - 'model' => $model, - 'messages' => $messages->messages, - ...$this->request->arguments, - ]); - $response->throw(); + if (isset($this->request->arguments['stream'])) { + $response = $this->openAI->http()->postAsync('chat/completions', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'model' => $model, + 'messages' => $messages->messages, + ...$this->request->arguments, + ]), + 'stream' => true, + ]); + } else { + $response = $this->openAI->http()->post('chat/completions', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'model' => $model, + 'messages' => $messages->messages, + ...$this->request->arguments, + ]), + ]); + } - if ($this->isStreamedResponse($response)) { + if ($response instanceof Promise) { return $this->handleStreamedResponse($response, [$this, 'response']); } return $this->handleResponse(new ChatResponse($response), [$this, 'response']); diff --git a/src/Capabilities/Chat/Responses/StreamedChatResponse.php b/src/Capabilities/Chat/Responses/StreamedChatResponse.php index ea33567..a464afd 100644 --- a/src/Capabilities/Chat/Responses/StreamedChatResponse.php +++ b/src/Capabilities/Chat/Responses/StreamedChatResponse.php @@ -2,7 +2,7 @@ namespace Outl1ne\NovaOpenAI\Capabilities\Chat\Responses; -use Illuminate\Http\Client\Response; +use GuzzleHttp\Psr7\Response; use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamChunk; use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamResponse; diff --git a/src/Capabilities/Embeddings/CreateEmbedding.php b/src/Capabilities/Embeddings/CreateEmbedding.php index 1ada5ab..06c212c 100644 --- a/src/Capabilities/Embeddings/CreateEmbedding.php +++ b/src/Capabilities/Embeddings/CreateEmbedding.php @@ -34,13 +34,16 @@ public function makeRequest(string $model, string $input, ?string $encodingForma 'model' => $model, 'input' => $input, ...$this->request->arguments, - ], fn (...$args) => $this->openAI->http()->withHeader('Content-Type', 'application/json')->post('embeddings', ...$args)); + ], fn ($args) => $this->openAI->http()->post('embeddings', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode($args), + ])); if ($response instanceof CachedEmbeddingsResponse) { return $this->handleCachedResponse($response, [$this, 'cachedResponse']); } else { - $response->throw(); - $result = $this->handleResponse(new EmbeddingsResponse($response), [$this, 'response']); $this->capability->cache->put([ diff --git a/src/Capabilities/Embeddings/Embeddings.php b/src/Capabilities/Embeddings/Embeddings.php index b198a83..d434267 100644 --- a/src/Capabilities/Embeddings/Embeddings.php +++ b/src/Capabilities/Embeddings/Embeddings.php @@ -7,8 +7,6 @@ class Embeddings extends Capability { - public EmbeddingsCache $cache; - public function create(string $model, string $input, ?string $encodingFormat = null, ?int $dimensions = null, ?string $user = null) { return (new CreateEmbedding($this))->makeRequest($model, $input, $encodingFormat, $dimensions, $user); diff --git a/src/Capabilities/Files/DeleteFile.php b/src/Capabilities/Files/DeleteFile.php index 2d65ee4..2eec28c 100644 --- a/src/Capabilities/Files/DeleteFile.php +++ b/src/Capabilities/Files/DeleteFile.php @@ -4,7 +4,7 @@ use Exception; use Outl1ne\NovaOpenAI\Capabilities\CapabilityClient; -use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\DeleteResponse; +use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\FileDeleteResponse; class DeleteFile extends CapabilityClient { @@ -12,16 +12,20 @@ class DeleteFile extends CapabilityClient public function makeRequest( string $fileId, - ): DeleteResponse { + ): FileDeleteResponse { $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("files/{$fileId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->delete("files/{$fileId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); - return $this->handleResponse(new DeleteResponse($response)); + return $this->handleResponse(new FileDeleteResponse($response)); } catch (Exception $e) { $this->handleException($e); } diff --git a/src/Capabilities/Files/ListFiles.php b/src/Capabilities/Files/ListFiles.php index be39c82..a2b8ba5 100644 --- a/src/Capabilities/Files/ListFiles.php +++ b/src/Capabilities/Files/ListFiles.php @@ -18,10 +18,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("files", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("files", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new FileListResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Files/Responses/FileContentResponse.php b/src/Capabilities/Files/Responses/FileContentResponse.php index 778a6fe..0d4bde6 100644 --- a/src/Capabilities/Files/Responses/FileContentResponse.php +++ b/src/Capabilities/Files/Responses/FileContentResponse.php @@ -6,12 +6,12 @@ class FileContentResponse extends Response { - public $content; + public readonly string $content; public function __construct(...$arguments) { parent::__construct(...$arguments); - $this->content = $this->data; + $this->content = $arguments[0]->getBody()->getContents(); } } diff --git a/src/Capabilities/Files/Responses/DeleteResponse.php b/src/Capabilities/Files/Responses/FileDeleteResponse.php similarity index 91% rename from src/Capabilities/Files/Responses/DeleteResponse.php rename to src/Capabilities/Files/Responses/FileDeleteResponse.php index 11a89dd..37e926d 100644 --- a/src/Capabilities/Files/Responses/DeleteResponse.php +++ b/src/Capabilities/Files/Responses/FileDeleteResponse.php @@ -4,7 +4,7 @@ use Outl1ne\NovaOpenAI\Capabilities\Responses\Response; -class DeleteResponse extends Response +class FileDeleteResponse extends Response { public string $id; diff --git a/src/Capabilities/Files/RetrieveFile.php b/src/Capabilities/Files/RetrieveFile.php index 30b5b45..29cee70 100644 --- a/src/Capabilities/Files/RetrieveFile.php +++ b/src/Capabilities/Files/RetrieveFile.php @@ -16,10 +16,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("files/{$fileId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("files/{$fileId}", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new FileResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Files/RetrieveFileContent.php b/src/Capabilities/Files/RetrieveFileContent.php index e67e517..87a7396 100644 --- a/src/Capabilities/Files/RetrieveFileContent.php +++ b/src/Capabilities/Files/RetrieveFileContent.php @@ -31,10 +31,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("files/{$assistantId}/content", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("files/{$assistantId}/content", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new FileContentResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Files/UploadFile.php b/src/Capabilities/Files/UploadFile.php index 920d5c9..8b944b4 100644 --- a/src/Capabilities/Files/UploadFile.php +++ b/src/Capabilities/Files/UploadFile.php @@ -20,14 +20,19 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->attach( - 'file', - $file, - $filename, - )->post("files", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("files", [ + 'multipart' => [ + [ + 'name' => 'file', + 'contents' => $file, + 'filename' => $filename, + ], + [ + 'name' => 'purpose', + 'contents' => $purpose, + ], + ], ]); - $response->throw(); return $this->handleResponse(new FileResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Responses/Response.php b/src/Capabilities/Responses/Response.php index 3d97847..c91a440 100644 --- a/src/Capabilities/Responses/Response.php +++ b/src/Capabilities/Responses/Response.php @@ -2,7 +2,7 @@ namespace Outl1ne\NovaOpenAI\Capabilities\Responses; -use Illuminate\Http\Client\Response as HttpResponse; +use GuzzleHttp\Psr7\Response as HttpResponse; use Outl1ne\NovaOpenAI\Models\OpenAIRequest; class Response @@ -13,15 +13,15 @@ class Response public ?RateLimit $rateLimit; public OpenAIRequest $request; - public readonly array $data; + public readonly ?array $data; public readonly array $headers; public ?string $model = null; public function __construct( public readonly HttpResponse $response, ) { - $this->data = $response->json(); - $this->headers = $response->headers(); + $this->data = json_decode($response->getBody(), true); + $this->headers = $response->getHeaders(); $this->usage = $this->createUsage(); $this->rateLimit = $this->createRateLimit(); diff --git a/src/Capabilities/Responses/StreamResponse.php b/src/Capabilities/Responses/StreamResponse.php index 2e081e6..d9d7f5e 100644 --- a/src/Capabilities/Responses/StreamResponse.php +++ b/src/Capabilities/Responses/StreamResponse.php @@ -2,7 +2,7 @@ namespace Outl1ne\NovaOpenAI\Capabilities\Responses; -use Illuminate\Http\Client\Response; +use GuzzleHttp\Psr7\Response; use Outl1ne\NovaOpenAI\Models\OpenAIRequest; class StreamResponse diff --git a/src/Capabilities/Threads/CreateMessage.php b/src/Capabilities/Threads/CreateMessage.php index ec89ed6..1c3b6ef 100644 --- a/src/Capabilities/Threads/CreateMessage.php +++ b/src/Capabilities/Threads/CreateMessage.php @@ -23,10 +23,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("threads/{$threadId}/messages", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("threads/{$threadId}/messages", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new MessageResponse($response), [$this, 'response']); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/CreateRun.php b/src/Capabilities/Threads/CreateRun.php index bc2c705..a83da59 100644 --- a/src/Capabilities/Threads/CreateRun.php +++ b/src/Capabilities/Threads/CreateRun.php @@ -30,10 +30,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("threads/{$threadId}/runs", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("threads/{$threadId}/runs", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new RunResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/CreateThread.php b/src/Capabilities/Threads/CreateThread.php index 2e4c31e..21900b8 100644 --- a/src/Capabilities/Threads/CreateThread.php +++ b/src/Capabilities/Threads/CreateThread.php @@ -21,11 +21,15 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post('threads', [ - 'messages' => $messages->messages ?? null, - ...$this->request->arguments, + $response = $this->openAI->http()->post('threads', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'messages' => $messages->messages ?? null, + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new ThreadResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/DeleteThread.php b/src/Capabilities/Threads/DeleteThread.php index 5830a16..0bd0e18 100644 --- a/src/Capabilities/Threads/DeleteThread.php +++ b/src/Capabilities/Threads/DeleteThread.php @@ -17,10 +17,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("threads/{$threadId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->delete("threads/{$threadId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new ThreadDeletionStatusResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/ListMessageFiles.php b/src/Capabilities/Threads/ListMessageFiles.php index 49b541c..46ed4c1 100644 --- a/src/Capabilities/Threads/ListMessageFiles.php +++ b/src/Capabilities/Threads/ListMessageFiles.php @@ -26,10 +26,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("threads/{$threadId}/messages/{$messageId}/files", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("threads/{$threadId}/messages/{$messageId}/files", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new MessageFilesResponse($response), [$this, 'response']); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/ListMessages.php b/src/Capabilities/Threads/ListMessages.php index 50124ba..9a3654d 100644 --- a/src/Capabilities/Threads/ListMessages.php +++ b/src/Capabilities/Threads/ListMessages.php @@ -25,10 +25,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("threads/{$threadId}/messages", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("threads/{$threadId}/messages", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new MessagesResponse($response), [$this, 'response']); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/ModifyMessage.php b/src/Capabilities/Threads/ModifyMessage.php index e877a2b..3d16775 100644 --- a/src/Capabilities/Threads/ModifyMessage.php +++ b/src/Capabilities/Threads/ModifyMessage.php @@ -20,10 +20,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("threads/{$threadId}/messages/{$messageId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("threads/{$threadId}/messages/{$messageId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new MessageResponse($response), [$this, 'response']); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/ModifyThread.php b/src/Capabilities/Threads/ModifyThread.php index 8943f96..d726185 100644 --- a/src/Capabilities/Threads/ModifyThread.php +++ b/src/Capabilities/Threads/ModifyThread.php @@ -19,10 +19,14 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("threads/{$threadId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->post("threads/{$threadId}", [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + ...$this->request->arguments, + ]), ]); - $response->throw(); return $this->handleResponse(new ThreadResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/RetrieveMessage.php b/src/Capabilities/Threads/RetrieveMessage.php index 6312ee5..9bce64b 100644 --- a/src/Capabilities/Threads/RetrieveMessage.php +++ b/src/Capabilities/Threads/RetrieveMessage.php @@ -18,10 +18,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("threads/{$threadId}/messages/{$messageId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("threads/{$threadId}/messages/{$messageId}", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new MessageResponse($response), [$this, 'response']); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/RetrieveMessageFile.php b/src/Capabilities/Threads/RetrieveMessageFile.php index cd89612..b3105ad 100644 --- a/src/Capabilities/Threads/RetrieveMessageFile.php +++ b/src/Capabilities/Threads/RetrieveMessageFile.php @@ -18,10 +18,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("threads/{$threadId}/messages{$messageId}/files/{$fileId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("threads/{$threadId}/messages{$messageId}/files/{$fileId}", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new MessageFileResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/RetrieveRun.php b/src/Capabilities/Threads/RetrieveRun.php index 172d288..fcb05d4 100644 --- a/src/Capabilities/Threads/RetrieveRun.php +++ b/src/Capabilities/Threads/RetrieveRun.php @@ -25,10 +25,11 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("threads/{$threadId}/runs/{$runId}", [ - ...$this->request->arguments, + $response = $this->openAI->http()->get("threads/{$threadId}/runs/{$runId}", [ + 'query' => [ + ...$this->request->arguments, + ], ]); - $response->throw(); return $this->handleResponse(new RunResponse($response)); } catch (Exception $e) { diff --git a/src/Capabilities/Threads/RetrieveThread.php b/src/Capabilities/Threads/RetrieveThread.php index a5909f2..d800426 100644 --- a/src/Capabilities/Threads/RetrieveThread.php +++ b/src/Capabilities/Threads/RetrieveThread.php @@ -18,8 +18,7 @@ public function makeRequest( $this->pending(); try { - $response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("threads/{$threadId}"); - $response->throw(); + $response = $this->openAI->http()->get("threads/{$threadId}"); return $this->handleResponse(new ThreadResponse($response)); } catch (Exception $e) { diff --git a/src/Facades/OpenAI.php b/src/Facades/OpenAI.php index 0c61177..4e24d02 100644 --- a/src/Facades/OpenAI.php +++ b/src/Facades/OpenAI.php @@ -10,7 +10,7 @@ * @method static \Outl1ne\NovaOpenAI\Capabilities\Embeddings\Embeddings embeddings() * @method static \Outl1ne\NovaOpenAI\Capabilities\Files\Files files() * @method static \Outl1ne\NovaOpenAI\Capabilities\Threads\Threads threads() - * @method static \Illuminate\Http\Client\PendingRequest http() + * @method static \GuzzleHttp\Client http() * * @see \Outl1ne\NovaOpenAI\OpenAI */ diff --git a/src/OpenAI.php b/src/OpenAI.php index 65db316..f8a4103 100644 --- a/src/OpenAI.php +++ b/src/OpenAI.php @@ -3,9 +3,8 @@ namespace Outl1ne\NovaOpenAI; use Closure; -use Illuminate\Support\Facades\Http; +use GuzzleHttp\Client; use Outl1ne\NovaOpenAI\Pricing\Pricing; -use Illuminate\Http\Client\PendingRequest; use Outl1ne\NovaOpenAI\Cache\EmbeddingsCache; use Outl1ne\NovaOpenAI\Capabilities\Chat\Chat; use Outl1ne\NovaOpenAI\Capabilities\Files\Files; @@ -56,8 +55,12 @@ public function files(): Files return new Files($this); } - public function http(): PendingRequest + public function http(): Client { - return clone Http::baseUrl($this->baseUrl)->timeout($this->timeout)->withHeaders($this->headers); + return new Client([ + 'base_uri' => $this->baseUrl, + 'timeout' => $this->timeout, + 'headers' => $this->headers, + ]); } } diff --git a/src/StreamHandler.php b/src/StreamHandler.php index 0d218a3..b8c70d2 100644 --- a/src/StreamHandler.php +++ b/src/StreamHandler.php @@ -4,13 +4,14 @@ use Closure; use Exception; -use Illuminate\Http\Client\Response; +use Illuminate\Support\Str; +use Psr\Http\Message\ResponseInterface; use Outl1ne\NovaOpenAI\Capabilities\Chat\Responses\StreamedChatChunk; use Outl1ne\NovaOpenAI\Capabilities\Chat\Responses\StreamedChatResponse; class StreamHandler { - public function __construct(protected Response $response, protected Closure $streamCallback, protected Closure $firstChunkCallback) + public function __construct(protected ResponseInterface $response, protected Closure $streamCallback, protected Closure $firstChunkCallback) { } diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8f64452 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,15 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/tests/Unit/AssistantTest.php b/tests/Unit/AssistantTest.php new file mode 100644 index 0000000..405168c --- /dev/null +++ b/tests/Unit/AssistantTest.php @@ -0,0 +1,80 @@ +create( + 'gpt-3.5-turbo', + 'Allan\'s assistant', + 'For testing purposes of nova-openai package.', + 'You are a kindergarten teacher. When asked a questions, anwser shortly and as a young child could understand.' + ); + $this->assertTrue($assistant instanceof AssistantResponse); + + $assistantModified = OpenAI::assistants()->modify($assistant->id, null, 'Allan\'s assistant!'); + $this->assertTrue($assistantModified instanceof AssistantResponse); + + $deletedAssistant = OpenAI::assistants()->delete($assistant->id); + $this->assertTrue($deletedAssistant instanceof DeleteResponse); + } + + public function test_assistant_files(): void + { + $assistant = OpenAI::assistants()->create( + 'gpt-3.5-turbo', + 'Allan\'s assistant', + 'For testing purposes of nova-openai package.', + 'You are a kindergarten teacher. When asked a questions, anwser shortly and as a young child could understand.', + [ + [ + 'type' => 'retrieval', + ], + ], + ); + $this->assertTrue($assistant instanceof AssistantResponse); + + $file = OpenAI::files()->upload( + 'sample file content', + 'test_assistant_files.txt', + 'assistants', + ); + $this->assertTrue($file instanceof FileResponse); + + $assistantFile = OpenAI::assistants()->files()->create($assistant->id, $file->id); + $this->assertTrue($assistantFile instanceof AssistantFileResponse); + + $assistantFiles = OpenAI::assistants()->files()->list($assistant->id); + $this->assertTrue($assistantFiles instanceof AssistantFileListResponse); + + $deletedAssistantFile = OpenAI::assistants()->files()->delete($assistant->id, $file->id); + $this->assertTrue($deletedAssistantFile instanceof DeleteResponse); + + // Cleanup + $deletedAssistant = OpenAI::assistants()->delete($assistant->id); + $this->assertTrue($deletedAssistant instanceof DeleteResponse); + + $deletedFile = OpenAI::files()->delete($file->id); + $this->assertTrue($deletedFile instanceof FileDeleteResponse); + } +} diff --git a/tests/Unit/ChatTest.php b/tests/Unit/ChatTest.php new file mode 100644 index 0000000..ba62d24 --- /dev/null +++ b/tests/Unit/ChatTest.php @@ -0,0 +1,55 @@ +create( + model: 'gpt-3.5-turbo', + messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), + ); + $this->assertTrue($response instanceof ChatResponse); + $this->assertIsArray($response->choices); + } + + public function test_chat_json_response(): void + { + $response = OpenAI::chat()->create( + model: 'gpt-3.5-turbo', + messages: (new Messages)->system('You are a helpful assistant.')->user('Suggest me tasty fruits as JSON array of fruits.'), + responseFormat: (new ResponseFormat)->json(), + ); + $this->assertTrue($response instanceof ChatResponse); + $this->assertIsArray($response->choices); + $this->assertJson($response->choices[0]['message']['content']); + } + + public function test_chat_stream(): void + { + $response = OpenAI::chat()->stream(function (string $newChunk, string $message) { + })->create( + model: 'gpt-3.5-turbo', + messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), + ); + $this->assertTrue($response instanceof StreamedChatResponse); + $this->assertIsArray($response->choices); + } +} diff --git a/tests/Unit/EmbeddingsTest.php b/tests/Unit/EmbeddingsTest.php new file mode 100644 index 0000000..de93a18 --- /dev/null +++ b/tests/Unit/EmbeddingsTest.php @@ -0,0 +1,41 @@ +create( + 'text-embedding-3-small', + 'The food was delicious and the waiter...' + ); + $this->assertTrue($response instanceof EmbeddingsResponse); + $this->assertIsArray($response->embedding->vector); + } + + public function test_embeddings_without_storing_output(): void + { + $response = OpenAI::embeddings()->storing(function ($model) { + $model->output = null; + return $model; + })->create( + 'text-embedding-3-small', + 'The food was delicious and the waiter...' + ); + $this->assertTrue($response->request->output === null); + } +} diff --git a/tests/Unit/FilesTest.php b/tests/Unit/FilesTest.php new file mode 100644 index 0000000..0f38db3 --- /dev/null +++ b/tests/Unit/FilesTest.php @@ -0,0 +1,54 @@ +upload( + 'sample file content', + 'file.txt', + 'assistants', + ); + $this->assertTrue($file instanceof FileResponse); + + $files = OpenAI::files()->list(); + $this->assertTrue($files instanceof FileListResponse); + + $file2 = OpenAI::files()->retrieve($file->id); + $this->assertTrue($file2 instanceof FileResponse); + + $deletedFile = OpenAI::files()->delete($file->id); + $this->assertTrue($deletedFile instanceof FileDeleteResponse); + } + + // public function test_file_content_retrieval(): void + // { + // $file = OpenAI::files()->upload( + // 'sample file content', + // 'fine-tune.txt', + // 'fine-tune', + // ); + // $this->assertTrue($file instanceof FileResponse); + + // $fileContent = OpenAI::files()->retrieveContent($file->id); + // $this->assertTrue($fileContent instanceof FileContentResponse); + // } +} diff --git a/tests/Unit/ThreadsTest.php b/tests/Unit/ThreadsTest.php new file mode 100644 index 0000000..24ae9b8 --- /dev/null +++ b/tests/Unit/ThreadsTest.php @@ -0,0 +1,65 @@ +create( + 'gpt-3.5-turbo', + 'Allan', + 'nova-openai testimiseks', + 'You are a kindergarten teacher. When asked a questions, anwser shortly and as a young child could understand.' + ); + $this->assertTrue($assistant instanceof AssistantResponse); + + $thread = OpenAI::threads() + ->create((new Messages)->user('What is your purpose in one short sentence?')); + $this->assertTrue($thread instanceof ThreadResponse); + + $message = OpenAI::threads()->messages() + ->create($thread->id, Message::user('How does AI work? Explain it in simple terms in one sentence.')); + $this->assertTrue($message instanceof MessageResponse); + + $run = OpenAI::threads()->run()->execute($thread->id, $assistant->id); + $this->assertTrue($run instanceof RunResponse); + + $status = null; + while ($status !== 'completed') { + $runStatus = OpenAI::threads()->run()->retrieve($thread->id, $run->id); + $this->assertTrue($runStatus instanceof RunResponse); + sleep(1); + } + + $messages = OpenAI::threads()->messages()->list($thread->id); + $this->assertTrue($messages instanceof MessagesResponse); + + // cleanup + $deletedThread = OpenAI::threads()->delete($thread->id); + $this->assertTrue($deletedThread instanceof ThreadDeletionStatusResponse); + $deletedAssistant = OpenAI::assistants()->delete($assistant->id); + $this->assertTrue($deletedAssistant instanceof DeleteResponse); + } +}