Skip to content

Instantly share code, notes, and snippets.

@fului
Last active January 28, 2025 19:55
Show Gist options
  • Select an option

  • Save fului/ffea2057cb31f175fb9bb83c8683c0ad to your computer and use it in GitHub Desktop.

Select an option

Save fului/ffea2057cb31f175fb9bb83c8683c0ad to your computer and use it in GitHub Desktop.
OpenAI fluent layer for openai-php/laravel
<?php
namespace App\Services;
use Illuminate\Support\Collection;
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
use OpenAI\Responses\Chat\CreateResponse;
use RuntimeException;
class OpenAIResponse
{
public function __construct(
public readonly CreateResponse $response,
public readonly ?array $schema = null
) {}
/**
* Get the choices from the response.
*/
public function choices(): Collection
{
$choices = $this->response['choices'] ?? [];
return collect($choices);
}
/**
* Get the first message from the response.
*/
public function firstMessage(): ?string
{
// return $this->choices()[0]['message']['content'] ?? null;
return $this->choices()->first()['message']['content'] ?? null;
}
/**
* Validate the first message against the JSON schema and sanitize it.
*
* @return array The validated and sanitized response adhering to the schema.
*/
public function firstMessageViaSchema(): array
{
if (!$this->schema) {
throw new RuntimeException('No schema provided for validation.');
}
$message = $this->firstMessage();
if (!$message) {
// Return all fields as null if there's no content.
return $this->defaultSchemaValues();
}
// Decode JSON response
$data = json_decode($message);
if (json_last_error() !== JSON_ERROR_NONE) {
// Return all fields as null if the response is not valid JSON.
return $this->defaultSchemaValues();
}
// Validate the response
$validator = new Validator();
$validator->validate($data, (object) $this->schema, Constraint::CHECK_MODE_APPLY_DEFAULTS);
// Return sanitized data or all-null values if validation fails
return $validator->isValid() ? $this->sanitizeResponse((array) $data) : $this->defaultSchemaValues();
}
/**
* Sanitize the response to ensure it adheres to the schema.
*
* @param array $data The response data.
* @return array The sanitized response.
*/
private function sanitizeResponse(array $data): array
{
$sanitized = [];
foreach ($this->schema['properties'] as $field => $definition) {
$sanitized[$field] = $data[$field] ?? null;
}
return $sanitized;
}
/**
* Return a default response with all fields set to null based on the schema.
*
* @return array
*/
private function defaultSchemaValues(): array
{
$defaults = [];
foreach ($this->schema['properties'] as $field => $definition) {
$defaults[$field] = null;
}
return $defaults;
}
/**
* Get the raw response.
*/
public function raw(): CreateResponse
{
return $this->response;
}
/**
* Get the response as JSON.
*/
public function toJson(): string
{
return json_encode($this->response);
}
/**
* Get token usage stats.
*/
public function usage(): ?array
{
return $this->response['usage'] ?? null;
}
/**
* Get the completion time.
*/
public function completionTime(): ?string
{
return $this->response['created'] ? date('c', $this->response['created']) : null;
}
/**
* Get the status of the response.
*/
public function status(): string
{
return isset($this->response['error']) ? 'error' : 'success';
}
/**
* Get the error message, if any.
*/
public function errorMessage(): ?string
{
return $this->response['error']['message'] ?? null;
}
/**
* Magic method to return the response in the specified format.
*/
public function __toString(): string
{
return print_r($this->response, true);
}
/**
* Magic method to return the response as JSON.
*/
public function __toJson(): string
{
return $this->toJson();
}
}
<?php
namespace App\Services;
use OpenAI\Laravel\Facades\OpenAI;
class OpenAIService
{
private string $model;
private array $messages = [];
private array $options = [];
private ?string $image = null;
private bool $responseFormatJson = false;
private array $format = [];
private ?array $schema = null;
private ?int $tokens = null;
private float $temperature = 1.0;
private array $stopSequences = [];
private float $topP = 1.0;
public function __construct()
{
$this->model = config('openai.default_model');
}
public static function chat(): self
{
return new self;
}
/**
* Set the model to use.
*/
public function withModel(string $model): self
{
$this->model = $model;
return $this;
}
/**
* Add a message (system or user).
*/
private function addMessage(string $role, string|array $content): self
{
$this->messages[] = [
'role' => $role,
// 'content' => is_array($content) ? json_encode($content) : $content,
'content' => $content,
];
return $this;
}
/**
* Add a system message.
*/
public function system(string|array $message): self
{
return $this->addMessage('system', $message);
}
/**
* Add a user message.
*/
public function user(string|array $message): self
{
return $this->addMessage('user', $message);
}
/**
* Attach an image in base64 format.
*/
public function withImage(string $base64Image): self
{
return $this->user([
[
'type' => 'image_url',
'image_url' => [
'url' => $base64Image,
],
],
]);
}
/**
* Specify the desired response format.
*/
public function withJsonFormat(?array $format = null, string $prefixMessage = 'JSON Response format: '): self
{
$this->options['response_format'] = [
'type' => 'json_object',
];
if (! empty($format)) {
$this->user($prefixMessage . json_encode($format));
}
return $this;
}
public function withJsonSchema(array $schema): self
{
$this->schema = $schema;
$this->options['response_format'] = [
'type' => 'json_schema',
'json_schema' => $schema,
];
return $this;
}
/**
* Limit the number of tokens.
*/
public function usingTokens(int $tokens): self
{
$this->tokens = $tokens;
return $this;
}
/**
* Set the temperature for randomness.
*/
public function withTemperature(float $temperature): self
{
$this->temperature = $temperature;
return $this;
}
/**
* Add stop sequences.
*/
public function withStopSequences(array $stopSequences): self
{
$this->stopSequences = $stopSequences;
return $this;
}
/**
* Set the top-p value.
*/
public function withTopP(float $topP): self
{
$this->topP = $topP;
return $this;
}
public function debug(): array
{
$options = [
'model' => $this->model,
'messages' => $this->messages,
'response_format' => null,
'max_tokens' => $this->tokens,
'temperature' => $this->temperature,
'top_p' => $this->topP,
'stop' => $this->stopSequences,
];
$options = array_merge($options, $this->options);
// remove items that are null, 0 or []
$options = array_filter($options, fn ($value) => $value !== null && $value !== 0 && $value !== []);
// ksort($options);
return $options;
}
/**
* Fetch the response from OpenAI.
*/
public function fetch(): OpenAIResponse
{
$options = [
'model' => $this->model,
'messages' => $this->messages,
'max_tokens' => $this->tokens,
'temperature' => $this->temperature,
'top_p' => $this->topP,
'stop' => $this->stopSequences,
];
$options = array_merge($options, $this->options);
$response = OpenAI::chat()->create($options);
return new OpenAIResponse($response, $this->schema['schema'] ?? null);
}
}

