feat(ai-sdk): authority-aware re-ranking for legal RAG (Phase 1) (#31)
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 5s
CI / validate-canonical-controls (push) Successful in 4s
CI / loc-budget (push) Successful in 28s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Successful in 58s
CI / iace-gt-coverage (push) Successful in 16s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

This commit was merged in pull request #31.
This commit is contained in:
2026-06-23 09:30:52 +00:00
parent 23c6ac6f32
commit a1f425d43a
9 changed files with 641 additions and 2 deletions
@@ -93,6 +93,13 @@ func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string,
hits = denseHits
}
// Stratified: den binding_law-Pool ERGAENZEN (nicht ersetzen), damit die Pflichtquelle
// immer Kandidat ist — Guidance bleibt als Auslegungskontext erhalten. Best-effort:
// Fehler beim Binding-Query degradieren still auf den semantischen Pool.
if bindingHits, bErr := c.searchBinding(ctx, collection, embedding, topK); bErr == nil {
hits = mergeDedupHits(hits, bindingHits)
}
results := make([]LegalSearchResult, len(hits))
for i, hit := range hits {
// Legal-Metadaten nach rag_reingest_spec.md §2: bevorzugt die normalisierten Felder
@@ -121,12 +128,41 @@ func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string,
Pages: getIntSlice(hit.Payload, "pages"),
SourceURL: getString(hit.Payload, "source"),
Score: hit.Score,
AuthorityWeight: getInt(hit.Payload, "authority_weight"),
SourceClass: getString(hit.Payload, "source_class"),
Jurisdiction: getString(hit.Payload, "jurisdiction"),
}
}
// Authority-aware Re-Ranking: bindendes Recht der passenden Jurisdiktion/Domaene nach
// oben, Guidance/Fremdrecht/Off-Domain runter (nichts wird geloescht). Reihenfolge only,
// Response-Schema unveraendert. Score traegt den Authority-Score, damit nachgelagerte
// Multi-Collection-Merges (Advisor) die Ordnung bewahren.
results = rerankByAuthority(query, results)
if topK > 0 && len(results) > topK {
results = results[:topK]
}
return results, nil
}
// mergeDedupHits concatenates two hit lists, keeping the first occurrence of each point ID.
func mergeDedupHits(primary, extra []qdrantSearchHit) []qdrantSearchHit {
seen := make(map[string]bool, len(primary)+len(extra))
out := make([]qdrantSearchHit, 0, len(primary)+len(extra))
for _, list := range [][]qdrantSearchHit{primary, extra} {
for _, h := range list {
id := fmt.Sprint(h.ID)
if seen[id] {
continue
}
seen[id] = true
out = append(out, h)
}
}
return out
}
// FormatLegalContextForPrompt formats the legal context for inclusion in an LLM prompt.
func (c *LegalRAGClient) FormatLegalContextForPrompt(lc *LegalContext) string {
if lc == nil || len(lc.Results) == 0 {