Skip to content

Commit

Permalink
Deepseek structured output (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
sixlive authored Jan 19, 2025
1 parent 1aafb91 commit 9331354
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 18 deletions.
2 changes: 1 addition & 1 deletion docs/components/ProviderSupport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default {
{
name: "DeepSeek",
text: Supported,
structured: Planned,
structured: Supported,
embeddings: Unsupported,
image: Unsupported,
tools: Supported,
Expand Down
4 changes: 0 additions & 4 deletions docs/providers/deepseek.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
## Provider-specific options

## Limitations
### Structured Ouput

Does not support structured output.

### Embeddings

Does not support embeddings.
Expand Down
8 changes: 7 additions & 1 deletion src/Providers/DeepSeek/DeepSeek.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down
92 changes: 92 additions & 0 deletions src/Providers/DeepSeek/Handlers/Structured.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace EchoLabs\Prism\Providers\DeepSeek\Handlers;

use EchoLabs\Prism\Exceptions\PrismException;
use EchoLabs\Prism\Providers\DeepSeek\Maps\FinishReasonMap;
use EchoLabs\Prism\Providers\DeepSeek\Maps\MessageMap;
use EchoLabs\Prism\Structured\Request;
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
use EchoLabs\Prism\ValueObjects\ProviderResponse;
use EchoLabs\Prism\ValueObjects\ResponseMeta;
use EchoLabs\Prism\ValueObjects\Usage;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Throwable;

class Structured
{
public function __construct(protected PendingRequest $client) {}

public function handle(Request $request): ProviderResponse
{
try {
$request = $this->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)
)));
}
}
1 change: 1 addition & 0 deletions tests/Fixtures/deepseek/structured-1.json
Original file line number Diff line number Diff line change
@@ -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"}
35 changes: 23 additions & 12 deletions tests/Providers/DeepSeek/StructuredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

0 comments on commit 9331354

Please sign in to comment.