Azure mini finishes fastest. Azure full produces the most thorough advocate brief. Norwegian specialist v3 is a Qwen2.5 fine-tune trained on barnevernsloven, ECHR, and forvaltningsloven — highest precision for § 4-25, Strand Lobben, and procedural red flags.
Engine applies to the final advocacy synthesis only. Norwegian specialist v3 is the recommended choice for Barnevernet documents — it is fine-tuned on § 4-25, Strand Lobben, forvaltningsloven § 17/§ 41, and procedural red-flag detection. Classification, party extraction, and timeline always use azure-mini.
Azure mini is the default and finishes fastest. Azure full is the most thorough. Norwegian specialist v3 is a Qwen2.5 fine-tune optimised for barnevernsloven, ECHR, and forvaltningsloven — best for cases involving § 4-25, Strand Lobben, or procedural challenges.
Engine applies to the final synthesis only. Norwegian specialist v3 excels at identifying legally significant discrepancies in Barnevernet documents — procedural violations, threshold errors, and missing statutory justifications. Classification, party extraction, timelines, and cross-referencing always use azure-mini.
diff --git a/includes/CaseResults.php b/includes/CaseResults.php
index f4c2899..0af400b 100644
--- a/includes/CaseResults.php
+++ b/includes/CaseResults.php
@@ -33,6 +33,7 @@ final class CaseResults
'ask',
'redact',
'transcribe',
+ 'legal-analysis',
];
/** True when the user is on a tier that gets saved results (Plus, Pro, or active Plus trial). */
@@ -234,10 +235,11 @@ final class CaseResults
'deep-research' => 'Dyp analyse',
'discrepancy' => 'Motstrid',
'timeline' => 'Tidslinje',
- 'summarize' => 'Sammendrag',
- 'ask' => 'Spørsmål & svar',
- 'redact' => 'Anonymisering',
- 'transcribe' => 'Transkripsjon',
+ 'summarize' => 'Sammendrag',
+ 'ask' => 'Spørsmål & svar',
+ 'redact' => 'Anonymisering',
+ 'transcribe' => 'Transkripsjon',
+ 'legal-analysis' => 'Juridisk analyse',
][$tool] ?? ucfirst($tool);
}
@@ -251,10 +253,11 @@ final class CaseResults
'deep-research' => '🔬',
'discrepancy' => '🔍',
'timeline' => '📅',
- 'summarize' => '📝',
- 'ask' => '💬',
- 'redact' => '🖊️',
- 'transcribe' => '🎙️',
+ 'summarize' => '📝',
+ 'ask' => '💬',
+ 'redact' => '🖊️',
+ 'transcribe' => '🎙️',
+ 'legal-analysis' => '⚖️🇳🇴',
][$tool] ?? '📄';
}
@@ -272,6 +275,7 @@ final class CaseResults
'ask' => [$input['question'] ?? null],
'redact' => [$input['text'] ?? null],
'transcribe' => [$input['filename'] ?? null],
+ 'legal-analysis' => [$input['doc_type'] ?? null, $input['text'] ?? null],
default => [$input['title'] ?? null, $input['query'] ?? null, $input['text'] ?? null],
};
foreach ($candidates as $c) {
diff --git a/includes/LegalAnalysisAgent.php b/includes/LegalAnalysisAgent.php
new file mode 100644
index 0000000..22a48fd
--- /dev/null
+++ b/includes/LegalAnalysisAgent.php
@@ -0,0 +1,309 @@
+azureMini = (new DbnAzureOpenAiGateway())->withDeployment('gpt-4o-mini');
+ $this->legalSvc = new DbnLegalToolsService();
+ }
+
+ /**
+ * Pass 1 — extract distinct legal issues. Azure-only.
+ *
+ * @return array
+ */
+ public function extractIssues(string $text, string $language, string $docType): array
+ {
+ $locale = dbnToolsLanguageName($language);
+ $text = mb_substr($text, 0, 24000, 'UTF-8'); // keep prompt within 4o-mini context
+
+ $prompt = <<",
+ "brief_context": "<≤2 sentences from the document that triggered this question>",
+ "doc_type": "",
+ "severity_hint": ""
+ }
+ ]
+}
+
+Rules:
+- Skip non-legal observations (logistics, social commentary, opinions).
+- Each question should be answerable with citations to barnevernsloven, EMK Art. X,
+ named Høyesterett/EMD cases — NOT general advice.
+- If the document has fewer than 5 real legal issues, return fewer entries.
+- If NO real legal issue exists, return {"issues": []}.
+
+DOCUMENT:
+---
+{$text}
+---
+PROMPT;
+
+ $raw = $this->azureMini->chatText(
+ [
+ ['role' => 'system', 'content' => 'You return valid JSON only. No prose, no fences.'],
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ ['json' => true, 'temperature' => 0.1, 'max_tokens' => 1500, 'timeout' => 90]
+ );
+
+ $decoded = $this->azureMini->decodeJsonObject($raw);
+ $issues = is_array($decoded['issues'] ?? null) ? $decoded['issues'] : [];
+
+ $clean = [];
+ $id = 1;
+ foreach ($issues as $issue) {
+ $question = trim((string)($issue['question'] ?? ''));
+ if ($question === '' || mb_strlen($question, 'UTF-8') < 10) {
+ continue;
+ }
+ $clean[] = [
+ 'id' => $id++,
+ 'question' => mb_substr($question, 0, 280, 'UTF-8'),
+ 'brief_context' => mb_substr(trim((string)($issue['brief_context'] ?? '')), 0, 400, 'UTF-8'),
+ 'doc_type' => (string)($issue['doc_type'] ?? $docType),
+ 'severity_hint' => in_array($issue['severity_hint'] ?? '', ['high','medium','low'], true)
+ ? $issue['severity_hint']
+ : 'medium',
+ ];
+ if (count($clean) >= self::MAX_ISSUES) {
+ break;
+ }
+ }
+ return $clean;
+ }
+
+ /**
+ * Pass 2 — single targeted question to dbn-legal-agent-v3 with corpus context.
+ * Ocelot-only. Capped at 350 tokens / 60s to avoid the documented loop bug.
+ *
+ * @param array{id:int,question:string,brief_context:string,doc_type:string,severity_hint:string} $issue
+ * @return array{id:int,question:string,answer:string,severity:string,legal_basis:string,citations_from_corpus:array,what_to_check:string,brief_context:string}
+ */
+ public function answerIssue(array $issue, string $corpusContext, string $language): array
+ {
+ $sysMsg = 'Du er en ekspert på norsk barnevernsloven og EMD-praksis. '
+ . 'Svar alltid på norsk med korrekt juridisk terminologi. '
+ . 'Bruk terskler fra barnevernsloven 2021: § 4-25 krever «klar nødvendighet». '
+ . 'Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. '
+ . 'Aldri oppfinn paragrafnumre, saksnumre eller dommernavn. '
+ . 'Avslutt med en «Kilder:»-seksjon som lister lovparagrafer og dommer du har sitert.';
+
+ $userMsg = $issue['question'];
+ if ($issue['brief_context'] !== '') {
+ $userMsg .= "\n\nKontekst fra saken: " . $issue['brief_context'];
+ }
+ if ($corpusContext !== '') {
+ $userMsg .= "\n\nRelevante kilder fra Do Better Norge-korpuset:\n" . $corpusContext;
+ }
+
+ $answer = '';
+ $error = null;
+ try {
+ $response = dbnToolsCallGpuLlm(
+ [
+ ['role' => 'system', 'content' => $sysMsg],
+ ['role' => 'user', 'content' => $userMsg],
+ ],
+ [
+ 'model' => self::LEGAL_MODEL,
+ 'temperature' => 0.1,
+ 'max_tokens' => self::LEGAL_MAX_TOKENS,
+ 'timeout' => self::LEGAL_TIMEOUT,
+ ]
+ );
+ $answer = trim((string)($response['choices'][0]['message']['content'] ?? ''));
+ } catch (Throwable $e) {
+ $error = $e->getMessage();
+ }
+
+ $clean = dbnToolsExtractCleanAnswer($answer);
+ if (mb_strlen($clean, 'UTF-8') < 30) {
+ $clean = $answer !== ''
+ ? $answer
+ : ($error !== null ? "[Modellfeil: $error]" : '[Modellen returnerte ingen brukbar tekst.]');
+ }
+
+ $severity = $clean !== '' ? dbnToolsInferCheckSeverity($clean) : $issue['severity_hint'];
+ $legalBasis = dbnToolsExtractCheckLegalBasis($clean);
+
+ return [
+ 'id' => $issue['id'],
+ 'question' => $issue['question'],
+ 'brief_context' => $issue['brief_context'],
+ 'answer' => $clean,
+ 'severity' => $severity,
+ 'legal_basis' => $legalBasis,
+ 'citations_from_corpus' => [], // populated by orchestrator if it kept the chunks
+ 'what_to_check' => 'Verifiser med norsk familieretsadvokat før handling.',
+ ];
+ }
+
+ /**
+ * Pass 3 — synthesise overall assessment. Azure-only.
+ */
+ public function synthesise(array $issues, string $language, string $docType): array
+ {
+ $locale = dbnToolsLanguageName($language);
+
+ $bullets = [];
+ foreach ($issues as $i) {
+ $bullets[] = sprintf(
+ "- [%s] %s\n Svar: %s",
+ strtoupper((string)$i['severity']),
+ $i['question'],
+ mb_substr((string)$i['answer'], 0, 600, 'UTF-8')
+ );
+ }
+ $issuesBlock = implode("\n", $bullets);
+
+ $prompt = <<",
+ "next_steps": ["", "", ""],
+ "disclaimer": "This is automated legal analysis, not legal advice. Verify with a qualified Norwegian lawyer before acting."
+}
+PROMPT;
+
+ try {
+ $raw = $this->azureMini->chatText(
+ [
+ ['role' => 'system', 'content' => 'You return valid JSON only. No prose, no fences.'],
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ ['json' => true, 'temperature' => 0.2, 'max_tokens' => 700, 'timeout' => 60]
+ );
+ $decoded = $this->azureMini->decodeJsonObject($raw);
+ if (is_array($decoded) && !empty($decoded['overall_assessment'])) {
+ return [
+ 'overall_assessment' => (string)$decoded['overall_assessment'],
+ 'next_steps' => is_array($decoded['next_steps'] ?? null) ? array_slice($decoded['next_steps'], 0, 5) : [],
+ 'disclaimer' => (string)($decoded['disclaimer'] ?? 'Automated analysis — not legal advice.'),
+ ];
+ }
+ } catch (Throwable $e) {
+ error_log('legal-analysis synthesis failed: ' . $e->getMessage());
+ }
+
+ return [
+ 'overall_assessment' => 'Synthesis step did not return structured output. See individual issue answers below.',
+ 'next_steps' => [],
+ 'disclaimer' => 'Automated analysis — not legal advice. Verify with a qualified Norwegian lawyer.',
+ ];
+ }
+
+ /**
+ * Full orchestrated run. Emits progress events via the $emit callable.
+ *
+ * @param callable $emit (string $event, array $payload): void
+ */
+ public function runFullAnalysis(string $text, string $language, string $docType, callable $emit): array
+ {
+ $startMs = (int)round(microtime(true) * 1000);
+
+ // Pass 1
+ $emit('progress', ['step' => 'extracting_issues', 'detail' => 'Identifying distinct legal issues…']);
+ $issues = $this->extractIssues($text, $language, $docType);
+
+ if (empty($issues)) {
+ return [
+ 'ok' => true,
+ 'issues' => [],
+ 'overall_assessment' => 'No discrete legal issues identified in this document.',
+ 'next_steps' => [],
+ 'disclaimer' => 'Automated analysis — not legal advice.',
+ 'model' => self::LEGAL_MODEL,
+ 'latency_ms' => (int)round(microtime(true) * 1000) - $startMs,
+ ];
+ }
+
+ $emit('progress', [
+ 'step' => 'issues_extracted',
+ 'detail' => sprintf('Found %d legal issue(s); asking specialist…', count($issues)),
+ 'issues' => array_map(fn($i) => ['id' => $i['id'], 'question' => $i['question'], 'severity_hint' => $i['severity_hint']], $issues),
+ ]);
+
+ // Pass 2 — one issue at a time
+ $answered = [];
+ foreach ($issues as $issue) {
+ $emit('progress', [
+ 'step' => 'issue_searching_corpus',
+ 'detail' => sprintf('Issue %d: searching legal corpus…', $issue['id']),
+ 'issue_id' => $issue['id'],
+ ]);
+
+ $corpusQuery = $issue['question'] . "\n" . $issue['brief_context'];
+ $corpusContext = $this->legalSvc->corpusContextForSummarize($corpusQuery, 3);
+
+ $emit('progress', [
+ 'step' => 'issue_answering',
+ 'detail' => sprintf('Issue %d: asking dbn-legal-agent-v3…', $issue['id']),
+ 'issue_id' => $issue['id'],
+ ]);
+
+ $answer = $this->answerIssue($issue, $corpusContext, $language);
+ $answered[] = $answer;
+
+ $emit('issue_answered', ['issue' => $answer]);
+ }
+
+ // Pass 3
+ $emit('progress', ['step' => 'synthesising', 'detail' => 'Synthesising overall assessment…']);
+ $synth = $this->synthesise($answered, $language, $docType);
+
+ return [
+ 'ok' => true,
+ 'issues' => $answered,
+ 'overall_assessment' => $synth['overall_assessment'],
+ 'next_steps' => $synth['next_steps'],
+ 'disclaimer' => $synth['disclaimer'],
+ 'doc_type' => $docType,
+ 'model' => self::LEGAL_MODEL,
+ 'latency_ms' => (int)round(microtime(true) * 1000) - $startMs,
+ ];
+ }
+}
diff --git a/includes/i18n.php b/includes/i18n.php
index d051d0e..b1af41b 100644
--- a/includes/i18n.php
+++ b/includes/i18n.php
@@ -1511,6 +1511,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'timeline' => ['Timeline', 'Events and deadlines', 'Extract dates, hearings, Barnevernet milestones, and legal deadlines from notes or files.', 'Process-and-forget'],
'redact' => ['Redact', 'Privacy protection', 'Remove names, ID numbers, phone numbers, and addresses before sharing documents.', 'Deterministic first'],
'summarize' => ['Summarize', 'Document summary', 'Extract key facts, dates, parties, and legal references from any document — with optional legal corpus enrichment.', 'Process-and-forget'],
+ 'legal-analysis' => ['Legal Analysis', 'Deep Norwegian-law Q&A', 'Extract distinct legal issues from a document and answer each with the dbn-legal-agent-v3 fine-tune — citations from barnevernsloven, EMK, and Høyesterett.', 'Fine-tune · Norsk'],
'korrespond' => ['Korrespond', 'Draft & reply to authorities', 'Draft replies or new correspondence to NAV, Barnevernet, schools, Bufdir and other Norwegian authorities — Norwegian + your language, side-by-side, citations verified against the legal corpus.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ Analyzer', 'Barnevernet documents', 'Analyze child-welfare documents from your perspective with procedural red flags and citations.', 'Document + RAG'],
'advocate' => ['Advocate', 'Partisan brief', 'Choose who you represent and generate a source-grounded brief for that position.', 'ECHR + Lovdata'],
@@ -1524,6 +1525,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'timeline' => ['Tidslinje', 'Hendelser og frister', 'Hent ut datoer, møter, barnevernsmilepæler og juridiske frister fra notater eller filer.', 'Behandles og glemmes'],
'redact' => ['Sladder', 'Personvern', 'Fjern navn, ID-numre, telefonnumre og adresser før du deler dokumenter.', 'Deterministisk først'],
'summarize' => ['Sammendrag', 'Dokumentsammendrag', 'Hent ut nøkkelfakta, datoer, parter og juridiske referanser fra et dokument — med valgfri korpusberikelse.', 'Behandles og glemmes'],
+ 'legal-analysis' => ['Juridisk analyse', 'Dyp norsk-rett Q&A', 'Hent ut distinkte juridiske spørsmål fra et dokument og få svar fra dbn-legal-agent-v3 fine-tunen — kilder fra barnevernsloven, EMK og Høyesterett.', 'Fine-tune · Norsk'],
'korrespond' => ['Korrespond', 'Brev og svar til myndighetene', 'Skriv utkast til svar eller nytt brev til NAV, barnevernet, skolen, Bufdir og andre norske myndigheter — bokmål + ditt språk side om side, med verifiserte lovhenvisninger.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ-analyse', 'Barnevernsdokumenter', 'Analyser barnevernsdokumenter fra ditt perspektiv med prosessuelle røde flagg og kilder.', 'Dokument + RAG'],
'advocate' => ['Advokatmodus', 'Partsinnlegg', 'Velg hvem du representerer og lag et kildebelagt innlegg for den posisjonen.', 'EMD + Lovdata'],
@@ -1537,6 +1539,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'timeline' => ['Хронологія', 'Події та строки', 'Витягуйте дати, слухання, етапи Barnevernet і юридичні строки з нотаток або файлів.', 'Обробити і забути'],
'redact' => ['Редагування', 'Захист приватності', 'Видаляйте імена, ідентифікаційні номери, телефони та адреси перед поширенням документів.', 'Детермінований метод'],
'summarize' => ['Резюме', 'Резюме документа', 'Витягуйте ключові факти, дати, сторони та юридичні посилання — з можливістю збагачення корпусом.', 'Обробити і забути'],
+ 'legal-analysis' => ['Юридичний аналіз', 'Глибокий аналіз норвезького права', 'Витягніть юридичні питання з документа та отримайте відповіді від моделі dbn-legal-agent-v3 — з цитатами з barnevernsloven, ЄКПЛ і Verховного суду Норвегії.', 'Fine-tune · Norsk'],
'korrespond' => ['Korrespond', 'Листи і відповіді органам влади', 'Створюйте чернетки відповідей або нових листів до NAV, Barnevernet, школи, Bufdir та інших норвезьких органів — норвезькою + вашою мовою поряд, із перевіреними посиланнями на закон.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ аналізатор', 'Документи Barnevernet', 'Аналізуйте документи захисту дітей з вашої позиції, з процесуальними ризиками та джерелами.', 'Документ + RAG'],
'advocate' => ['Адвокат', 'Позиційний бриф', 'Оберіть, кого представляєте, і створіть бриф із джерелами на підтримку цієї позиції.', 'ЄСПЛ + Lovdata'],
@@ -1550,6 +1553,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'timeline' => ['Oś czasu', 'Wydarzenia i terminy', 'Wyodrębniaj daty, rozprawy, etapy Barnevernet i terminy prawne z notatek lub plików.', 'Przetwórz i zapomnij'],
'redact' => ['Redakcja', 'Ochrona prywatności', 'Usuń imiona, numery identyfikacyjne, telefony i adresy przed udostępnieniem dokumentów.', 'Metoda deterministyczna'],
'summarize' => ['Streszczenie', 'Streszczenie dokumentu', 'Wyodrębniaj kluczowe fakty, daty, strony i odniesienia prawne — z opcjonalnym wzbogaceniem korpusem.', 'Przetwórz i zapomnij'],
+ 'legal-analysis' => ['Analiza prawna', 'Głęboka analiza prawa norweskiego', 'Wyodrębnij odrębne kwestie prawne z dokumentu i uzyskaj odpowiedzi od modelu dbn-legal-agent-v3 — z cytatami z barnevernsloven, EKPC i norweskiego Sądu Najwyższego.', 'Fine-tune · Norsk'],
'korrespond' => ['Korrespond', 'Pisma i odpowiedzi do urzędów', 'Twórz projekty odpowiedzi lub nowych pism do NAV, Barnevernet, szkoły, Bufdir i innych norweskich organów — norweski + Twój język obok siebie, ze zweryfikowanymi odniesieniami do ustaw.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['Analizator BVJ', 'Dokumenty Barnevernet', 'Analizuj dokumenty opieki nad dziećmi z Twojej perspektywy, z ryzykami proceduralnymi i źródłami.', 'Dokument + RAG'],
'advocate' => ['Adwokat', 'Stronniczy brief', 'Wybierz, kogo reprezentujesz, i wygeneruj brief oparty na źródłach dla tej pozycji.', 'ETPC + Lovdata'],
@@ -1561,19 +1565,20 @@ function dbnToolsLaunchedTools(?string $language = null): array
];
$selected = $copy[$language] ?? $copy['en'];
- $order = ['transcribe', 'timeline', 'redact', 'summarize', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations'];
+ $order = ['transcribe', 'timeline', 'redact', 'summarize', 'legal-analysis', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations'];
$icons = [
- 'transcribe' => 'TR',
- 'timeline' => 'TL',
- 'redact' => 'RX',
- 'summarize' => 'SZ',
- 'korrespond' => 'KOR',
- 'barnevernet' => 'BVJ',
- 'advocate' => 'ADV',
- 'deep-research' => 'DR',
- 'discrepancy' => 'DC',
- 'corpus' => 'KB',
- 'citations' => 'CIT',
+ 'transcribe' => 'TR',
+ 'timeline' => 'TL',
+ 'redact' => 'RX',
+ 'summarize' => 'SZ',
+ 'legal-analysis' => 'LA',
+ 'korrespond' => 'KOR',
+ 'barnevernet' => 'BVJ',
+ 'advocate' => 'ADV',
+ 'deep-research' => 'DR',
+ 'discrepancy' => 'DC',
+ 'corpus' => 'KB',
+ 'citations' => 'CIT',
];
$out = [];
foreach ($order as $slug) {
diff --git a/legal-analysis.php b/legal-analysis.php
new file mode 100644
index 0000000..5fb6bf0
--- /dev/null
+++ b/legal-analysis.php
@@ -0,0 +1,104 @@
+
+
+
+
+
+
Ready
+
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.
+
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.