Skip to content
Merged
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,30 @@ $imageMessage = new Image($imageBinaryContent);
$mimeType = $imageMessage->getMimeType(); // Get the MIME type of the image
```

## Schema and Schema Objects

You can use the `Schema` class to define a schema for a structured output. The `Schema` class utilizes `SchemaObject`s to define each property of the schema, following the [JSON Schema](https://json-schema.org/) format.

```php
use Utopia\Agents\Schema\Schema;
use Utopia\Agents\Schema\SchemaObject;

$object = new SchemaObject();
$object->addProperty('location', [
'type' => SchemaObject::TYPE_STRING,
'description' => 'The city and state, e.g. San Francisco, CA',
]);

$schema = new Schema(
name: 'get_weather',
description: 'Get the current weather in a given location in well structured JSON',
object: $object,
required: $object->getNames()
);

$agent->setSchema($schema);
```

## Tests

To run all unit tests, use the following Docker command:
Expand Down
7 changes: 7 additions & 0 deletions src/Agents/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ abstract public function getModel(): string;
*/
abstract public function setModel(string $model): self;

/**
* Check if the model supports JSON schema
*
* @return bool
*/
abstract public function isSchemaSupported(): bool;

/**
* Get the current agent
*
Expand Down
90 changes: 79 additions & 11 deletions src/Agents/Adapters/Anthropic.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Utopia\Agents\Adapter;
use Utopia\Agents\Message;
use Utopia\Agents\Messages\Text;
use Utopia\Agents\Schema;
use Utopia\Fetch\Chunk;
use Utopia\Fetch\Client;

Expand Down Expand Up @@ -85,6 +86,16 @@ public function __construct(
$this->setModel($model);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return true;
}

/**
* Send a message to the Anthropic API
*
Expand Down Expand Up @@ -143,25 +154,54 @@ public function send(array $messages, ?callable $listener = null): Message
];
}

$schema = $this->getAgent()->getSchema();
$payload = [
'model' => $this->model,
'system' => $systemMessages,
'messages' => $formattedMessages,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'stream' => true,
];

if (isset($schema)) {
$payload['tools'] = [
[
'name' => $schema->getName(),
'description' => $schema->getDescription(),
'input_schema' => [
'type' => 'object',
'properties' => $schema->getProperties(),
'required' => $schema->getRequired(),
],
],
];
$payload['tool_choice'] = [
'type' => 'tool',
'name' => $schema->getName(),
];
$payload['stream'] = false;
} else {
$payload['stream'] = true;
}

$content = '';
$response = $client->fetch(
'https://api.anthropic.com/v1/messages',
Client::METHOD_POST,
$payload,
[],
function ($chunk) use (&$content, $listener) {
$content .= $this->process($chunk, $listener);
}
);
if ($payload['stream']) {
$response = $client->fetch(
'https://api.anthropic.com/v1/messages',
Client::METHOD_POST,
$payload,
[],
function ($chunk) use (&$content, $listener) {
$content .= $this->process($chunk, $listener);
}
);
} else {
$response = $client->fetch(
'https://api.anthropic.com/v1/messages',
Client::METHOD_POST,
$payload,
);
}

if ($response->getStatusCode() >= 400) {
throw new \Exception(
Expand All @@ -170,7 +210,35 @@ function ($chunk) use (&$content, $listener) {
);
}

return new Text($content);
if ($payload['stream']) {
return new Text($content);
}

$body = $response->getBody();
$json = is_string($body) ? json_decode($body, true) : null;

$text = '';
if (is_array($json) && $schema !== null) {
$content = $json['content'] ?? null;
if (is_array($content) && isset($content[0])) {
$item = $content[0];
if (is_array($item) &&
isset($item['type']) && $item['type'] === 'tool_use' &&
isset($item['name']) && $item['name'] === $schema->getName()) {
$text = $item['input'];
}
}
}

if ($text === '') {
$text = is_string($body) ? $body : (is_array($json) ? json_encode($json) : '');
}

if (is_array($text)) {
$text = json_encode($text);
}

return new Text($text);
}

/**
Expand Down
25 changes: 22 additions & 3 deletions src/Agents/Adapters/Deepseek.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ public function __construct(
$this->setModel($model);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return true;
}

/**
* Send a message to the Deepseek API
*
Expand Down Expand Up @@ -109,6 +119,11 @@ public function send(array $messages, ?callable $listener = null): Message
$systemMessage = $this->getAgent()->getDescription().
(empty($instructions) ? '' : "\n\n".implode("\n\n", $instructions));

$schema = $this->getAgent()->getSchema();
if ($schema !== null) {
$systemMessage .= "\n\n"."USE THE JSON SCHEMA BELOW TO GENERATE A VALID JSON RESPONSE: \n".$schema->toJson();
}

if (! empty($systemMessage)) {
array_unshift($formattedMessages, [
'role' => 'system',
Expand All @@ -124,6 +139,12 @@ public function send(array $messages, ?callable $listener = null): Message
'stream' => true,
];

if ($schema !== null) {
$payload['response_format'] = [
'type' => 'json_object',
];
}

$content = '';
$response = $client->fetch(
'https://api.deepseek.com/chat/completions',
Expand All @@ -142,9 +163,7 @@ function ($chunk) use (&$content, $listener) {
);
}

$message = new Text($content);

return $message;
return new Text($content);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Agents/Adapters/Gemini.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public function __construct(
$this->setModel($model);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return false;
}

/**
* Send a message to the API
*
Expand Down
78 changes: 61 additions & 17 deletions src/Agents/Adapters/OpenAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Utopia\Agents\Adapter;
use Utopia\Agents\Message;
use Utopia\Agents\Messages\Text;
use Utopia\Agents\Schema;
use Utopia\Fetch\Chunk;
use Utopia\Fetch\Client;

Expand Down Expand Up @@ -103,6 +104,16 @@ public function __construct(
$this->setModel($model);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return true;
}

/**
* Send a message to the API
*
Expand Down Expand Up @@ -154,9 +165,28 @@ public function send(array $messages, ?callable $listener = null): Message
'model' => $this->model,
'messages' => $formattedMessages,
'temperature' => $this->temperature,
'stream' => true,
];

$schema = $this->getAgent()->getSchema();
if ($schema !== null) {
$payload['response_format'] = [
'type' => 'json_schema',
'json_schema' => [
'name' => $schema->getName(),
'strict' => true,
'schema' => [
'type' => 'object',
'properties' => $schema->getProperties(),
'required' => $schema->getRequired(),
'additionalProperties' => false,
],
],
];
$payload['stream'] = false;
} else {
$payload['stream'] = true;
}

// Use 'max_completion_tokens' for o-series models, else 'max_tokens'
$oSeriesModels = [
self::MODEL_O3,
Expand All @@ -170,26 +200,40 @@ public function send(array $messages, ?callable $listener = null): Message
}

$content = '';
$response = $client->fetch(
$this->endpoint,
Client::METHOD_POST,
$payload,
[],
function ($chunk) use (&$content, $listener) {
$content .= $this->process($chunk, $listener);
}
);

if ($response->getStatusCode() >= 400) {
throw new \Exception(
ucfirst($this->getName()).' API error: '.$content,
$response->getStatusCode()
if ($payload['stream']) {
$response = $client->fetch(
$this->endpoint,
Client::METHOD_POST,
$payload,
[],
function ($chunk) use (&$content, $listener) {
$content .= $this->process($chunk, $listener);
}
);
}

$message = new Text($content);
if ($response->getStatusCode() >= 400) {
throw new \Exception(
ucfirst($this->getName()).' API error: '.$content,
$response->getStatusCode()
);
}
} else {
$response = $client->fetch(
$this->endpoint,
Client::METHOD_POST,
$payload,
);
$body = $response->getBody();
$json = is_string($body) ? json_decode($body, true) : null;
if (is_array($json) && isset($json['choices'][0]['message']['content'])) {
$content = $json['choices'][0]['message']['content'];
} else {
throw new \Exception('Invalid response format received from the API');
}
}

return $message;
return new Text($content);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Agents/Adapters/Perplexity.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ public function __construct(
);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return false;
}

/**
* Get available models
*
Expand Down
10 changes: 10 additions & 0 deletions src/Agents/Adapters/XAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public function __construct(
);
}

/**
* Check if the model supports JSON schema
*
* @return bool
*/
public function isSchemaSupported(): bool
{
return false;
}

/**
* Get available models
*
Expand Down
Loading