add catalog mode
This commit is contained in:
118
src/Catalog/EntityCatalogService.php
Normal file
118
src/Catalog/EntityCatalogService.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Catalog;
|
||||
|
||||
use App\Tag\TagVectorSearchClient;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* EntityCatalogService
|
||||
*
|
||||
* Deterministische Katalog-Listen auf Basis eines Entity-Terms:
|
||||
* - TagVectorSearch (Score-Gate + Ambiguity-Check)
|
||||
* - DB Query auf document_tag + document (ACTIVE)
|
||||
* - Rückgabe als EIN Textblock (string) oder null (Fallback auf normalen Retrieval)
|
||||
*/
|
||||
final class EntityCatalogService
|
||||
{
|
||||
private const MIN_SCORE = 0.55;
|
||||
private const AMBIGUITY_DELTA = 0.05;
|
||||
|
||||
public function __construct(
|
||||
private readonly TagVectorSearchClient $tagVectorClient,
|
||||
private readonly Connection $connection,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return string|null Textblock oder null (wenn kein sicherer Catalog möglich ist)
|
||||
*/
|
||||
public function listByTerm(string $entityTerm): ?string
|
||||
{
|
||||
$entityTerm = trim($entityTerm);
|
||||
if ($entityTerm === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1) Tag-Vektorsuche (Top 3 für Ambiguity-Prüfung)
|
||||
$hits = $this->tagVectorClient->search($entityTerm, 3);
|
||||
|
||||
if ($hits === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$best = $hits[0];
|
||||
|
||||
$bestScore = isset($best['score']) ? (float)$best['score'] : 0.0;
|
||||
if ($bestScore < self::MIN_SCORE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2) Ambiguity: wenn Top2 zu nah ist → konservativ abbrechen
|
||||
if (isset($hits[1])) {
|
||||
$secondScore = isset($hits[1]['score']) ? (float)$hits[1]['score'] : 0.0;
|
||||
if (abs($bestScore - $secondScore) < self::AMBIGUITY_DELTA) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$tagHex = (string)($best['tag_id'] ?? '');
|
||||
if ($tagHex === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3) DB Query: alle ACTIVE Dokumente zu diesem Tag
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'
|
||||
SELECT d.title
|
||||
FROM document d
|
||||
INNER JOIN document_tag dt ON dt.document_id = d.id
|
||||
WHERE dt.tag_id = :tagId
|
||||
AND d.status = :status
|
||||
ORDER BY d.title ASC
|
||||
',
|
||||
[
|
||||
'tagId' => Uuid::fromString($tagHex)->toBinary(),
|
||||
'status' => 'ACTIVE',
|
||||
]
|
||||
);
|
||||
|
||||
if ($rows === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$titles = [];
|
||||
foreach ($rows as $row) {
|
||||
$t = trim((string)($row['title'] ?? ''));
|
||||
if ($t !== '') {
|
||||
$titles[] = $t;
|
||||
}
|
||||
}
|
||||
|
||||
if ($titles === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildTextBlock($entityTerm, $titles);
|
||||
}
|
||||
|
||||
private function buildTextBlock(string $entityTerm, array $titles): string
|
||||
{
|
||||
$headline = match ($entityTerm) {
|
||||
'geräte' => 'Folgende Geräte sind verfügbar:',
|
||||
'indikatoren' => 'Folgende Indikatoren sind verfügbar:',
|
||||
'funktionen' => 'Folgende Funktionen sind verfügbar:',
|
||||
'zubehör' => 'Folgendes Zubehör ist verfügbar:',
|
||||
default => 'Folgende Einträge sind verfügbar:',
|
||||
};
|
||||
|
||||
$lines = [];
|
||||
foreach ($titles as $title) {
|
||||
$lines[] = '- ' . $title;
|
||||
}
|
||||
|
||||
return $headline . "\n\n" . implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user