Add Legal Analysis tool — two-pass DBN-legal pipeline

Restores the dbn-legal-agent-v3 fine-tune on ocelot (was silently aliased
to plain qwen2.5:14b in LiteLLM since the viper retirement) and ships a
new tool that uses it via a two-pass flow:

  Pass 1 (Azure 4o-mini)  → extract up to 5 distinct legal issues
  Pass 2 (ocelot v3 only) → answer each issue, ≤350 tokens, with corpus
  Pass 3 (Azure 4o-mini)  → synthesise overall assessment + next steps

The 12GB-VRAM constraint motivates the split: dbn-legal-agent-v3 stays
hot in VRAM through the 5 sequential per-issue calls because issue
extraction and synthesis run on Azure, not on ocelot.

New surface:
  - includes/LegalAnalysisAgent.php
  - api/legal-analysis.php           (NDJSON streaming endpoint)
  - legal-analysis.php               (dedicated tool page)
  - assets/js/legal-analysis.js      (streamed UI with per-issue cards)
  - Save-result + case-result.php rendering for legal-analysis output
  - Nav registration in all four UI languages

Add-on integration: a "⚖️🇳🇴 Run deep legal analysis on this text"
button now appears on Summarize, Ask, and Redact result pages and
streams the same pipeline inline below the existing result.

Existing tools relabelled: the misleading "🇳🇴 Norwegian specialist v3 "
option on advocate/deep-research/discrepancy/barnevernet is now honestly
"DBN Legal Agent" — now that the real fine-tune is actually deployed,
the label finally matches reality. The advocate.php v2 option was
removed since the v2 GGUF is retired.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 04:21:01 +02:00
parent 2013648ee0
commit 7e6463ed22
14 changed files with 1361 additions and 25 deletions
+48
View File
@@ -298,6 +298,53 @@ function crField(string $label, string $value): string {
<?php endif; ?>
<?= crListBlock('What Remains Uncertain', (array)($output['what_remains_uncertain'] ?? [])) ?>
<?php elseif ($toolSlug === 'legal-analysis'): ?>
<?php if (!empty($output['overall_assessment'])): ?>
<div class="cr-block">
<h3>Overall assessment</h3>
<p style="line-height:1.6"><?= htmlspecialchars((string)$output['overall_assessment']) ?></p>
</div>
<?php endif; ?>
<?php if (!empty($output['next_steps']) && is_array($output['next_steps'])): ?>
<?= crListBlock('Next steps', $output['next_steps']) ?>
<?php endif; ?>
<?php if (!empty($output['issues']) && is_array($output['issues'])): ?>
<div class="cr-block">
<h3>Legal issues</h3>
<?php foreach ($output['issues'] as $iss): if (!is_array($iss)) continue;
$sev = (string)($iss['severity'] ?? 'medium');
$sevColor = $sev === 'high' ? '#fee2e2' : ($sev === 'low' ? '#dcfce7' : '#fef3c7');
$sevText = $sev === 'high' ? '#b91c1c' : ($sev === 'low' ? '#166534' : '#92400e');
?>
<article style="border:1px solid #d8dde7;border-radius:8px;padding:0.85rem 1rem;margin-bottom:0.75rem;">
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.4rem;">
<span style="font-weight:700;color:#64748b;font-size:0.85rem;">#<?= (int)($iss['id'] ?? 0) ?></span>
<span style="font-size:0.7rem;font-weight:700;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:999px;background:<?= $sevColor ?>;color:<?= $sevText ?>;"><?= htmlspecialchars(strtoupper($sev)) ?></span>
</div>
<h4 style="margin:0.2rem 0 0.4rem;font-size:1.02rem;"><?= htmlspecialchars((string)($iss['question'] ?? '')) ?></h4>
<?php if (!empty($iss['brief_context'])): ?>
<p style="font-size:0.85rem;color:#64748b;margin:0 0 0.5rem"><em><?= htmlspecialchars((string)$iss['brief_context']) ?></em></p>
<?php endif; ?>
<div style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid #eef2f7;">
<h5 style="margin:0 0 0.3rem;font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em;color:#64748b;">Svar (dbn-legal-agent-v3)</h5>
<p style="margin:0;line-height:1.55;white-space:pre-wrap;"><?= htmlspecialchars((string)($iss['answer'] ?? '')) ?></p>
</div>
<?php if (!empty($iss['legal_basis'])): ?>
<div style="margin-top:0.5rem;font-size:0.85rem;color:#475569;background:#f8fafc;padding:0.3rem 0.55rem;border-radius:5px;display:inline-block;">
<strong>Lovgrunnlag:</strong> <?= htmlspecialchars((string)$iss['legal_basis']) ?>
</div>
<?php endif; ?>
<?php if (!empty($iss['what_to_check'])): ?>
<p style="margin:0.55rem 0 0;font-size:0.8rem;color:#94a3b8;"><em><?= htmlspecialchars((string)$iss['what_to_check']) ?></em></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($output['disclaimer'])): ?>
<p style="font-size:0.85rem;color:#64748b;margin-top:1rem;"><em><?= htmlspecialchars((string)$output['disclaimer']) ?></em></p>
<?php endif; ?>
<?php else: ?>
<?php if ($primaryOutput !== ''): ?>
<div class="cr-output"><?= htmlspecialchars($primaryOutput) ?></div>
@@ -381,6 +428,7 @@ function crField(string $label, string $value): string {
'ask': '/ask.php',
'redact': '/redact.php',
'transcribe': '/transcribe.php',
'legal-analysis':'/legal-analysis.php',
}[tool] || '/dashboard.php';
window.location.href = path + '?rerun=' + <?= (int)$result['id'] ?>;
});