. */ declare(strict_types=1); namespace App\Services\InfoProviderSystem; use App\Entity\Parts\ManufacturingStatus; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; /** * This class allows to convert the JSON data returned by an LLM into the DTOs used by the info provider system later. */ final class DTOJsonSchemaConverter { /** * Returns the JSON schema, that defines the expected structure of the JSON data returned by the LLM. * @return array */ public function getJSONSchema(): array { return [ 'name' => 'clock', 'strict' => true, 'schema' => [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string', 'description' => 'Product name'], 'description' => ['type' => 'string', 'description' => 'A short description of the product, maybe containing the most important things. Onnly One line.'], 'manufacturer' => ['type' => ['string', 'null'], 'description' => 'Manufacturer name'], 'mpn' => ['type' => ['string', 'null'], 'description' => 'Manufacturer Part Number'], 'category' => ['type' => ['string', 'null'], 'description' => 'Product category, e.g. "Passive components -> Resistors"'], 'manufacturing_status' => ['type' => ['string', 'null'], 'enum' => ['active', 'obsolete', 'nrfnd', 'discontinued', null], 'description' => 'Manufacturing status'], 'footprint' => ['type' => ['string', 'null'], 'description' => 'Package/footprint type, like "SOT-23", "DIP-8", "QFN-32" etc.'], 'mass' => ['type' => ['number', 'null'], 'description' => 'Mass of the product in grams'], 'gtin' => ['type' => ['string', 'null'], 'description' => 'Global Trade Item Number (GTIN) / EAN / UPC code for barcodes'], 'notes' => ['type' => ['string', 'null'], 'description' => 'Optional long description of the part with more details than description. Can be markdown formatted.'], 'parameters' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string'], 'symbol' => ['type' => ['string', 'null'], 'description' => 'An optional quantity symbol for the parameter in latex code, like R_1'], 'value_typical' => ['type' => ['number', 'null'], 'description' => 'The typical value of the parameter. For example, for a resistor this could be 100 for a 100 Ohm resistor. Also used if only one numeric value is given. If used an unit should be given'], 'value_min' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the minimum value. Null if no range is given.'], 'value_max' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the maximum value. Null if not a range.'], 'value_text' => ['type' => ['string', 'null'], 'description' => 'When a value is not numeric it can be put here as text. Only use if it does not fit in value_min, value_typical or value_max. E.g. "Yes", "Red", etc.'], 'group' => ['type' => ['string', 'null'], 'description' => 'An optional group name for the parameter, e.g. "Electrical parameters", "Mechanical parameters" etc.'], 'unit' => ['type' => ['string', 'null'], 'description' => 'The unit of the parameter values, e.g. kg, Ohm, V, etc.'], ], 'required' => ['name', 'value_typical', 'value_min', 'value_max', 'value_text'] ], ], 'datasheets' => [ 'description' => 'A list of datasheets, manuals, or other technical documents related to the product. Not images, but actual documents, preferably PDFs.', 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'url' => ['type' => 'string'], 'description' => ['type' => 'string'], ], 'required' => ['url'], ], ], 'images' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'url' => ['type' => 'string'], 'description' => ['type' => 'string'], ], 'required' => ['url'], ], ], 'vendor_infos' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'distributor_name' => ['type' => 'string', 'description' => 'Name of the distributor or vendor. Typically the shop name'], 'order_number' => ['type' => ['string', 'null'], 'description' => 'The order number or SKU used by the distributor. Optional, but can help to find the product on the distributor website.'], 'product_url' => ['type' => 'string'], 'prices_include_vat' => ['type' => ['boolean', 'null'], 'description' => 'Whether the prices include VAT or not. Null if unknown.'], 'prices' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'minimum_quantity' => ['type' => 'integer', 'description' => 'Minimum quantity for this price tier. 1 when no tiered pricing is available.'], 'price' => ['type' => 'number', 'description' => 'Price for the given minimum quantity.'], 'currency' => ['type' => 'string', 'description' => 'Currency ISO code, e.g. USD'], ], 'required' => ['minimum_quantity', 'price', 'currency'], ], ], ], 'required' => ['distributor_name', 'product_url'], ], ], 'manufacturer_product_url' => ['type' => ['string', 'null'], 'description' => 'Manufacturer product page URL'], ], 'required' => ['name', 'description'], ] ]; } public function jsonToDTO(array $data, string $providerKey, string $providerId, ?string $productUrl = null, string $distributorNameFallback = '???'): PartDetailDTO { // Map manufacturing status $manufacturingStatus = null; if (!empty($data['manufacturing_status'])) { $status = strtolower((string) $data['manufacturing_status']); $manufacturingStatus = match ($status) { 'active' => ManufacturingStatus::ACTIVE, 'obsolete', 'discontinued' => ManufacturingStatus::DISCONTINUED, 'nrfnd', 'not recommended for new designs' => ManufacturingStatus::NRFND, 'eol' => ManufacturingStatus::EOL, 'announced' => ManufacturingStatus::ANNOUNCED, default => null, }; } // Build parameters $parameters = null; if (!empty($data['parameters']) && is_array($data['parameters'])) { $parameters = []; foreach ($data['parameters'] as $p) { if (!empty($p['name'])) { $parameters[] = new ParameterDTO( name: $p['name'], value_text: $p['value_text'] ?? null, value_typ: isset($p['value_typical']) && is_numeric($p['value_typical']) ? (float) $p['value_typical'] : null, value_min: isset($p['value_min']) && is_numeric($p['value_min']) ? (float) $p['value_min'] : null, value_max: isset($p['value_max']) && is_numeric($p['value_max']) ? (float) $p['value_max'] : null, unit: $p['unit'] ?? null, symbol: $p['symbol'] ?? null, group: $p['group'] ?? null, ); } } } // Build datasheets $datasheets = null; if (!empty($data['datasheets']) && is_array($data['datasheets'])) { $datasheets = []; foreach ($data['datasheets'] as $d) { if (!empty($d['url'])) { $datasheets[] = new FileDTO( url: $d['url'], name: $d['description'] ?? 'Datasheet' ); } } } // Build images $images = null; if (!empty($data['images']) && is_array($data['images'])) { $images = []; foreach ($data['images'] as $i) { if (!empty($i['url'])) { $images[] = new FileDTO( url: $i['url'], name: $i['description'] ?? 'Image' ); } } } // Build vendor infos $vendorInfos = null; if (!empty($data['vendor_infos']) && is_array($data['vendor_infos'])) { $vendorInfos = []; foreach ($data['vendor_infos'] as $v) { $prices = []; if (!empty($v['prices']) && is_array($v['prices'])) { foreach ($v['prices'] as $p) { $prices[] = new PriceDTO( minimum_discount_amount: (int) ($p['minimum_quantity'] ?? 1), price: (string) ($p['price'] ?? 0), currency_iso_code: $p['currency'] ?? null, price_related_quantity: 1, ); } } $vendorInfos[] = new PurchaseInfoDTO( distributor_name: $v['distributor_name'] ?? $distributorNameFallback, order_number: $v['order_number'] ?? 'Unknown', prices: $prices, product_url: $v['product_url'] ?? $productUrl, prices_include_vat: $v['prices_include_vat'] ?? null, ); } } // Get preview image URL $previewImageUrl = null; if (!empty($data['images']) && is_array($data['images']) && !empty($data['images'][0]['url'])) { $previewImageUrl = $data['images'][0]['url']; } return new PartDetailDTO( provider_key: $providerKey, provider_id: $providerId, name: $data['name'] ?? 'Unknown', description: $data['description'] ?? '', category: $data['category'] ?? null, manufacturer: $data['manufacturer'] ?? null, mpn: $data['mpn'] ?? null, preview_image_url: $previewImageUrl, manufacturing_status: $manufacturingStatus, provider_url: $productUrl, footprint: $data['footprint'] ?? null, gtin: $data['gtin'] ?? null, notes: $data['notes'] ?? null, datasheets: $datasheets, images: $images, parameters: $parameters, vendor_infos: $vendorInfos, mass: isset($data['mass']) && is_numeric($data['mass']) ? (float) $data['mass'] : null, manufacturer_product_url: $data['manufacturer_product_url'] ?? null, ); } }