017c9b3c12
ai-sdk (legal_rag_client/scroll/types) liest die gepinnten Spec-Felder
article_label/regulation_code/article/paragraph/sub/citation_style/is_recital
mit Fallback auf alt-ingestierte Chunks (regulation_id, section); neuer getBool-Helfer.
Advisor + Drafting-Engine bilden die Quellenzeile primaer aus article_label
("BDSG § 38 Abs. 1"), sonst aus den strukturierten Feldern. 17 Tests gruen, tsc sauber.
Vertrag: docs-src/development/rag_reingest_spec.md (§2/§7). Deploy an den Re-Ingest
gekoppelt — neue Felder sind bis dahin leer (graceful Fallback).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
/**
|
|
* Compliance-Advisor RAG-Suche.
|
|
*
|
|
* Fragt die ai-compliance-sdk (`/sdk/v1/rag/search`) ab statt des frueheren
|
|
* `rag-service:8097` (auf prod nicht erreichbar). Die ai-sdk embeddet die Query
|
|
* mit bge-m3 (prod: ollama-embed) und sucht in den Qdrant-Compliance-Collections
|
|
* — damit profitiert der Advisor vom reicheren Embedding.
|
|
*
|
|
* Fehler je Collection werden geschluckt (graceful: Antwort ohne diesen Treffer).
|
|
*/
|
|
|
|
const SDK_URL =
|
|
process.env.SDK_API_URL || process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
|
|
|
|
const DEFAULT_USER = '00000000-0000-0000-0000-000000000001'
|
|
const DEFAULT_TENANT =
|
|
process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
|
|
|
|
// Compliance-relevante Collections (ai-sdk-Whitelist `AllowedCollections`).
|
|
export const COMPLIANCE_COLLECTIONS = [
|
|
'bp_compliance_gesetze',
|
|
'bp_compliance_ce',
|
|
'bp_compliance_datenschutz',
|
|
'bp_dsfa_corpus',
|
|
'bp_compliance_recht',
|
|
'bp_legal_templates',
|
|
] as const
|
|
|
|
interface SdkRagResult {
|
|
text?: string
|
|
regulation_code?: string
|
|
regulation_name?: string
|
|
regulation_short?: string
|
|
article_label?: string
|
|
article?: string
|
|
paragraph?: string
|
|
sub?: string
|
|
citation_style?: string
|
|
is_recital?: boolean
|
|
category?: string
|
|
source_url?: string
|
|
score?: number
|
|
}
|
|
|
|
interface ScoredPassage {
|
|
content: string
|
|
source: string
|
|
score: number
|
|
}
|
|
|
|
/** Normalisiert eine ai-sdk-RAG-Antwort auf {content, source, score}. */
|
|
export function mapSdkResults(results: SdkRagResult[] | undefined): ScoredPassage[] {
|
|
return (results || [])
|
|
.map((r) => ({
|
|
content: r.text || '',
|
|
// Fundstelle: article_label ist die fertig formatierte, druckbare Quelle aus der
|
|
// Ingestion ("BDSG § 38 Abs. 1"); Fallback baut sie aus den strukturierten Feldern
|
|
// (bzw. alt-ingestierte Chunks ohne Legal-Metadaten). Siehe rag_reingest_spec.md §2/§7.
|
|
source:
|
|
(r.article_label && r.article_label.trim()) ||
|
|
[r.regulation_short || r.regulation_name || r.regulation_code, r.article, r.paragraph, r.sub]
|
|
.filter(Boolean)
|
|
.join(' ') ||
|
|
'Unbekannt',
|
|
score: typeof r.score === 'number' ? r.score : 0,
|
|
}))
|
|
.filter((p) => p.content)
|
|
}
|
|
|
|
async function searchCollection(collection: string, query: string): Promise<ScoredPassage[]> {
|
|
try {
|
|
const res = await fetch(`${SDK_URL}/sdk/v1/rag/search`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-User-ID': DEFAULT_USER,
|
|
'X-Tenant-ID': DEFAULT_TENANT,
|
|
},
|
|
body: JSON.stringify({ query, collection, top_k: 3 }),
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
if (!res.ok) return []
|
|
const data = await res.json()
|
|
return mapSdkResults(data.results)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fragt alle Compliance-Collections parallel ab und liefert die Top-8-Passagen
|
|
* als formatierten Kontextblock (oder '' wenn nichts erreichbar/gefunden).
|
|
*/
|
|
export async function queryAdvisorRAG(query: string): Promise<string> {
|
|
const settled = await Promise.all(
|
|
COMPLIANCE_COLLECTIONS.map((c) => searchCollection(c, query)),
|
|
)
|
|
const all = settled.flat()
|
|
if (all.length === 0) return ''
|
|
all.sort((a, b) => b.score - a.score)
|
|
return all
|
|
.slice(0, 8)
|
|
.map((r, i) => `[Quelle ${i + 1}: ${r.source}]\n${r.content}`)
|
|
.join('\n\n---\n\n')
|
|
}
|