Add dbn-legal-agent targeted check step to BVJ Analyzer (Step 6b)

Probe testing revealed the fine-tune loops when asked to check a brief
directly (tool-planning architecture conflict) but answers focused legal
Q&A reliably in ~55s. New step 6b asks one targeted question per document
type (akuttvedtak → § 4-25 klar nødvendighet, adopsjon → Strand Lobben,
undersøkelse → fvl § 17/§ 41) and merges the finding into
procedural_red_flags with check_model provenance. Silent on timeout/error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:38:29 +02:00
parent 04555a96b1
commit 47aa35e946
2 changed files with 141 additions and 0 deletions
+122
View File
@@ -845,3 +845,125 @@ function dbnToolsLiteLLMEmbedBatch(array $texts, string $model = 'nomic-embed-te
usort($data, fn($a, $b) => ($a['index'] ?? 0) <=> ($b['index'] ?? 0));
return array_map(fn($d) => $d['embedding'], $data);
}
// ── dbn-legal-agent targeted check step ──────────────────────────────────────
//
// dbn-legal-agent is a QLoRA fine-tune trained for single-turn Q&A with a baked-in
// "Kilder:" citation scaffold. It loops when asked to check a full brief (no trained
// behavior for that task) but answers focused legal questions reliably in ~55s.
//
// Strategy: ask ONE targeted question per document type, cap tokens at 350 to cut
// off before the tool-planning loop kicks in, parse the answer for legal findings.
function dbnToolsRunLegalCheck(string $brief, string $docType): array
{
$question = dbnToolsSelectCheckQuestion($docType, $brief);
if ($question === null) {
return [];
}
$opts = [
'model' => 'dbn-legal-agent',
'temperature' => 0.1,
'max_tokens' => 350,
'timeout' => 120,
// No 'json' key — plain narrative, no response_format flag
];
try {
$response = dbnToolsCallGpuLlm(
[['role' => 'user', 'content' => $question]],
$opts
);
$text = trim((string)($response['choices'][0]['message']['content'] ?? ''));
} catch (Throwable $e) {
return [];
}
if (empty($text) || str_word_count($text) < 15) {
return [];
}
$clean = dbnToolsExtractCleanAnswer($text);
if (mb_strlen($clean) < 40) {
return [];
}
return [[
'description' => mb_substr($clean, 0, 280),
'severity' => dbnToolsInferCheckSeverity($clean),
'legal_basis' => dbnToolsExtractCheckLegalBasis($clean),
'source_refs' => [],
'what_to_check'=> 'Verifiser med norsk familieretsadvokat',
'check_model' => 'dbn-legal-agent',
]];
}
function dbnToolsSelectCheckQuestion(string $docType, string $brief): ?string
{
$t = mb_strtolower($docType);
$b = mb_strtolower($brief);
if (str_contains($t, 'akutt') || str_contains($t, 'emergency')) {
return 'Hva er den korrekte rettslige terskelen for midlertidig plassering utenfor hjemmet '
. 'etter barnevernsloven § 4-25 (2021-loven)? Er det forskjell mellom § 4-6 (1992-loven) '
. 'og § 4-25 (2021-loven), og er «klar nødvendighet» det riktige kravet?';
}
if (str_contains($t, 'adopsjon') || str_contains($t, 'adoption')
|| str_contains($b, '§ 5-8') || str_contains($b, '§ 4-20')) {
return 'Hva krever Høyesterett og EMD (særlig Strand Lobben mot Norge, saksnr. 37283/13, 2019) '
. 'for at fratakelse av foreldreansvar og adopsjon uten samtykke skal være lovlig? '
. 'Hvilken rolle spiller dokumentert rehabiliteringsplan og biologiske familiebånd?';
}
if (str_contains($t, 'undersøkelse') || str_contains($t, 'investigation')
|| (str_contains($b, 'varsel') && str_contains($b, 'hjemmebesøk'))) {
return 'Hva er konsekvensene etter forvaltningsloven § 17 (kontradiksjonsprinsippet) og '
. '§ 41 (ugyldighet) dersom barnevernstjenesten gjennomfører hjemmebesøk uten å ha '
. 'sendt varsel om undersøkelse i en ikke-akutt situasjon?';
}
// Generic procedural check for other document types
return 'Hva er de viktigste prosessuelle kravene barnevernstjenesten må oppfylle ved '
. 'inngripende vedtak etter barnevernsloven 2021, særlig knyttet til varsel, '
. 'begrunnelse, forholdsmessighet og EMD-forpliktelser?';
}
function dbnToolsExtractCleanAnswer(string $text): string
{
// Strip loop artifacts: tool-planning stubs and "Final:" repetition blocks
$cutPatterns = ['Tool calls:', 'Tool_calls:', '"tool_calls"', 'Final:', 'Final answer:'];
foreach ($cutPatterns as $marker) {
$pos = mb_stripos($text, $marker);
if ($pos !== false && $pos > 40) {
$text = mb_substr($text, 0, $pos);
}
}
// Preserve Kilder: section for legal citations but cap its length
$srcPos = mb_strpos($text, 'Kilder:');
if ($srcPos !== false && $srcPos > 60) {
$text = mb_substr($text, 0, $srcPos + 180);
}
return trim($text);
}
function dbnToolsInferCheckSeverity(string $text): string
{
if (preg_match('/ugyldig|§\s*41|kontradiksjon|klar nødvendighet|strand lobben|biologiske bånd/i', $text)) {
return 'high';
}
if (preg_match('/prosessuell|varsel|terskel|paragrafnummer|forholdsmessig|rehabiliter/i', $text)) {
return 'medium';
}
return 'low';
}
function dbnToolsExtractCheckLegalBasis(string $text): string
{
preg_match_all('/§\s*\d[\d\-a-zA-Z]*/u', $text, $m);
if (!empty($m[0])) {
return implode(', ', array_unique(array_slice($m[0], 0, 3)));
}
return '';
}