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
+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
$toolName = 'legal-analysis';
$toolTitle = 'Legal Analysis';
$toolKind = 'Deep Legal Q&A';
$toolBadge = 'Two-pass';
$extraScripts = ['assets/js/legal-analysis.js'];
require_once __DIR__ . '/includes/layout.php';
?>
<form id="laForm" class="tool-form" novalidate>
<div class="lang-switcher" id="laLangSwitcher" role="group" aria-label="UI language">
<button type="button" class="lang-btn la-lang-btn is-active" data-lang="no">&#127475;&#127476; NO</button>
<button type="button" class="lang-btn la-lang-btn" data-lang="en">&#127468;&#127463; EN</button>
</div>
<div class="control-row" id="laDocTypeControl">
<span class="control-label">Document type</span>
<label><input type="radio" name="laDocType" value="auto" checked> Auto-detect</label>
<label><input type="radio" name="laDocType" value="barnevernet"> Barnevernet</label>
<label><input type="radio" name="laDocType" value="adopsjon"> Adopsjon</label>
<label><input type="radio" name="laDocType" value="emergency"> Akutt-plassering</label>
<label><input type="radio" name="laDocType" value="samvær"> Samvær</label>
<label><input type="radio" name="laDocType" value="fylkesnemnd"> Fylkesnemnd</label>
<label><input type="radio" name="laDocType" value="other"> Annet</label>
</div>
<p class="upload-hint">
Engine: <strong>dbn-legal-agent-v3</strong> (Norwegian legal fine-tune on GPU). Expect ~30-60 seconds per issue, up to 5 issues per run.
</p>
<div id="docPickerSection" class="doc-picker-section">
<button type="button" id="docPickerBtn" class="doc-picker-btn" aria-haspopup="dialog">
<svg class="doc-picker-btn__icon" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="2" y="1" width="9" height="12" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M5 5h5M5 8h3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><rect x="7" y="9" width="6" height="5" rx="1" fill="white" stroke="currentColor" stroke-width="1.3"/><path d="M9 11h2M9 12.5h1" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></svg>
<span>Select from My Docs</span>
</button>
<div id="docPickerChips" class="doc-picker-chips" aria-label="Selected documents"></div>
<input type="hidden" id="docPickerIds" name="doc_ids" value="">
</div>
<div class="upload-zone" id="laUploadZone" role="region" aria-label="File upload">
<input type="file" id="laUploadInput" multiple accept=".pdf,.docx,.txt" aria-label="Choose files to analyse">
<div id="laUploadPrompt" class="upload-prompt">
<span class="upload-icon" aria-hidden="true">&#8679;</span>
<p>Drop up to 5 files here, or <label for="laUploadInput" class="upload-browse">browse</label></p>
<p class="upload-hint"><strong>PDF</strong>, <strong>DOCX</strong>, <strong>TXT</strong> &mdash; text extracted in memory, never stored</p>
</div>
<div id="laUploadFileInfo" class="upload-file is-hidden">
<ul id="laUploadFileList" class="upload-file-list"></ul>
<button type="button" id="laUploadClear" class="upload-clear">&times; Clear files</button>
</div>
</div>
<label class="input-label" for="laInput">Pasted text <small class="control-hint">(optional if file or doc selected)</small></label>
<textarea id="laInput" name="text" rows="8" placeholder="Paste a case note, court decision, vedtak, brev, or any legal document text. You can also upload a file or select from My Docs above — at least one source is required."></textarea>
<div class="form-footer">
<p id="laStatus" class="form-status" role="status" aria-live="polite"></p>
<button id="laRunButton" type="submit">Run legal analysis</button>
</div>
</form>
<section id="laResults" class="results" aria-live="polite">
<div class="empty-state">
<h3>Ready</h3>
<p>Upload a document or paste text — the tool will extract up to 5 distinct legal issues, then ask the Norwegian-law fine-tune to answer each one with citations.</p>
<p class="upload-hint">Pass 1 uses Azure GPT-4o-mini to spot issues. Pass 2 calls the dbn-legal-agent-v3 fine-tune on ocelot for each one. Pass 3 synthesises the overall picture. A typical run takes 2-5 minutes.</p>
</div>
</section>
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
<div class="is-hidden" id="languageControl" aria-hidden="true">
<input type="radio" name="language" value="en">
<input type="radio" name="language" value="no" checked>
</div>
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
<div class="is-hidden" id="audioZone" aria-hidden="true">
<input type="file" id="audioInput" style="display:none">
<div id="audioPrompt"></div>
<div id="audioFileInfo"><ol id="audioQueueList"></ol><button type="button" id="audioClear"></button></div>
</div>
<div class="is-hidden" id="diarizeControl" aria-hidden="true">
<input type="checkbox" id="diarizeCheck">
<input type="number" id="numSpeakersInput">
</div>
<div class="is-hidden" id="transcribeLangControl" aria-hidden="true"><input type="radio" name="transcribeLang" value="no" checked></div>
<div class="is-hidden" id="vocabControl" aria-hidden="true">
<div id="vocabPresets"></div>
<textarea id="initPromptInput"></textarea>
</div>
<div class="is-hidden" id="aliasSection" aria-hidden="true">
<button type="button" id="addAliasRow"></button>
<div id="aliasRows"></div>
</div>
<div class="is-hidden" id="exemptSection" aria-hidden="true">
<button type="button" id="addExemptRow"></button>
<div id="exemptRows"></div>
</div>
<div class="is-hidden" id="uploadZone" aria-hidden="true">
<input type="file" id="uploadInput">
<div id="uploadPrompt"></div>
<div id="uploadFileInfo"><ul id="uploadFileList"></ul><button type="button" id="uploadClear"></button></div>
</div>
<?php require_once __DIR__ . '/includes/layout_footer.php'; ?>