From a05c7843ec1fd48fa6bb503480177976b8b23b27 Mon Sep 17 00:00:00 2001 From: Allan Tatter Date: Mon, 21 Oct 2024 09:58:07 +0300 Subject: [PATCH] feat(responses): Add json and thread methods to MessagesResponse feat(parameters): Add StaticMake trait to Messages class test(assistant): Add test for assistant response JSON schema format test(chat): Use Messages::make() for messages creation in ChatTest --- README.md | 83 +++++++++++++++++-- .../Threads/Parameters/Messages.php | 4 + .../Threads/Responses/MessagesResponse.php | 16 ++++ .../Threads/Responses/RunResponse.php | 5 +- tests/Unit/AssistantTest.php | 27 ++++++ tests/Unit/ChatTest.php | 10 +-- tests/Unit/ThreadsTest.php | 8 +- 7 files changed, 131 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2d53069..b71d3eb 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ $deletedFile = OpenAI::files()->delete($file->id); ```php $response = OpenAI::chat()->create( model: 'gpt-3.5-turbo', - messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), -); + messages: Messages::make()->system('You are a helpful assistant.')->user('Hello!'), +)->json(); ``` Enable JSON response formatting: @@ -104,9 +104,58 @@ Enable JSON response formatting: ```php $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(), -); + messages: Messages::make()->system('You are a helpful assistant.')->user('Suggest me tasty fruits as JSON array of fruits.'), + responseFormat: ResponseFormat::make()->json(), +)->json(); +``` + +JSON Structured Outputs example: + +```php +$response = OpenAI::chat()->create( + model: 'gpt-4o-mini', + messages: Messages::make()->system('You are a helpful assistant.')->user('Suggest me 10 tasty fruits.'), + responseFormat: ResponseFormat::make()->jsonSchema( + JsonObject::make() + ->property('fruits', JsonArray::make()->items(JsonString::make())) + ->property('number_of_fruits_in_response', JsonInteger::make()) + ->property('number_of_fruits_in_response_divided_by_three', JsonNumber::make()) + ->property('is_number_of_fruits_in_response_even', JsonBoolean::make()) + ->property('fruit_most_occurring_color', JsonEnum::make()->enums(['red', 'green', 'blue'])) + ->property( + 'random_integer_or_string_max_one_character', + JsonAnyOf::make() + ->schema(JsonInteger::make()) + ->schema(JsonString::make()) + ), + ), +)->json(); +``` + +With raw JSON schema: + +```php +$response = OpenAI::chat()->create( + model: 'gpt-4o-mini', + messages: Messages::make()->system('You are a helpful assistant.')->user('Suggest me tasty fruits.'), + responseFormat: ResponseFormat::make()->jsonSchema([ + 'name' => 'response', + 'strict' => true, + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'fruits' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + ], + 'additionalProperties' => false, + 'required' => ['fruits'], + ], + ]), +)->json(); ``` #### Streaming @@ -116,7 +165,7 @@ $response = OpenAI::chat()->stream(function (string $newChunk, string $message) echo $newChunk; })->create( model: 'gpt-3.5-turbo', - messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), + messages: Messages::make()->system('You are a helpful assistant.')->user('Hello!'), ); ``` @@ -164,6 +213,23 @@ Retrieving a file content. $fileContent = OpenAI::files()->retrieveContent($file->id); ``` +### Vector Stores + +```php +$filePath = __DIR__ . '/../test.txt'; +$file = OpenAI::files()->upload( + file_get_contents($filePath), + basename($filePath), + 'assistants', +); + +$vectorStore = OpenAI::vectorStores()->create([$file->id]); +$vectorStores = OpenAI::vectorStores()->list(); +$vectorStoreRetrieved = OpenAI::vectorStores()->retrieve($vectorStore->id); +$vectorStoreModified = OpenAI::vectorStores()->modify($vectorStore->id, 'Modified vector store'); +$vectorStoreDeleted = OpenAI::vectorStores()->delete($vectorStore->id); +``` + ### Threads ```php @@ -174,11 +240,10 @@ $assistant = OpenAI::assistants()->create( 'You are a kindergarten teacher. When asked a questions, anwser shortly and as a young child could understand.' ); $thread = OpenAI::threads() - ->create((new ThreadMessages)->user('What is your purpose in one short sentence?')); + ->create(Messages::make()->user('What is your purpose in one short sentence?')); $message = OpenAI::threads()->messages() ->create($thread->id, ThreadMessage::user('How does AI work? Explain it in simple terms in one sentence.')); -$run = OpenAI::threads()->run()->execute($thread->id, $assistant->id)->wait(); -$messages = OpenAI::threads()->messages()->list($thread->id); +$response = OpenAI::threads()->run()->execute($thread->id, $assistant->id)->wait()->json(); // cleanup $deletedThread = OpenAI::threads()->delete($thread->id); diff --git a/src/Capabilities/Threads/Parameters/Messages.php b/src/Capabilities/Threads/Parameters/Messages.php index 6d44a53..1bacc99 100644 --- a/src/Capabilities/Threads/Parameters/Messages.php +++ b/src/Capabilities/Threads/Parameters/Messages.php @@ -2,8 +2,12 @@ namespace Outl1ne\NovaOpenAI\Capabilities\Threads\Parameters; +use Outl1ne\NovaOpenAI\Traits\StaticMake; + class Messages { + use StaticMake; + public array $messages = []; public function user(string $content, ?array $attachments = null, ?array $metadata = null): self diff --git a/src/Capabilities/Threads/Responses/MessagesResponse.php b/src/Capabilities/Threads/Responses/MessagesResponse.php index 650eb72..1c6b116 100644 --- a/src/Capabilities/Threads/Responses/MessagesResponse.php +++ b/src/Capabilities/Threads/Responses/MessagesResponse.php @@ -18,4 +18,20 @@ public function __construct(...$arguments) $this->appendMeta('has_more', $this->data['has_more']); $this->messages = $this->data['data']; } + + public function json(): ?object + { + return json_decode($this->messages[0]['content'][0]['text']['value']); + } + + public function thread(): ?array + { + return collect($this->messages)->map(function ($message) { + $messageValue = $message['content'][0]['text']['value']; + return [ + 'role' => $message['role'], + 'message' => json_validate($messageValue) ? json_decode($messageValue) : $messageValue, + ]; + })->toArray(); + } } diff --git a/src/Capabilities/Threads/Responses/RunResponse.php b/src/Capabilities/Threads/Responses/RunResponse.php index f27218a..b321e37 100644 --- a/src/Capabilities/Threads/Responses/RunResponse.php +++ b/src/Capabilities/Threads/Responses/RunResponse.php @@ -44,7 +44,7 @@ public function __construct(...$arguments) $this->appendMeta('required_action', $this->data['required_action']); } - public function wait(?callable $errorCallback = null): RunResponse + public function wait(?callable $errorCallback = null): MessagesResponse { $status = null; while ($status !== 'completed') { @@ -63,6 +63,7 @@ public function wait(?callable $errorCallback = null): RunResponse // sleep for 100ms usleep(100_000); } - return $runStatus; + + return OpenAI::threads()->messages()->list($this->data['thread_id']); } } diff --git a/tests/Unit/AssistantTest.php b/tests/Unit/AssistantTest.php index 720376d..187f3e5 100644 --- a/tests/Unit/AssistantTest.php +++ b/tests/Unit/AssistantTest.php @@ -4,9 +4,13 @@ use Outl1ne\NovaOpenAI\Facades\OpenAI; use Orchestra\Testbench\Concerns\WithWorkbench; +use Outl1ne\NovaOpenAI\Capabilities\Threads\Parameters\Messages; use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\FileResponse; +use Outl1ne\NovaOpenAI\Capabilities\Chat\Parameters\ResponseFormat; use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\FileDeleteResponse; use Outl1ne\NovaOpenAI\Capabilities\Assistants\Responses\DeleteResponse; +use Outl1ne\NovaOpenAI\Capabilities\Chat\Parameters\JsonSchema\JsonObject; +use Outl1ne\NovaOpenAI\Capabilities\Chat\Parameters\JsonSchema\JsonString; use Outl1ne\NovaOpenAI\Capabilities\Assistants\Responses\AssistantResponse; use Outl1ne\NovaOpenAI\Capabilities\Assistants\Responses\AssistantFileResponse; use Outl1ne\NovaOpenAI\Capabilities\Assistants\Responses\AssistantListResponse; @@ -81,4 +85,27 @@ public function test_assistant_files(): void $deletedFile = OpenAI::files()->delete($file->id); $this->assertTrue($deletedFile instanceof FileDeleteResponse); } + + public function test_assistant_response_format_json_schema(): void + { + $assistant = OpenAI::assistants()->create( + 'gpt-4o-mini', + '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.', + responseFormat: ResponseFormat::make()->jsonSchema( + JsonObject::make() + ->property('answer', JsonString::make()) + ), + ); + + $thread = OpenAI::threads() + ->create(Messages::make()->user('What is your purpose in one short sentence?')); + $response = OpenAI::threads()->run()->execute($thread->id, $assistant->id)->wait(); + + $this->assertIsString($response->json()?->answer); + + OpenAI::threads()->delete($thread->id); + OpenAI::assistants()->delete($assistant->id); + } } diff --git a/tests/Unit/ChatTest.php b/tests/Unit/ChatTest.php index 829b4c6..47d9c7e 100644 --- a/tests/Unit/ChatTest.php +++ b/tests/Unit/ChatTest.php @@ -32,7 +32,7 @@ public function test_chat(): void { $response = OpenAI::chat()->create( model: 'gpt-4o-mini', - messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), + messages: Messages::make()->system('You are a helpful assistant.')->user('Hello!'), ); $this->assertTrue($response instanceof ChatResponse); $this->assertIsArray($response->choices); @@ -42,8 +42,8 @@ public function test_chat_json_response(): void { $response = OpenAI::chat()->create( model: 'gpt-4o-mini', - messages: (new Messages)->system('You are a helpful assistant.')->user('Suggest me tasty fruits as JSON array of fruits.'), - responseFormat: (new ResponseFormat)->json(), + messages: Messages::make()->system('You are a helpful assistant.')->user('Suggest me tasty fruits as JSON array of fruits.'), + responseFormat: ResponseFormat::make()->json(), ); $this->assertTrue($response instanceof ChatResponse); $this->assertIsArray($response->choices); @@ -106,7 +106,7 @@ public function test_chat_stream(): void { $response = OpenAI::chat()->stream(function (string $newChunk, string $message) {})->create( model: 'gpt-4o-mini', - messages: (new Messages)->system('You are a helpful assistant.')->user('Hello!'), + messages: Messages::make()->system('You are a helpful assistant.')->user('Hello!'), ); $this->assertTrue($response instanceof StreamedChatResponse); $this->assertIsArray($response->choices); @@ -116,7 +116,7 @@ public function test_chat_image_url(): void { $response = OpenAI::chat()->create( model: 'gpt-4o-mini', - messages: (new Messages)->system('You are a helpful assistant.')->user([ + messages: Messages::make()->system('You are a helpful assistant.')->user([ [ 'type' => 'text', 'text' => 'Describe what\'s on the attached photo', diff --git a/tests/Unit/ThreadsTest.php b/tests/Unit/ThreadsTest.php index 4e40c68..63b40ef 100644 --- a/tests/Unit/ThreadsTest.php +++ b/tests/Unit/ThreadsTest.php @@ -37,7 +37,7 @@ public function test_threads(): void $this->assertTrue($assistant instanceof AssistantResponse); $thread = OpenAI::threads() - ->create((new Messages)->user('What is your purpose in one short sentence?')); + ->create(Messages::make()->user('What is your purpose in one short sentence?')); $this->assertTrue($thread instanceof ThreadResponse); $message = OpenAI::threads()->messages() @@ -59,11 +59,7 @@ public function test_threads(): void ])); $this->assertTrue($message2 instanceof MessageResponse); - $run = OpenAI::threads()->run()->execute($thread->id, $assistant->id)->wait(); - $this->assertTrue($run instanceof RunResponse); - $this->assertEquals($run->meta['status'], 'completed'); - - $messages = OpenAI::threads()->messages()->list($thread->id); + $messages = OpenAI::threads()->run()->execute($thread->id, $assistant->id)->wait(); $this->assertTrue($messages instanceof MessagesResponse); // cleanup