tagVectorClient->search($entityTerm, self::SEARCH_LIMIT); if ($hits === []) { return null; } $best = $hits[0]; $bestScore = (float) ($best['score'] ?? 0.0); if ($bestScore < CatalogIntentConfig::MIN_SCORE) { return null; } if (($best['tag_type'] ?? null) !== TagTypes::CATALOG_ENTITY) { return null; } if (isset($hits[1])) { $secondScore = (float) ($hits[1]['score'] ?? 0.0); if (abs($bestScore - $secondScore) < CatalogIntentConfig::AMBIGUITY_DELTA) { return null; } } $tagId = trim((string) ($best['tag_id'] ?? '')); if ($tagId === '') { return null; } try { $tagBinaryId = Uuid::fromString($tagId)->toBinary(); } catch (\Throwable) { return null; } $tagLabel = trim((string) ($best['label'] ?? '')); $rows = $this->connection->fetchAllAssociative( ' SELECT DISTINCT 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' => $tagBinaryId, 'status' => Document::STATUS_ACTIVE, ] ); if ($rows === []) { return null; } $titles = []; foreach ($rows as $row) { $title = trim((string) ($row['title'] ?? '')); if ($title === '') { continue; } $titles[$title] = $title; } if ($titles === []) { return null; } return $this->buildTextBlock( $tagLabel !== '' ? $tagLabel : null, array_values($titles) ); } /** * Builds a stable human-readable list block for the prompt. * * @param list $titles */ private function buildTextBlock(?string $tagLabel, array $titles): string { $headline = 'Folgende Einträge sind verfügbar:'; if ($tagLabel !== null && trim($tagLabel) !== '') { $headline = sprintf('Folgende %s sind verfügbar:', trim($tagLabel)); } $lines = []; foreach ($titles as $title) { $lines[] = '- ' . $title; } return $headline . "\n\n" . implode("\n", $lines); } }