feat(ai-sdk): Advisor Reasoning Stack — Clarity+G1+Concept-Injector+Context-Scope+Term-Resolution+E4-Curation+Intent-Signal
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Legal Concept Ontology — the fachliche IP bridge for the Concept->Norm recall
|
||||
// injector. The words users type ("Datenschutzerklärung", "Cookie Banner") are
|
||||
// rarely identical to the article titles that actually govern them (Art. 12/13/14
|
||||
// DSGVO, § 25 TDDDG). Embedding similarity misses this leap, so these bridges are
|
||||
// curated: concept keyword -> load-bearing norm_ids. This is NOT a fallback to
|
||||
// hardcoding — it is domain knowledge that surfaces the normatively load-bearing
|
||||
// units within the (already correctly retrieved) documents.
|
||||
type conceptNorm struct {
|
||||
keywords []string
|
||||
normIDs []string
|
||||
}
|
||||
|
||||
var legalConceptOntology = []conceptNorm{
|
||||
{[]string{"datenschutzerklärung", "datenschutzerklaerung", "privacy policy", "datenschutzhinweise", "datenschutzinformation"},
|
||||
[]string{"EU-DSGVO-Art12", "EU-DSGVO-Art13", "EU-DSGVO-Art14"}},
|
||||
{[]string{"cookie banner", "cookie-banner", "cookies", "cookie", "tracking"},
|
||||
[]string{"DE-TDDDG-§25", "EU-DSGVO-Art6", "EU-DSGVO-Art7"}},
|
||||
{[]string{"dsfa", "folgenabschätzung", "folgenabschaetzung", "datenschutz-folgenabschätzung"},
|
||||
[]string{"EU-DSGVO-Art35", "EU-DSGVO-Art36"}},
|
||||
{[]string{"auskunft", "auskunftsrecht", "auskunftsersuchen"},
|
||||
[]string{"EU-DSGVO-Art15"}},
|
||||
{[]string{"löschung", "loeschung", "vergessenwerden", "recht auf vergessen"},
|
||||
[]string{"EU-DSGVO-Art17"}},
|
||||
{[]string{"datenübertragbarkeit", "datenuebertragbarkeit", "portabilität", "portabilitaet"},
|
||||
[]string{"EU-DSGVO-Art20"}},
|
||||
{[]string{"widerspruch", "widerspruchsrecht"},
|
||||
[]string{"EU-DSGVO-Art21"}},
|
||||
{[]string{"datenpanne", "datenschutzverletzung", "data breach", "verletzung des schutzes"},
|
||||
[]string{"EU-DSGVO-Art33", "EU-DSGVO-Art34"}},
|
||||
// E4-Quick-Curation (2026-07-01): resolved abbreviations (E2) pull their core norms.
|
||||
{[]string{"technische und organisatorische maßnahmen", "technische und organisatorische massnahmen"},
|
||||
[]string{"EU-DSGVO-Art32", "EU-DSGVO-Art25", "EU-DSGVO-Art5"}},
|
||||
{[]string{"verzeichnis von verarbeitungstätigkeiten", "verzeichnis von verarbeitungstaetigkeiten", "verarbeitungsverzeichnis"},
|
||||
[]string{"EU-DSGVO-Art30"}},
|
||||
{[]string{"auftragsverarbeitungsvertrag", "auftragsverarbeitung", "auftragsverarbeiter"},
|
||||
[]string{"EU-DSGVO-Art28"}},
|
||||
{[]string{"datenschutzbeauftragt"},
|
||||
[]string{"EU-DSGVO-Art37", "EU-DSGVO-Art38", "EU-DSGVO-Art39"}},
|
||||
}
|
||||
|
||||
// ConceptNorms returns the load-bearing norm_ids for the concepts named in the
|
||||
// query (dedup, order-preserving). Empty if no concept is named.
|
||||
func ConceptNorms(query string) []string {
|
||||
q := strings.ToLower(query)
|
||||
seen := map[string]bool{}
|
||||
out := []string{}
|
||||
for _, cn := range legalConceptOntology {
|
||||
for _, kw := range cn.keywords {
|
||||
if strings.Contains(q, kw) {
|
||||
for _, nid := range cn.normIDs {
|
||||
if !seen[nid] {
|
||||
seen[nid] = true
|
||||
out = append(out, nid)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// InjectConceptNorms merges concept-injected norm units into the results so the
|
||||
// load-bearing norms are VISIBLE in the evidence set. Dedups by citation_unit
|
||||
// (skips norms already retrieved), then re-sorts by score — the injected units
|
||||
// carry a just-below-top score so they surface high WITHOUT displacing the top
|
||||
// document hit (inject, don't blindly dominate). Caps at topK.
|
||||
func InjectConceptNorms(results, injected []LegalSearchResult, topK int) []LegalSearchResult {
|
||||
if len(injected) == 0 {
|
||||
return results
|
||||
}
|
||||
present := map[string]bool{}
|
||||
for _, r := range results {
|
||||
if r.CitationUnit != "" {
|
||||
present[r.CitationUnit] = true
|
||||
}
|
||||
}
|
||||
merged := append([]LegalSearchResult{}, results...)
|
||||
for _, in := range injected {
|
||||
if in.CitationUnit != "" && !present[in.CitationUnit] {
|
||||
merged = append(merged, in)
|
||||
present[in.CitationUnit] = true
|
||||
}
|
||||
}
|
||||
sort.SliceStable(merged, func(i, j int) bool { return merged[i].Score > merged[j].Score })
|
||||
if topK > 0 && len(merged) > topK {
|
||||
merged = merged[:topK]
|
||||
}
|
||||
return merged
|
||||
}
|
||||
Reference in New Issue
Block a user