diff --git a/composer.json b/composer.json index 868c1be..076cc58 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,8 @@ "ext-pdo": "*", "ext-pgsql": "*", "doctrine/doctrine-migrations-bundle": "^3.4", + "nyholm/psr7": "^1.8", + "openai-php/client": "^0.13.0", "symfony/asset-mapper": "^7.3", "symfony/console": "7.2.*", "symfony/dotenv": "7.2.*", @@ -24,6 +26,8 @@ "symfony/yaml": "7.2.*" }, "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-symfony": "^2.0", "symfony/debug-bundle": "7.2.*", "symfony/maker-bundle": "^1.0", "symfony/var-dumper": "7.2.*" @@ -47,6 +51,7 @@ } }, "scripts": { + "test": "phpstan analyse", "auto-scripts": { "cache:clear": "symfony-cmd", "assets:install %PUBLIC_DIR%": "symfony-cmd", @@ -59,4 +64,4 @@ "@auto-scripts" ] } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index dccb2ee..7e22d66 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af2c42f4eb216435ec2aed6bb8b1dd11", + "content-hash": "aff00a8d8bd55186f046d02ac6fd7c4d", "packages": [ { "name": "composer/semver", @@ -792,6 +792,310 @@ }, "time": "2025-01-24T11:45:48+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "openai-php/client", + "version": "v0.13.0", + "source": { + "type": "git", + "url": "https://github.com/openai-php/client.git", + "reference": "399229860cea244843753bf1d9c28aee0e74c3a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openai-php/client/zipball/399229860cea244843753bf1d9c28aee0e74c3a6", + "reference": "399229860cea244843753bf1d9c28aee0e74c3a6", + "shasum": "" + }, + "require": { + "php": "^8.2.0", + "php-http/discovery": "^1.20.0", + "php-http/multipart-stream-builder": "^1.4.2", + "psr/http-client": "^1.0.3", + "psr/http-client-implementation": "^1.0.1", + "psr/http-factory-implementation": "*", + "psr/http-message": "^1.1.0|^2.0.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.9.3", + "guzzlehttp/psr7": "^2.7.1", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/collision": "^8.8.0", + "pestphp/pest": "^3.8.2|^4.0.0", + "pestphp/pest-plugin-arch": "^3.1.1|^4.0.0", + "pestphp/pest-plugin-type-coverage": "^3.5.1|^4.0.0", + "phpstan/phpstan": "^1.12.25", + "symfony/var-dumper": "^7.2.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/OpenAI.php" + ], + "psr-4": { + "OpenAI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri" + } + ], + "description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API", + "keywords": [ + "GPT-3", + "api", + "client", + "codex", + "dall-e", + "language", + "natural", + "openai", + "php", + "processing", + "sdk" + ], + "support": { + "issues": "https://github.com/openai-php/client/issues", + "source": "https://github.com/openai-php/client/tree/v0.13.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-05-14T21:43:59+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" + }, + "time": "2024-09-04T13:22:54+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -944,6 +1248,166 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -4410,6 +4874,135 @@ }, "time": "2024-12-30T11:07:19+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:55:28+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/5005288e07583546ea00b52de4a9ac412eb869d7", + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.1.2", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.6" + }, + "time": "2025-05-14T07:00:05+00:00" + }, { "name": "symfony/debug-bundle", "version": "v7.2.0", diff --git a/config/packages/http_discovery.yaml b/config/packages/http_discovery.yaml new file mode 100644 index 0000000..2a789e7 --- /dev/null +++ b/config/packages/http_discovery.yaml @@ -0,0 +1,10 @@ +services: + Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory' + + http_discovery.psr17_factory: + class: Http\Discovery\Psr17Factory diff --git a/config/secrets/dev/dev.OPENAI_API_KEY.66949e.php b/config/secrets/dev/dev.OPENAI_API_KEY.66949e.php new file mode 100644 index 0000000..101a19d --- /dev/null +++ b/config/secrets/dev/dev.OPENAI_API_KEY.66949e.php @@ -0,0 +1,3 @@ + null, +]; diff --git a/config/services.yaml b/config/services.yaml index 515d34b..12e943d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,4 +10,8 @@ services: exclude: - '../src/DependencyInjection/' - '../src/Entity/' - - '../src/Kernel.php' \ No newline at end of file + - '../src/Kernel.php' + + App\Domain\AI\AIClient: + arguments: + $apiKey: '%env(OPENAI_API_KEY)%' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d26ad55..1c03c76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - proxy database: - image: postgres:17-alpine + image: pgvector/pgvector:pg17 hostname: database.evwiki.test environment: - POSTGRES_USER=postgres diff --git a/migrations/Version20250530193246.php b/migrations/Version20250530193246.php new file mode 100644 index 0000000..db1368a --- /dev/null +++ b/migrations/Version20250530193246.php @@ -0,0 +1,37 @@ +addSql(<<addSql('DROP TABLE embeddings'); + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1e4cec1 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,16 @@ +includes: + - vendor/phpstan/phpstan-symfony/extension.neon + +parameters: + level: 10 + paths: + - src + - bin + excludePaths: + - src/Kernel.php (?) + - var/* + - vendor/* + + # Bootstrap file for better analysis + bootstrapFiles: + - vendor/autoload.php \ No newline at end of file diff --git a/src/Application/Commands/AIClientCommand.php b/src/Application/Commands/AIClientCommand.php new file mode 100644 index 0000000..4ce73f2 --- /dev/null +++ b/src/Application/Commands/AIClientCommand.php @@ -0,0 +1,65 @@ +addArgument('text', InputArgument::REQUIRED, 'The text to embed'); + $this->addOption('embed', null, InputOption::VALUE_NONE, 'Embed the text'); + $this->addOption('search', null, InputOption::VALUE_NONE, 'Search for similar texts'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $textArg = $input->getArgument('text'); + $text = is_string($textArg) ? $textArg : ''; + + if ($input->getOption('embed')) { + $persistedEmbedding = $this->embedText($text); + $output->writeln($persistedEmbedding->phraseHash); + } else if ($input->getOption('search')) { + $results = $this->embeddingRepository->searchByLargeEmbeddingVector(new LargeEmbeddingVector(new Vector($this->aiClient->embedText($text)))); + foreach ($results as $result) { + $output->writeln($result->embedding->phrase); + } + } else { + $output->writeln($this->aiClient->generateText($text)); + } + + return Command::SUCCESS; + } + + private function embedText(string $text): PersistedEmbedding + { + $embedding = $this->aiClient->embedText($text); + $vector = new Vector($embedding); + + return $this->embeddingRepository->create(new Embedding($text, new LargeEmbeddingVector($vector))); + } +} \ No newline at end of file diff --git a/src/Application/Commands/LoadFixtures.php b/src/Application/Commands/LoadFixtures.php index bf533bc..b3658de 100644 --- a/src/Application/Commands/LoadFixtures.php +++ b/src/Application/Commands/LoadFixtures.php @@ -37,7 +37,9 @@ use Symfony\Component\Console\Style\SymfonyStyle; )] class LoadFixtures extends Command { + /** @var array */ private array $brandIds = []; + /** @var array */ private array $carModelIds = []; public function __construct( @@ -73,11 +75,14 @@ class LoadFixtures extends Command $io->progressStart(count($carModels)); foreach ($carModels as $carModelData) { + $model = $carModelData['model']; + $brandName = $carModelData['brand']; + $persistedCarModel = $this->carModelRepository->create( - $carModelData['model'], - $this->brandIds[$carModelData['brand']] + $model, + $this->brandIds[$brandName] ); - $this->carModelIds[$carModelData['model']->name] = $persistedCarModel->id; + $this->carModelIds[$model->name] = $persistedCarModel->id; $io->progressAdvance(); } @@ -90,9 +95,12 @@ class LoadFixtures extends Command $io->progressStart(count($carRevisions)); foreach ($carRevisions as $carRevisionData) { + $revision = $carRevisionData['revision']; + $modelName = $carRevisionData['model']; + $this->carRevisionRepository->create( - $carRevisionData['revision'], - $this->carModelIds[$carRevisionData['model']] + $revision, + $this->carModelIds[$modelName] ); $io->progressAdvance(); } diff --git a/src/Domain/AI/AIClient.php b/src/Domain/AI/AIClient.php new file mode 100644 index 0000000..5c4c05a --- /dev/null +++ b/src/Domain/AI/AIClient.php @@ -0,0 +1,41 @@ +client = OpenAI::client($this->apiKey); + } + + public function generateText(string $prompt): string + { + $response = $this->client->chat()->create([ + 'model' => 'gpt-4o-mini', + 'messages' => [ + ['role' => 'user', 'content' => $prompt], + ], + ]); + + return $response->choices[0]->message->content ?? ''; + } + + /** + * @return float[] + */ + public function embedText(string $text): array + { + $response = $this->client->embeddings()->create([ + 'model' => 'text-embedding-3-large', + 'input' => $text, + ]); + + return $response->embeddings[0]->embedding; + } +} \ No newline at end of file diff --git a/src/Domain/Model/AI/Embedding.php b/src/Domain/Model/AI/Embedding.php new file mode 100644 index 0000000..c2fc005 --- /dev/null +++ b/src/Domain/Model/AI/Embedding.php @@ -0,0 +1,16 @@ +dimension() !== self::DIMENSION) { + throw new \InvalidArgumentException('Vector must be ' . self::DIMENSION . ' dimensions'); + } + } +} \ No newline at end of file diff --git a/src/Domain/Model/AI/SmallEmbeddingVector.php b/src/Domain/Model/AI/SmallEmbeddingVector.php new file mode 100644 index 0000000..9d9e23e --- /dev/null +++ b/src/Domain/Model/AI/SmallEmbeddingVector.php @@ -0,0 +1,18 @@ +dimension() !== self::DIMENSION) { + throw new \InvalidArgumentException('Vector must be ' . self::DIMENSION . ' dimensions'); + } + } +} \ No newline at end of file diff --git a/src/Domain/Model/BrandCollection.php b/src/Domain/Model/BrandCollection.php index 610c78b..59444d3 100644 --- a/src/Domain/Model/BrandCollection.php +++ b/src/Domain/Model/BrandCollection.php @@ -4,11 +4,17 @@ namespace App\Domain\Model; class BrandCollection { + /** + * @param Brand[] $brands + */ public function __construct( private readonly array $brands, ) { } + /** + * @return Brand[] + */ public function array(): array { return $this->brands; diff --git a/src/Domain/Model/CarModelCollection.php b/src/Domain/Model/CarModelCollection.php index 77f93f4..a1d072f 100644 --- a/src/Domain/Model/CarModelCollection.php +++ b/src/Domain/Model/CarModelCollection.php @@ -4,11 +4,17 @@ namespace App\Domain\Model; class CarModelCollection { + /** + * @param CarModel[] $carModels + */ public function __construct( private readonly array $carModels, ) { } + /** + * @return CarModel[] + */ public function array(): array { return $this->carModels; diff --git a/src/Domain/Model/CarRevisionCollection.php b/src/Domain/Model/CarRevisionCollection.php index d5ce8ff..46c64eb 100644 --- a/src/Domain/Model/CarRevisionCollection.php +++ b/src/Domain/Model/CarRevisionCollection.php @@ -4,11 +4,17 @@ namespace App\Domain\Model; class CarRevisionCollection { + /** + * @param CarRevision[] $carRevisions + */ public function __construct( private readonly array $carRevisions, ) { } + /** + * @return CarRevision[] + */ public function array(): array { return $this->carRevisions; diff --git a/src/Domain/Model/Persistence/PersistedEmbedding.php b/src/Domain/Model/Persistence/PersistedEmbedding.php new file mode 100644 index 0000000..f4bb157 --- /dev/null +++ b/src/Domain/Model/Persistence/PersistedEmbedding.php @@ -0,0 +1,14 @@ +values); + } +} \ No newline at end of file diff --git a/src/Domain/Repository/EmbeddingRepository.php b/src/Domain/Repository/EmbeddingRepository.php new file mode 100644 index 0000000..854b6dc --- /dev/null +++ b/src/Domain/Repository/EmbeddingRepository.php @@ -0,0 +1,30 @@ +image, [ + new CarTile($skodaElroq85->image ?? new Image(), array_filter([ new BrandTile('Skoda'), - new PriceTile($skodaElroq85->catalogPrice), + $skodaElroq85->catalogPrice ? new PriceTile($skodaElroq85->catalogPrice) : null, new AvailabilityTile('Verfügbar', new Date(1, 1, 2020)), new RangeTile($wltpRange->range), - new ConsumptionTile($drivingCharacteristics->consumption), - new AccelerationTile($drivingCharacteristics->acceleration), - ]), + $drivingCharacteristics->consumption ? new ConsumptionTile($drivingCharacteristics->consumption) : null, + $drivingCharacteristics->acceleration ? new AccelerationTile($drivingCharacteristics->acceleration) : null, + ])), - new SubSectionTile('Performance', [ - new PowerTile($drivingCharacteristics->power), - new TopSpeedTile($drivingCharacteristics->topSpeed), + new SubSectionTile('Performance', array_filter([ + $drivingCharacteristics->power ? new PowerTile($drivingCharacteristics->power) : null, + $drivingCharacteristics->topSpeed ? new TopSpeedTile($drivingCharacteristics->topSpeed) : null, new DrivetrainTile(new Drivetrain('rear')), - ], 'Individual performance metrics'), + ])), new SubSectionTile('Reichweite', [ new RangeTile($wltpRange->range), new RealRangeTile($realRangeTests), - ], 'Range data from different sources'), + ]), - new SubSectionTile('Batterie', [ - new BatteryTile($skodaElroq85->battery), - new BatteryDetailsTile($skodaElroq85->battery), - ], 'Battery capacity and technology'), + new SubSectionTile('Batterie', array_filter([ + $skodaElroq85->battery ? new BatteryTile($skodaElroq85->battery) : null, + $skodaElroq85->battery ? new BatteryDetailsTile($skodaElroq85->battery) : null, + ])), - new SubSectionTile('Laden', [ + new SubSectionTile('Laden', array_filter([ new ChargingTile($chargingSpeed), - new ChargeTimeTile($chargingProperties->chargeTimeProperties), - new ChargingConnectivityTile($chargingProperties->chargingConnectivity), - ], 'Charging capabilities and compatibility'), + $chargingProperties->chargeTimeProperties ? new ChargeTimeTile($chargingProperties->chargeTimeProperties) : null, + $chargingProperties->chargingConnectivity ? new ChargingConnectivityTile($chargingProperties->chargingConnectivity) : null, + ])), ]), ]); } diff --git a/src/Domain/Search/TileCollection.php b/src/Domain/Search/TileCollection.php index 861017a..1c551c9 100644 --- a/src/Domain/Search/TileCollection.php +++ b/src/Domain/Search/TileCollection.php @@ -6,10 +6,16 @@ use App\Domain\Search\Tiles\SectionTile; class TileCollection { + /** + * @param SectionTile[] $tiles + */ public function __construct( private readonly array $tiles, ) {} + /** + * @return SectionTile[] + */ public function array(): array { return $this->tiles; diff --git a/src/Domain/Search/Tiles/CarTile.php b/src/Domain/Search/Tiles/CarTile.php index dacde97..b07342b 100644 --- a/src/Domain/Search/Tiles/CarTile.php +++ b/src/Domain/Search/Tiles/CarTile.php @@ -6,6 +6,9 @@ use App\Domain\Model\Image; final readonly class CarTile { + /** + * @param object[] $tiles + */ public function __construct( public Image $image, public array $tiles diff --git a/src/Domain/Search/Tiles/SectionTile.php b/src/Domain/Search/Tiles/SectionTile.php index 6ce773c..275ce0f 100644 --- a/src/Domain/Search/Tiles/SectionTile.php +++ b/src/Domain/Search/Tiles/SectionTile.php @@ -4,6 +4,9 @@ namespace App\Domain\Search\Tiles; class SectionTile { + /** + * @param object[] $tiles + */ public function __construct( public readonly string $title, public readonly array $tiles, diff --git a/src/Domain/Search/Tiles/SubSectionTile.php b/src/Domain/Search/Tiles/SubSectionTile.php index 70c9ab5..70d7129 100644 --- a/src/Domain/Search/Tiles/SubSectionTile.php +++ b/src/Domain/Search/Tiles/SubSectionTile.php @@ -4,6 +4,9 @@ namespace App\Domain\Search\Tiles; class SubSectionTile { + /** + * @param object[] $tiles + */ public function __construct( public readonly string $title, public readonly array $tiles, diff --git a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php index ea9db26..54748be 100644 --- a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/ModelMapper.php @@ -6,17 +6,18 @@ use App\Domain\Model\Brand; class ModelMapper { + /** + * @param array $data + */ public function map(array $data): Brand { return new Brand( - id: (string) ($data['id'] ?? null), - name: $data['name'] ?? '', - logo: $data['logo'] ?? '', - description: $data['description'] ?? '', - foundedYear: (int) ($data['founded_year'] ?? 0), - headquarters: $data['headquarters'] ?? '', - website: $data['website'] ?? '', - carModels: json_decode($data['car_models'] ?? '[]', true), + name: is_string($data['name'] ?? null) ? $data['name'] : '', + logo: isset($data['logo']) && is_string($data['logo']) ? $data['logo'] : null, + description: isset($data['description']) && is_string($data['description']) ? $data['description'] : null, + foundedYear: isset($data['founded_year']) && is_numeric($data['founded_year']) ? (int) $data['founded_year'] : null, + headquarters: isset($data['headquarters']) && is_string($data['headquarters']) ? $data['headquarters'] : null, + website: isset($data['website']) && is_string($data['website']) ? $data['website'] : null, ); } } \ No newline at end of file diff --git a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/PostgreSQLBrandRepository.php b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/PostgreSQLBrandRepository.php index 85c5536..2da2b6b 100644 --- a/src/Infrastructure/PostgreSQL/Repository/BrandRepository/PostgreSQLBrandRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/BrandRepository/PostgreSQLBrandRepository.php @@ -23,7 +23,7 @@ final class PostgreSQLBrandRepository implements BrandRepository $brands = []; $mapper = new ModelMapper(); - foreach ($result as $brand) { + foreach ($result->fetchAllAssociative() as $brand) { $brands[] = $mapper->map($brand); } diff --git a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php index aeeb05b..130a233 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/ModelMapper.php @@ -7,17 +7,21 @@ use App\Domain\Model\Brand; class ModelMapper { + /** + * @param array $data + */ public function map(array $data): CarModel { - $content = json_decode($data['content'] ?? '{}', true); + $contentString = is_string($data['content'] ?? null) ? $data['content'] : '{}'; + $content = json_decode($contentString, true); $brand = null; - if (!empty($content['brand'])) { - $brand = new Brand($content['brand']); + if (is_array($content) && !empty($content['brand']) && is_string($content['brand'])) { + $brand = new Brand(name: $content['brand']); } return new CarModel( - name: $data['name'] ?? '', + name: is_string($data['name'] ?? null) ? $data['name'] : '', brand: $brand, ); } diff --git a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/PostgreSQLCarModelRepository.php b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/PostgreSQLCarModelRepository.php index 0b4e694..fc7426f 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/PostgreSQLCarModelRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarModelRepository/PostgreSQLCarModelRepository.php @@ -23,7 +23,7 @@ final class PostgreSQLCarModelRepository implements CarModelRepository $carModels = []; $mapper = new ModelMapper(); - foreach ($result as $carModel) { + foreach ($result->fetchAllAssociative() as $carModel) { $carModels[] = $mapper->map($carModel); } @@ -43,7 +43,7 @@ final class PostgreSQLCarModelRepository implements CarModelRepository $mapper = new ModelMapper(); $carModel = $mapper->map($data); - return new PersistedCarModel($data['id'], $carModel); + return new PersistedCarModel(is_string($data['id'] ?? null) ? $data['id'] : '', $carModel); } public function findByBrandId(string $brandId): CarModelCollection @@ -54,7 +54,7 @@ final class PostgreSQLCarModelRepository implements CarModelRepository $carModels = []; $mapper = new ModelMapper(); - foreach ($result as $carModel) { + foreach ($result->fetchAllAssociative() as $carModel) { $carModels[] = $mapper->map($carModel); } @@ -76,7 +76,7 @@ final class PostgreSQLCarModelRepository implements CarModelRepository SQL; $content = json_encode([ - 'brand' => $carModel->brand?->name ?? null, + 'brand' => $carModel->brand->name ?? null, ]); $this->connection->executeStatement($sql, [ @@ -101,7 +101,7 @@ final class PostgreSQLCarModelRepository implements CarModelRepository SQL; $content = json_encode([ - 'brand' => $carModel->brand?->name ?? null, + 'brand' => $carModel->brand->name ?? null, ]); $this->connection->executeStatement($sql, [ diff --git a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php index 2433521..be92a90 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/ModelMapper.php @@ -23,22 +23,31 @@ use App\Domain\Model\Value\Range; class ModelMapper { + /** + * @param array $data + */ public function map(array $data): CarRevision { - $content = json_decode($data['content'] ?? '{}', true); + $contentString = is_string($data['content'] ?? null) ? $data['content'] : '{}'; + $content = json_decode($contentString, true); + + if (!is_array($content)) { + $content = []; + } $productionBegin = null; - if (!empty($content['production_begin'])) { - $productionBegin = new Date(1, 1, $content['production_begin']); + if (isset($content['production_begin']) && is_numeric($content['production_begin'])) { + $productionBegin = new Date(1, 1, (int) $content['production_begin']); } $productionEnd = null; - if (!empty($content['production_end'])) { - $productionEnd = new Date(1, 1, $content['production_end']); + if (isset($content['production_end']) && is_numeric($content['production_end'])) { + $productionEnd = new Date(1, 1, (int) $content['production_end']); } $catalogPrice = null; - if (!empty($content['catalog_price']) && !empty($content['catalog_price_currency'])) { + if (isset($content['catalog_price']) && isset($content['catalog_price_currency']) && + is_numeric($content['catalog_price']) && is_string($content['catalog_price_currency'])) { $currency = match($content['catalog_price_currency']) { 'EUR' => Currency::euro(), 'USD' => Currency::usd(), @@ -46,53 +55,56 @@ class ModelMapper }; $catalogPrice = new Price( - $content['catalog_price'], + (int) $content['catalog_price'], $currency ); } $image = null; - if (!empty($content['image_url'])) { + if (isset($content['image_url']) && is_string($content['image_url'])) { $image = new Image($content['image_url']); } $drivingCharacteristics = null; - if (!empty($content['driving_characteristics'])) { + if (isset($content['driving_characteristics']) && is_array($content['driving_characteristics'])) { $dc = $content['driving_characteristics']; $drivingCharacteristics = new DrivingCharacteristics( - power: !empty($dc['power_kw']) ? new Power($dc['power_kw']) : null, - acceleration: !empty($dc['acceleration_0_100']) ? new Acceleration($dc['acceleration_0_100']) : null, - topSpeed: !empty($dc['top_speed_kmh']) ? new Speed($dc['top_speed_kmh']) : null, - consumption: !empty($dc['consumption_kwh_100km']) ? new Consumption(new Energy($dc['consumption_kwh_100km'])) : null, + power: (isset($dc['power_kw']) && is_numeric($dc['power_kw'])) ? new Power((float) $dc['power_kw']) : null, + acceleration: (isset($dc['acceleration_0_100']) && is_numeric($dc['acceleration_0_100'])) ? new Acceleration((float) $dc['acceleration_0_100']) : null, + topSpeed: (isset($dc['top_speed_kmh']) && is_numeric($dc['top_speed_kmh'])) ? new Speed((int) $dc['top_speed_kmh']) : null, + consumption: (isset($dc['consumption_kwh_100km']) && is_numeric($dc['consumption_kwh_100km'])) ? new Consumption(new Energy((float) $dc['consumption_kwh_100km'])) : null, ); } $battery = null; - if (!empty($content['battery'])) { + if (isset($content['battery']) && is_array($content['battery'])) { $b = $content['battery']; - if (!empty($b['usable_capacity_kwh']) && !empty($b['total_capacity_kwh'])) { + if (isset($b['usable_capacity_kwh']) && isset($b['total_capacity_kwh']) && + is_numeric($b['usable_capacity_kwh']) && is_numeric($b['total_capacity_kwh'])) { $battery = new BatteryProperties( - usableCapacity: new Energy($b['usable_capacity_kwh']), - totalCapacity: new Energy($b['total_capacity_kwh']), - cellChemistry: !empty($b['cell_chemistry']) ? CellChemistry::from($b['cell_chemistry']) : CellChemistry::LithiumIronPhosphate, - model: $b['model'] ?? '', - manufacturer: $b['manufacturer'] ?? '', + usableCapacity: new Energy((float) $b['usable_capacity_kwh']), + totalCapacity: new Energy((float) $b['total_capacity_kwh']), + cellChemistry: (isset($b['cell_chemistry']) && (is_string($b['cell_chemistry']) || is_int($b['cell_chemistry']))) ? CellChemistry::from($b['cell_chemistry']) : CellChemistry::LithiumIronPhosphate, + model: is_string($b['model'] ?? null) ? $b['model'] : '', + manufacturer: is_string($b['manufacturer'] ?? null) ? $b['manufacturer'] : '', ); } } $chargingProperties = null; - if (!empty($content['charging']['top_charging_speed_kw'])) { + if (isset($content['charging']) && is_array($content['charging']) && + isset($content['charging']['top_charging_speed_kw']) && + is_numeric($content['charging']['top_charging_speed_kw'])) { $chargingProperties = new ChargingProperties( - topChargingSpeed: new Power($content['charging']['top_charging_speed_kw']) + topChargingSpeed: new Power((float) $content['charging']['top_charging_speed_kw']) ); } $rangeProperties = null; - if (!empty($content['range'])) { + if (isset($content['range']) && is_array($content['range'])) { $r = $content['range']; - $wltp = !empty($r['wltp_km']) ? new WltpRange(new Range($r['wltp_km'])) : null; - $nefz = !empty($r['nefz_km']) ? new NefzRange(new Range($r['nefz_km'])) : null; + $wltp = (isset($r['wltp_km']) && is_numeric($r['wltp_km'])) ? new WltpRange(new Range((int) $r['wltp_km'])) : null; + $nefz = (isset($r['nefz_km']) && is_numeric($r['nefz_km'])) ? new NefzRange(new Range((int) $r['nefz_km'])) : null; if ($wltp || $nefz) { $rangeProperties = new RangeProperties( @@ -103,7 +115,7 @@ class ModelMapper } return new CarRevision( - name: $data['name'] ?? '', + name: is_string($data['name'] ?? null) ? $data['name'] : '', productionBegin: $productionBegin, productionEnd: $productionEnd, drivingCharacteristics: $drivingCharacteristics, diff --git a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/PostgreSQLCarRevisionRepository.php b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/PostgreSQLCarRevisionRepository.php index 7c3f588..0096bb5 100644 --- a/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/PostgreSQLCarRevisionRepository.php +++ b/src/Infrastructure/PostgreSQL/Repository/CarRevisionRepository/PostgreSQLCarRevisionRepository.php @@ -23,7 +23,7 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository $carRevisions = []; $mapper = new ModelMapper(); - foreach ($result as $carRevision) { + foreach ($result->fetchAllAssociative() as $carRevision) { $carRevisions[] = $mapper->map($carRevision); } @@ -43,7 +43,7 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository $mapper = new ModelMapper(); $carRevision = $mapper->map($data); - return new PersistedCarRevision($data['id'], $carRevision); + return new PersistedCarRevision(is_string($data['id'] ?? null) ? $data['id'] : '', $carRevision); } public function findByCarModelId(string $carModelId): CarRevisionCollection @@ -54,7 +54,7 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository $carRevisions = []; $mapper = new ModelMapper(); - foreach ($result as $carRevision) { + foreach ($result->fetchAllAssociative() as $carRevision) { $carRevisions[] = $mapper->map($carRevision); } @@ -76,26 +76,26 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository SQL; $content = json_encode([ - 'production_begin' => $carRevision->productionBegin?->year ?? null, - 'production_end' => $carRevision->productionEnd?->year ?? null, - 'catalog_price' => $carRevision->catalogPrice?->price ?? null, + 'production_begin' => $carRevision->productionBegin->year ?? null, + 'production_end' => $carRevision->productionEnd->year ?? null, + 'catalog_price' => $carRevision->catalogPrice->price ?? null, 'catalog_price_currency' => $carRevision->catalogPrice?->currency->currency ?? null, - 'image_url' => $carRevision->image?->url ?? null, + 'image_url' => $carRevision->image->externalPublicUrl ?? null, 'driving_characteristics' => [ - 'power_kw' => $carRevision->drivingCharacteristics?->power?->kilowatts ?? null, - 'acceleration_0_100' => $carRevision->drivingCharacteristics?->acceleration?->secondsFrom0To100 ?? null, - 'top_speed_kmh' => $carRevision->drivingCharacteristics?->topSpeed?->kmh ?? null, + 'power_kw' => $carRevision->drivingCharacteristics->power->kilowatts ?? null, + 'acceleration_0_100' => $carRevision->drivingCharacteristics->acceleration->secondsFrom0To100 ?? null, + 'top_speed_kmh' => $carRevision->drivingCharacteristics->topSpeed->kmh ?? null, 'consumption_kwh_100km' => $carRevision->drivingCharacteristics?->consumption?->energyPer100Km->kwh() ?? null, ], 'battery' => [ 'usable_capacity_kwh' => $carRevision->battery?->usableCapacity->kwh() ?? null, 'total_capacity_kwh' => $carRevision->battery?->totalCapacity->kwh() ?? null, 'cell_chemistry' => $carRevision->battery?->cellChemistry->value ?? null, - 'model' => $carRevision->battery?->model ?? null, - 'manufacturer' => $carRevision->battery?->manufacturer ?? null, + 'model' => $carRevision->battery->model ?? null, + 'manufacturer' => $carRevision->battery->manufacturer ?? null, ], 'charging' => [ - 'top_charging_speed_kw' => $carRevision->chargingProperties?->topChargingSpeed?->kilowatts ?? null, + 'top_charging_speed_kw' => $carRevision->chargingProperties->topChargingSpeed->kilowatts ?? null, ], 'range' => [ 'wltp_km' => $carRevision->rangeProperties?->wltp?->range->kilometers ?? null, @@ -125,26 +125,26 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository SQL; $content = json_encode([ - 'production_begin' => $carRevision->productionBegin?->year ?? null, - 'production_end' => $carRevision->productionEnd?->year ?? null, - 'catalog_price' => $carRevision->catalogPrice?->price ?? null, + 'production_begin' => $carRevision->productionBegin->year ?? null, + 'production_end' => $carRevision->productionEnd->year ?? null, + 'catalog_price' => $carRevision->catalogPrice->price ?? null, 'catalog_price_currency' => $carRevision->catalogPrice?->currency->currency ?? null, - 'image_url' => $carRevision->image?->url ?? null, + 'image_url' => $carRevision->image->externalPublicUrl ?? null, 'driving_characteristics' => [ - 'power_kw' => $carRevision->drivingCharacteristics?->power?->kilowatts ?? null, - 'acceleration_0_100' => $carRevision->drivingCharacteristics?->acceleration?->secondsFrom0To100 ?? null, - 'top_speed_kmh' => $carRevision->drivingCharacteristics?->topSpeed?->kmh ?? null, + 'power_kw' => $carRevision->drivingCharacteristics->power->kilowatts ?? null, + 'acceleration_0_100' => $carRevision->drivingCharacteristics->acceleration->secondsFrom0To100 ?? null, + 'top_speed_kmh' => $carRevision->drivingCharacteristics->topSpeed->kmh ?? null, 'consumption_kwh_100km' => $carRevision->drivingCharacteristics?->consumption?->energyPer100Km->kwh() ?? null, ], 'battery' => [ 'usable_capacity_kwh' => $carRevision->battery?->usableCapacity->kwh() ?? null, 'total_capacity_kwh' => $carRevision->battery?->totalCapacity->kwh() ?? null, 'cell_chemistry' => $carRevision->battery?->cellChemistry->value ?? null, - 'model' => $carRevision->battery?->model ?? null, - 'manufacturer' => $carRevision->battery?->manufacturer ?? null, + 'model' => $carRevision->battery->model ?? null, + 'manufacturer' => $carRevision->battery->manufacturer ?? null, ], 'charging' => [ - 'top_charging_speed_kw' => $carRevision->chargingProperties?->topChargingSpeed?->kilowatts ?? null, + 'top_charging_speed_kw' => $carRevision->chargingProperties->topChargingSpeed->kilowatts ?? null, ], 'range' => [ 'wltp_km' => $carRevision->rangeProperties?->wltp?->range->kilometers ?? null, diff --git a/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/PostgreSQLEmbeddingRepository.php b/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/PostgreSQLEmbeddingRepository.php new file mode 100644 index 0000000..23630dd --- /dev/null +++ b/src/Infrastructure/PostgreSQL/Repository/EmbeddingRepository/PostgreSQLEmbeddingRepository.php @@ -0,0 +1,90 @@ +phrase); + $this->connection->executeStatement(<< $hash, + 'phrase' => $embedding->phrase, + 'large_embedding_vector' => $embedding->largeEmbeddingVector !== null ? '[' . implode(',', $embedding->largeEmbeddingVector->vector->values) . ']' : null, + 'small_embedding_vector' => $embedding->smallEmbeddingVector !== null ? '[' . implode(',', $embedding->smallEmbeddingVector->vector->values) . ']' : null, + ]); + + return new PersistedEmbedding( + $hash, + $embedding, + ); + } + + public function delete(PersistedEmbedding $persistedEmbedding): void + { + $this->connection->delete('embeddings', ['phrase_hash' => $persistedEmbedding->phraseHash]); + } + + public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): array + { + $result = $this->connection->executeQuery(<< :vector + LIMIT :limit + SQL, [ + 'vector' => '[' . implode(',', $embeddingVector->vector->values) . ']', + 'limit' => $limit, + ]); + + $embeddings = []; + foreach ($result->fetchAllAssociative() as $row) { + $embeddings[] = new PersistedEmbedding( + is_string($row['phrase_hash'] ?? null) ? $row['phrase_hash'] : '', + new Embedding(is_string($row['phrase'] ?? null) ? $row['phrase'] : '', $embeddingVector) + ); + } + + return $embeddings; + } + + public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 10): array + { + $result = $this->connection->executeQuery(<< :vector + LIMIT :limit + SQL, [ + 'vector' => '[' . implode(',', $smallEmbeddingVector->vector->values) . ']', + 'limit' => $limit, + ]); + + $embeddings = []; + foreach ($result->fetchAllAssociative() as $row) { + $embeddings[] = new PersistedEmbedding( + is_string($row['phrase_hash'] ?? null) ? $row['phrase_hash'] : '', + new Embedding(is_string($row['phrase'] ?? null) ? $row['phrase'] : '', null, $smallEmbeddingVector) + ); + } + + return $embeddings; + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 503d463..44f95c8 100644 --- a/symfony.lock +++ b/symfony.lock @@ -35,6 +35,27 @@ "migrations/.gitignore" ] }, + "php-http/discovery": { + "version": "1.20", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.18", + "ref": "f45b5dd173a27873ab19f5e3180b2f661c21de02" + }, + "files": [ + "config/packages/http_discovery.yaml" + ] + }, + "phpstan/phpstan": { + "version": "2.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + } + }, "symfony/asset-mapper": { "version": "7.3", "recipe": {