Usage

These will allow you to communicate with the openai-php/laravel package, in a more fluent way.

Config

Add the following code snippet to your config/openai.php file.

/*
|--------------------------------------------------------------------------
| Default Model
|--------------------------------------------------------------------------
|
| The default model to use for requests. You can specify a different model
| for each request if needed.
*/

'default_model' => env('OPENAI_DEFAULT_MODEL', 'gpt-4o'),

Examples

Below are a few examples of how to use these classes, but there have a look at them, to see the full functionality.

Simple example

$response = OpenAIService::chat()
    ->withModel('gpt-4o')
    ->user('Tell me something about Dan Brown')
    ->fetch();

// get the raw response from OpenAI
dd($response->raw());

Include an image

Include a base64 encoded image and get the first choice message as well as the usage stats.

$path   = 'path/to/image.jpg';
$type   = pathinfo($path, PATHINFO_EXTENSION);
$data   = file_get_contents($path);
$base64 = 'data:image/'.$type.';base64,'.base64_encode($data);

$response = OpenAIService::chat()
    ->withModel('gpt-4o')
    ->user("Please provide a list of each book in this photo with\n\nStructure:\n* title\n* author")
    ->withImage($base64)
    ->withJsonFormat([
        'books' => [
            [
                'title' => 'string or null',
                'author' => 'string or null',
            ],
        ],
    ])
    ->fetch();

dd($response->firstMessage(), $response->usage());

With JSON schema

The response object will make sure that if any of the fields defined in the schema are missing from the OpenAI response, it will add them with a null value.

$response = OpenAIService::chat()
    ->user('Please provide details about the author of the following book in JSON format.')
    ->user("Book: The Da Vinci Code")
    ->user("Author: Dan Brown")
    ->withTemperature(1.0)
    ->withJsonSchema([
        'name' => 'book_author',
        'strict' => true,
        'schema' => [
            'type' => 'object',
            'properties' => [
                'name' => [
                    'type' => ['string', 'null'],
                    'description' => 'The full name of the author, based on the book title and author name provided, this is null if the author does not exist',
                ],
                'short_bio' => [
                    'type' => ['string', 'null'],
                    'description' => 'Short biography of the author, this is null if the author does not exist',
                ],
                'dob' => [
                    'type' => ['string', 'null'],
                    'description' => 'Date of birth of the author. Format: yyyy-mm-dd, this is null if the author does not exist',
                ],
                'dod' => [
                    'type' => ['string', 'null'],
                    'description' => 'Date of death of the author. Format: yyyy-mm-dd, this is null if the author does not exist or has not died yet',
                ],
                'wikipedia_url' => [
                    'type' => ['string', 'null'],
                    'description' => 'URL to the author\'s Wikipedia page. This is null if the author does not have a Wikipedia page',
                ],
            ],
            'required' => ['name', 'short_bio', 'dob', 'dod', 'wikipedia_url'],
            'additionalProperties' => false
        ]
    ])
    ->fetch();

dd($response->firstMessageViaSchema());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment