From 93313544322c078c7a2ee7d06a8dabdf91436271 Mon Sep 17 00:00:00 2001 From: TJ Miller Date: Sun, 19 Jan 2025 16:26:51 -0500 Subject: [PATCH] Deepseek structured output (#139) --- docs/components/ProviderSupport.vue | 2 +- docs/providers/deepseek.md | 4 - src/Providers/DeepSeek/DeepSeek.php | 8 +- .../DeepSeek/Handlers/Structured.php | 92 +++++++++++++++++++ tests/Fixtures/deepseek/structured-1.json | 1 + tests/Providers/DeepSeek/StructuredTest.php | 35 ++++--- 6 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 src/Providers/DeepSeek/Handlers/Structured.php create mode 100644 tests/Fixtures/deepseek/structured-1.json diff --git a/docs/components/ProviderSupport.vue b/docs/components/ProviderSupport.vue index b34c3d0..0a35d07 100644 --- a/docs/components/ProviderSupport.vue +++ b/docs/components/ProviderSupport.vue @@ -186,7 +186,7 @@ export default { { name: "DeepSeek", text: Supported, - structured: Planned, + structured: Supported, embeddings: Unsupported, image: Unsupported, tools: Supported, diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md index 50b24ef..6c5dc46 100644 --- a/docs/providers/deepseek.md +++ b/docs/providers/deepseek.md @@ -10,10 +10,6 @@ ## Provider-specific options ## Limitations -### Structured Ouput - -Does not support structured output. - ### Embeddings Does not support embeddings. diff --git a/src/Providers/DeepSeek/DeepSeek.php b/src/Providers/DeepSeek/DeepSeek.php index b85d676..d708f1b 100644 --- a/src/Providers/DeepSeek/DeepSeek.php +++ b/src/Providers/DeepSeek/DeepSeek.php @@ -9,6 +9,7 @@ use EchoLabs\Prism\Embeddings\Request as EmbeddingsRequest; use EchoLabs\Prism\Embeddings\Response as EmbeddingsResponse; use EchoLabs\Prism\Exceptions\PrismException; +use EchoLabs\Prism\Providers\DeepSeek\Handlers\Structured; use EchoLabs\Prism\Providers\DeepSeek\Handlers\Text; use EchoLabs\Prism\Structured\Request as StructuredRequest; use EchoLabs\Prism\Text\Request as TextRequest; @@ -36,7 +37,12 @@ public function text(TextRequest $request): ProviderResponse #[\Override] public function structured(StructuredRequest $request): ProviderResponse { - throw PrismException::unsupportedProviderAction(__FUNCTION__, class_basename($this)); + $handler = new Structured($this->client( + $request->clientOptions, + $request->clientRetry + )); + + return $handler->handle($request); } #[\Override] diff --git a/src/Providers/DeepSeek/Handlers/Structured.php b/src/Providers/DeepSeek/Handlers/Structured.php new file mode 100644 index 0000000..a890135 --- /dev/null +++ b/src/Providers/DeepSeek/Handlers/Structured.php @@ -0,0 +1,92 @@ +appendMessageForJsonMode($request); + + $response = $this->sendRequest($request); + + $this->validateResponse($response); + + return $this->createResponse($response); + } catch (Throwable $e) { + throw PrismException::providerRequestError($request->model, $e); + } + } + + public function sendRequest(Request $request): Response + { + return $this->client->post( + 'chat/completions', + array_merge([ + 'model' => $request->model, + 'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(), + 'max_completion_tokens' => $request->maxTokens ?? 2048, + ], array_filter([ + 'temperature' => $request->temperature, + 'top_p' => $request->topP, + 'response_format' => ['type' => 'json_object'], + ])) + ); + } + + protected function validateResponse(Response $response): void + { + $data = $response->json(); + + if (! $data) { + throw PrismException::providerResponseError(vsprintf( + 'DeepSeek Error: %s', + [ + (string) $response->getBody(), + ] + )); + } + } + + protected function createResponse(Response $response): ProviderResponse + { + $data = $response->json(); + + return new ProviderResponse( + text: data_get($data, 'choices.0.message.content') ?? '', + toolCalls: [], + usage: new Usage( + data_get($data, 'usage.prompt_tokens'), + data_get($data, 'usage.completion_tokens'), + ), + finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')), + responseMeta: new ResponseMeta( + id: data_get($data, 'id'), + model: data_get($data, 'model'), + ), + ); + } + + protected function appendMessageForJsonMode(Request $request): Request + { + return $request->addMessage(new SystemMessage(sprintf( + "Respond with JSON that matches the following schema: \n %s", + json_encode($request->schema->toArray(), JSON_PRETTY_PRINT) + ))); + } +} diff --git a/tests/Fixtures/deepseek/structured-1.json b/tests/Fixtures/deepseek/structured-1.json new file mode 100644 index 0000000..5972965 --- /dev/null +++ b/tests/Fixtures/deepseek/structured-1.json @@ -0,0 +1 @@ +{"id":"1920de37-0bb1-4cf8-9c4e-d73ad8d73d6a","object":"chat.completion","created":1737321412,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\"weather\": \"75º\", \"game_time\": \"3pm\", \"coat_required\": false}"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":187,"completion_tokens":26,"total_tokens":213,"prompt_cache_hit_tokens":128,"prompt_cache_miss_tokens":59},"system_fingerprint":"fp_3a5770e1b4"} \ No newline at end of file diff --git a/tests/Providers/DeepSeek/StructuredTest.php b/tests/Providers/DeepSeek/StructuredTest.php index fc88aa1..260e7e8 100644 --- a/tests/Providers/DeepSeek/StructuredTest.php +++ b/tests/Providers/DeepSeek/StructuredTest.php @@ -3,29 +3,40 @@ declare(strict_types=1); use EchoLabs\Prism\Enums\Provider; -use EchoLabs\Prism\Exceptions\PrismException; use EchoLabs\Prism\Prism; +use EchoLabs\Prism\Schema\BooleanSchema; use EchoLabs\Prism\Schema\ObjectSchema; use EchoLabs\Prism\Schema\StringSchema; +use Tests\Fixtures\FixtureResponse; -beforeEach(function (): void { - config()->set('prism.providers.deepseek.api_key', env('DEEPSEEK_API_KEY')); -}); - -it('Throws exception for structured', function (): void { - $this->expectException(PrismException::class); +it('returns structured output', function (): void { + FixtureResponse::fakeResponseSequence('v1/chat/completions', 'deepseek/structured'); $schema = new ObjectSchema( - 'user', - 'a user object', + 'output', + 'the output object', [ - new StringSchema('name', 'The user\'s name'), + new StringSchema('weather', 'The weather forecast'), + new StringSchema('game_time', 'The tigers game time'), + new BooleanSchema('coat_required', 'whether a coat is required'), ], + ['weather', 'game_time', 'coat_required'] ); - Prism::structured() + $response = Prism::structured() ->withSchema($schema) ->using(Provider::DeepSeek, 'deepseek-chat') - ->withPrompt('Hi, my name is TJ') + ->withSystemPrompt('The tigers game is at 3pm in Detroit, the temperature is expected to be 75º') + ->withPrompt('What time is the tigers game today and should I wear a coat?') ->generate(); + + expect($response->structured)->toBeArray(); + expect($response->structured)->toHaveKeys([ + 'weather', + 'game_time', + 'coat_required', + ]); + expect($response->structured['weather'])->toBeString(); + expect($response->structured['game_time'])->toBeString(); + expect($response->structured['coat_required'])->toBeBool(); });