feat(tools): reposition as Do Better Legal two-track Norwegian-law MCP

De-family-ify shared JSON tools (persona-aware routing + neutral base
prompt), make the verification review pick its engine per track
(family/child-welfare -> dbn-legal-agent-v3, others -> gpt-4o interim),
and route product-name strings through dbnToolsProductName(). Rebrand the
MCP/tools surface (mcp.php + i18n mcp_* strings) to Do Better Legal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 07:45:17 +02:00
parent d156f8cf6b
commit 7fcd317205
12 changed files with 158 additions and 72 deletions
+5 -1
View File
@@ -75,8 +75,12 @@ try {
'language' => $language, 'language' => $language,
]); ]);
$personaSlug = (isset($input['profile']) && is_string($input['profile']) && trim($input['profile']) !== '')
? trim($input['profile'])
: null;
$agent = new DbnKorrespondAgent(); $agent = new DbnKorrespondAgent();
$result = $agent->refine($intake, $classify, $originalDraftNo, $jurisdiction, $emit); $result = $agent->refine($intake, $classify, $originalDraftNo, $jurisdiction, $emit, $personaSlug);
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond_refine'); $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond_refine');
$result['ok'] = true; $result['ok'] = true;
$result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000);
+4 -2
View File
@@ -64,7 +64,8 @@ try {
$personaResolved = dbnToolsResolvePersona((int)$client['id'], $personaSlug); $personaResolved = dbnToolsResolvePersona((int)$client['id'], $personaSlug);
$agent = (new DbnLegalAnalysisAgent()) $agent = (new DbnLegalAnalysisAgent())
->withPersonaPrompt($personaResolved['system_prompt'] ?? null); ->withPersonaPrompt($personaResolved['system_prompt'] ?? null)
->withPersonaSlug($personaResolved['slug'] ?? $personaSlug);
// Pass 1 — extract issues (Azure, fast); deduct credit AFTER this succeeds // Pass 1 — extract issues (Azure, fast); deduct credit AFTER this succeeds
$emit('progress', ['step' => 'extracting_issues', 'detail' => 'Identifying distinct legal issues…']); $emit('progress', ['step' => 'extracting_issues', 'detail' => 'Identifying distinct legal issues…']);
@@ -132,7 +133,8 @@ try {
$legalCheck = dbnToolsRunLegalCheck( $legalCheck = dbnToolsRunLegalCheck(
mb_strimwidth((string)($synth['overall_assessment'] ?? ''), 0, 800), mb_strimwidth((string)($synth['overall_assessment'] ?? ''), 0, 800),
$docType, $docType,
$personaResolved['system_prompt'] ?? null $personaResolved['system_prompt'] ?? null,
$personaResolved['slug'] ?? $personaSlug
); );
} catch (Throwable) {} } catch (Throwable) {}
+5 -3
View File
@@ -847,9 +847,10 @@ PROMPT;
: ''; : '';
$docExcerpt = mb_substr($docText, 0, 8000, 'UTF-8'); $docExcerpt = mb_substr($docText, 0, 8000, 'UTF-8');
$product = dbnToolsProductName();
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are Do Better Norge Legal Tools. Produce a structured Barnevernet case analysis for: {$roleStr}. You are {$product} Tools. Produce a structured Barnevernet case analysis for: {$roleStr}.
HALLUCINATION RULES — READ FIRST: HALLUCINATION RULES — READ FIRST:
- You may ONLY cite statute sections (§), ECHR article numbers, ECHR application numbers, case names, and Bufdir/Statsforvalter circular references that appear verbatim in the numbered corpus sources below. - You may ONLY cite statute sections (§), ECHR article numbers, ECHR application numbers, case names, and Bufdir/Statsforvalter circular references that appear verbatim in the numbered corpus sources below.
@@ -984,7 +985,8 @@ PROMPT;
$checkFindings = dbnToolsRunLegalCheck( $checkFindings = dbnToolsRunLegalCheck(
(string)($json['advocacy_brief'] ?? ''), (string)($json['advocacy_brief'] ?? ''),
$docType, $docType,
$this->resolvedPersonaPrompt $this->resolvedPersonaPrompt,
$this->personaSlug
); );
if (!empty($checkFindings)) { if (!empty($checkFindings)) {
if (!is_array($json['procedural_red_flags'] ?? null)) { if (!is_array($json['procedural_red_flags'] ?? null)) {
@@ -1379,7 +1381,7 @@ PROMPT;
dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable'); dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable');
} }
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) { if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
dbnToolsAbort('Do Better Norge does not have an active family-legal subscription.', 503, 'subscription_missing'); dbnToolsAbort(dbnToolsProductName() . ' does not have an active family-legal subscription.', 503, 'subscription_missing');
} }
return $package; return $package;
} }
+1 -1
View File
@@ -56,7 +56,7 @@ final class DbnMcpRuntime
'mode' => ['type' => 'string', 'enum' => ['standard', 'strict']], 'mode' => ['type' => 'string', 'enum' => ['standard', 'strict']],
'output_format' => ['type' => 'string', 'enum' => ['contextual', 'generic', 'pseudonym']], 'output_format' => ['type' => 'string', 'enum' => ['contextual', 'generic', 'pseudonym']],
], ['text']), ], ['text']),
self::tool('dbn.translate', 'Translate legal document', 'Translate Norwegian family-law text with legal terminology annotations.', [ self::tool('dbn.translate', 'Translate legal document', 'Translate Norwegian legal text with legal terminology annotations.', [
'text' => $text, 'text' => $text,
'source_lang' => $lang, 'source_lang' => $lang,
'target_lang' => $lang, 'target_lang' => $lang,
+8 -5
View File
@@ -440,7 +440,7 @@ final class DbnDeepResearchAgent
dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable'); dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable');
} }
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) { if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
dbnToolsAbort('Do Better Norge does not have an active family-legal subscription.', 503, 'subscription_missing'); dbnToolsAbort(dbnToolsProductName() . ' does not have an active family-legal subscription.', 503, 'subscription_missing');
} }
return $package; return $package;
} }
@@ -487,8 +487,9 @@ final class DbnDeepResearchAgent
$priorContextBlock = implode("\n", $parts) . "\n\nNow investigate this branch:\n"; $priorContextBlock = implode("\n", $parts) . "\n\nNow investigate this branch:\n";
} }
$product = dbnToolsProductName();
$prompt = <<<PROMPT $prompt = <<<PROMPT
{$rolePrefix}{$priorContextBlock}You are reviewing the input below to set up a deep legal research pass against the Do Better Norge family-law corpus. {$rolePrefix}{$priorContextBlock}You are reviewing the input below to set up a deep legal research pass against the {$product} Norwegian legal corpus.
Input: Input:
{$seedDescription} {$seedDescription}
@@ -580,8 +581,9 @@ Rules:
- Write the questions in {$locale}. - Write the questions in {$locale}.
PROMPT; PROMPT;
} else { } else {
$product = dbnToolsProductName();
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are decomposing a Do Better Norge legal-research request into {$targetCount} focused sub-questions that should each be answered by the legal corpus (Norwegian family law, child welfare, ECHR/Hague). You are decomposing a {$product} legal-research request into {$targetCount} focused sub-questions that should each be answered by the legal corpus (Norwegian law — e.g. family, child welfare, immigration, labour, consumer/tenancy, ECHR/Hague).
Research brief: Research brief:
{$brief} {$brief}
@@ -1064,9 +1066,10 @@ PROMPT;
? "\nKey retrieval signals (statutory/factual terms that drove corpus search — ground your brief in these where sources permit):\n" . implode(', ', $keySignals) . "\n" ? "\nKey retrieval signals (statutory/factual terms that drove corpus search — ground your brief in these where sources permit):\n" . implode(', ', $keySignals) . "\n"
: ''; : '';
$product = dbnToolsProductName();
if ($advocateRole !== '') { if ($advocateRole !== '') {
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are Do Better Norge Legal Tools producing a legal preparation brief in {$locale}. You are {$product} Tools producing a legal preparation brief in {$locale}.
Your client: {$advocateRole} Your client: {$advocateRole}
{$priorContextSection} {$priorContextSection}
User input: User input:
@@ -1106,7 +1109,7 @@ Return JSON:
PROMPT; PROMPT;
} else { } else {
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are Do Better Norge Legal Tools running a deep-research synthesis. You MUST ground every claim in the numbered sources below, using inline `[n]` citation markers that map to the source list. Do NOT cite a source you did not use. Do NOT invent statutes, paragraph numbers, case names, dates, or parties. You are {$product} Tools running a deep-research synthesis. You MUST ground every claim in the numbered sources below, using inline `[n]` citation markers that map to the source list. Do NOT cite a source you did not use. Do NOT invent statutes, paragraph numbers, case names, dates, or parties.
{$priorContextSection} {$priorContextSection}
User input: User input:
{$seedDescription} {$seedDescription}
+3 -2
View File
@@ -803,9 +803,10 @@ PROMPT;
$docTypeB = $metaB['doc_type'] ?? $nameB; $docTypeB = $metaB['doc_type'] ?? $nameB;
$docDateB = $metaB['doc_date'] ?? '?'; $docDateB = $metaB['doc_date'] ?? '?';
$authority = $metaA['issuing_authority'] ?? $metaB['issuing_authority'] ?? 'the authority'; $authority = $metaA['issuing_authority'] ?? $metaB['issuing_authority'] ?? 'the authority';
$product = dbnToolsProductName();
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are Do Better Norge Legal Tools evaluating discrepancies between two Barnevernet document versions. You are {$product} Tools evaluating discrepancies between two Barnevernet document versions.
HALLUCINATION RULES: HALLUCINATION RULES:
- Only cite statute sections (§), ECHR articles, and case names that appear verbatim in the corpus sources below. - Only cite statute sections (§), ECHR articles, and case names that appear verbatim in the corpus sources below.
@@ -1040,7 +1041,7 @@ PROMPT;
dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable'); dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable');
} }
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) { if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
dbnToolsAbort('Do Better Norge does not have an active family-legal subscription.', 503, 'subscription_missing'); dbnToolsAbort(dbnToolsProductName() . ' does not have an active family-legal subscription.', 503, 'subscription_missing');
} }
return $package; return $package;
} }
+4 -3
View File
@@ -282,7 +282,7 @@ PROMPT;
if ($emit) { $emit('progress', ['detail' => self::L('legal_check', $userLang)]); } if ($emit) { $emit('progress', ['detail' => self::L('legal_check', $userLang)]); }
$legalCheck = []; $legalCheck = [];
try { try {
$legalCheck = dbnToolsRunLegalCheck($checked['draft'], $body); $legalCheck = dbnToolsRunLegalCheck($checked['draft'], $body, null, $persona);
} catch (Throwable $e) { /* silent — non-critical */ } } catch (Throwable $e) { /* silent — non-critical */ }
// ── Translate to user language (if not Norwegian) ─────────────────────── // ── Translate to user language (if not Norwegian) ───────────────────────
@@ -777,7 +777,8 @@ EOT,
array $classify, array $classify,
string $originalDraftNo, string $originalDraftNo,
string $jurisdiction, string $jurisdiction,
?callable $emit = null ?callable $emit = null,
?string $persona = null
): array { ): array {
$body = $intake['recipient_body'] ?? 'other'; $body = $intake['recipient_body'] ?? 'other';
$outputType = $intake['output_type'] ?? 'email'; $outputType = $intake['output_type'] ?? 'email';
@@ -808,7 +809,7 @@ EOT,
if ($emit) { $emit('progress', ['detail' => self::L('legal_check', $userLang)]); } if ($emit) { $emit('progress', ['detail' => self::L('legal_check', $userLang)]); }
$legalCheckRefine = []; $legalCheckRefine = [];
try { try {
$legalCheckRefine = dbnToolsRunLegalCheck($checked['draft'], $body); $legalCheckRefine = dbnToolsRunLegalCheck($checked['draft'], $body, null, $persona);
} catch (Throwable $e) { /* silent — non-critical */ } } catch (Throwable $e) { /* silent — non-critical */ }
$draftUser = $checked['draft']; $draftUser = $checked['draft'];
+19 -5
View File
@@ -28,6 +28,8 @@ final class DbnLegalAnalysisAgent
/** Persona system prompt (from the resolved chat profile); null ⇒ use the built-in barnevern prompt. */ /** Persona system prompt (from the resolved chat profile); null ⇒ use the built-in barnevern prompt. */
private ?string $personaPrompt = null; private ?string $personaPrompt = null;
/** Persona slug driving the per-track reviewer model; default = child-welfare (family track). */
private string $personaSlug = 'child-welfare';
public function withPersonaPrompt(?string $prompt): self public function withPersonaPrompt(?string $prompt): self
{ {
@@ -36,6 +38,15 @@ final class DbnLegalAnalysisAgent
return $this; return $this;
} }
public function withPersonaSlug(?string $slug): self
{
$slug = is_string($slug) ? trim($slug) : '';
if ($slug !== '') {
$this->personaSlug = $slug;
}
return $this;
}
public function __construct() public function __construct()
{ {
// On Azure: gpt-4o-mini for extraction/synthesis. On Bedrock: factory picks Haiku/Sonnet. // On Azure: gpt-4o-mini for extraction/synthesis. On Bedrock: factory picks Haiku/Sonnet.
@@ -408,11 +419,12 @@ PROMPT;
$userMsg .= "\n\n" . $ctxLabel . ': ' . $issue['brief_context']; $userMsg .= "\n\n" . $ctxLabel . ': ' . $issue['brief_context'];
} }
if ($corpusContext !== '') { if ($corpusContext !== '') {
$product = dbnToolsProductName();
$srcLabel = match ($language) { $srcLabel = match ($language) {
'no' => 'Relevante kilder fra Do Better Norge-korpuset', 'no' => 'Relevante kilder fra ' . $product . '-korpuset',
'pl' => 'Istotne źródła z korpusu Do Better Norge', 'pl' => 'Istotne źródła z korpusu ' . $product,
'uk' => 'Релевантні джерела з корпусу Do Better Norge', 'uk' => 'Релевантні джерела з корпусу ' . $product,
default => 'Relevant sources from the Do Better Norge corpus', default => 'Relevant sources from the ' . $product . ' corpus',
}; };
$userMsg .= "\n\n" . $srcLabel . ":\n" . $corpusContext; $userMsg .= "\n\n" . $srcLabel . ":\n" . $corpusContext;
} }
@@ -613,7 +625,9 @@ PROMPT;
try { try {
$legalCheck = dbnToolsRunLegalCheck( $legalCheck = dbnToolsRunLegalCheck(
mb_strimwidth($synth['overall_assessment'], 0, 800), mb_strimwidth($synth['overall_assessment'], 0, 800),
$docType $docType,
$this->personaPrompt,
$this->personaSlug
); );
} catch (Throwable) {} } catch (Throwable) {}
+33 -21
View File
@@ -39,8 +39,9 @@ final class DbnLegalToolsService
'shared' => 'Legal Library only', 'shared' => 'Legal Library only',
default => 'Legal Library + personal corpus', default => 'Legal Library + personal corpus',
}; };
$product = dbnToolsProductName();
$trace = [ $trace = [
$this->trace('Query interpretation', "Searching Do Better Norge {$scopeLabel}.", 'complete'), $this->trace('Query interpretation', "Searching {$product} {$scopeLabel}.", 'complete'),
$this->trace('Search tools used', 'ClientRagPipeline::searchAll with keyword mode.', 'running'), $this->trace('Search tools used', 'ClientRagPipeline::searchAll with keyword mode.', 'running'),
]; ];
@@ -252,12 +253,9 @@ Return JSON only with these keys:
} }
PROMPT; PROMPT;
// Persona voice/domain prepended to the JSON-enforcing scaffold (keeps the // Persona voice/domain folded into the JSON-enforcing scaffold (keeps the
// structured-output contract while applying the persona's legal framing). // structured-output contract while applying the persona's legal framing).
$system = $this->legalJsonSystemPrompt($language); $system = $this->legalJsonSystemPrompt($language, $personaResolved['system_prompt'] ?? null);
if (!empty($personaResolved['system_prompt'])) {
$system = $personaResolved['system_prompt'] . "\n\n" . $system;
}
$askDeployment = $personaModel; $askDeployment = $personaModel;
$raw = $gateway->withDeployment($askDeployment)->chatText([ $raw = $gateway->withDeployment($askDeployment)->chatText([
['role' => 'system', 'content' => $system], ['role' => 'system', 'content' => $system],
@@ -1166,7 +1164,7 @@ PROMPT;
dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable'); dbnToolsAbort('The family-legal corpus package is not active.', 503, 'package_unavailable');
} }
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) { if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
dbnToolsAbort('Do Better Norge does not have an active family-legal subscription.', 503, 'subscription_missing'); dbnToolsAbort(dbnToolsProductName() . ' does not have an active family-legal subscription.', 503, 'subscription_missing');
} }
return $package; return $package;
} }
@@ -1191,30 +1189,44 @@ PROMPT;
return [$this->azure, ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini']; return [$this->azure, ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini'];
} }
private function runJsonTool(string $prompt, string $language, int $maxTokens): array private function runJsonTool(string $prompt, string $language, int $maxTokens, ?array $persona = null): array
{ {
$raw = $this->azure->chatText([ // With a persona, route to its pinned engine (Track-1 → tuned Qwen, Track-2 → gpt-4o)
['role' => 'system', 'content' => $this->legalJsonSystemPrompt($language)], // and fold its domain framing into the system prompt. Without one (e.g. pasted-text
// tools), keep the default Azure routing with the neutral base prompt.
$personaPrompt = $persona['system_prompt'] ?? null;
if ($persona !== null) {
[$gateway, $model] = $this->personaGateway($persona, 'azure_mini');
$gateway = $gateway->withDeployment($model);
} else {
$gateway = $this->azure;
}
$raw = $gateway->chatText([
['role' => 'system', 'content' => $this->legalJsonSystemPrompt($language, $personaPrompt)],
['role' => 'user', 'content' => $prompt], ['role' => 'user', 'content' => $prompt],
], [ ], [
'json' => true, 'json' => true,
'temperature' => 0.1, 'temperature' => 0.1,
'max_tokens' => $maxTokens, 'max_tokens' => $maxTokens,
]); ]);
$json = $this->azure->decodeJsonObject($raw); $json = $gateway->decodeJsonObject($raw);
if (!$json) { if (!$json) {
dbnToolsAbort('Azure OpenAI did not return valid structured JSON.', 502, 'azure_invalid_json'); dbnToolsAbort('The model did not return valid structured JSON.', 502, 'invalid_json');
} }
return $json; return $json;
} }
private function legalJsonSystemPrompt(string $language): string private function legalJsonSystemPrompt(string $language, ?string $personaPrompt = null): string
{ {
$locale = dbnToolsLanguageName($language); $locale = dbnToolsLanguageName($language);
$product = dbnToolsProductName();
$personaPrompt = is_string($personaPrompt) ? trim($personaPrompt) : '';
// The persona (family, immigration, labour, …) supplies the domain framing; the
// base prompt stays domain-neutral so non-family tracks are not cast as child-welfare.
$personaBlock = $personaPrompt !== '' ? ($personaPrompt . "\n") : '';
return <<<PROMPT return <<<PROMPT
You are Do Better Norge Legal Tools — a source-grounded Norwegian legal preparation assistant. You are {$product} Tools — a source-grounded Norwegian legal preparation assistant covering all areas of Norwegian law.
Norwegian legal context: CPS cases follow the chain Barnevernstjenesten → Fylkesnemnda → Statsforvalteren → Tingrett → Lagmannsrett → Høyesterett. Key order types: akuttvedtak (emergency removal), omsorgsvedtak (care order), samværsvedtak (contact order). Relevant bodies: BUP (child psychiatry), PPT (educational psychology), NAV (welfare). {$personaBlock}Legal guardrails:
Use the DBN legal guardrails:
- Answer only from provided source excerpts or pasted text. - Answer only from provided source excerpts or pasted text.
- Treat your role as legal information and issue-spotting, not final legal advice. - Treat your role as legal information and issue-spotting, not final legal advice.
- Never invent statutes, paragraph numbers, case names, citations, parties, dates, or sources. - Never invent statutes, paragraph numbers, case names, citations, parties, dates, or sources.
@@ -1260,7 +1272,7 @@ PROMPT;
'title' => $title, 'title' => $title,
'excerpt' => $docSummary ?? $rawExcerpt, 'excerpt' => $docSummary ?? $rawExcerpt,
'chunk_text' => $rawExcerpt, 'chunk_text' => $rawExcerpt,
'package_or_corpus' => (string)($chunk['source_name'] ?? $chunk['source_type'] ?? 'Do Better Norge'), 'package_or_corpus' => (string)($chunk['source_name'] ?? $chunk['source_type'] ?? dbnToolsProductName()),
'score' => $score, 'score' => $score,
'document_id' => isset($chunk['document_id']) ? (int)$chunk['document_id'] : null, 'document_id' => isset($chunk['document_id']) ? (int)$chunk['document_id'] : null,
'chunk_id' => isset($chunk['id']) ? (int)$chunk['id'] : null, 'chunk_id' => isset($chunk['id']) ? (int)$chunk['id'] : null,
@@ -1354,7 +1366,7 @@ PROMPT;
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$row) { foreach ($rows as &$row) {
$row['similarity'] = 0.25; $row['similarity'] = 0.25;
$row['source_name'] = 'Do Better Norge private corpus'; $row['source_name'] = dbnToolsProductName() . ' private corpus';
$row['source_type'] = 'private'; $row['source_type'] = 'private';
} }
return $rows; return $rows;
@@ -1819,7 +1831,7 @@ PROMPT;
$enriched = $text; $enriched = $text;
$corpusUsed = $corpusContext !== ''; $corpusUsed = $corpusContext !== '';
if ($corpusUsed) { if ($corpusUsed) {
$enriched = "[Relevant legal context from Do Better Norge corpus]\n" $enriched = '[Relevant legal context from ' . dbnToolsProductName() . " corpus]\n"
. $corpusContext . $corpusContext
. "\n\n---\n\nDocument to summarise:\n" . "\n\n---\n\nDocument to summarise:\n"
. $text; . $text;
@@ -1874,7 +1886,7 @@ PROMPT;
} }
$corpusNote = $corpusUsed $corpusNote = $corpusUsed
? 'Summary enriched with ' . count(array_filter(explode('=== ', $corpusContext))) . ' passage(s) from the Do Better Norge legal corpus.' ? 'Summary enriched with ' . count(array_filter(explode('=== ', $corpusContext))) . ' passage(s) from the ' . dbnToolsProductName() . ' legal corpus.'
: 'No corpus search performed; summarised from document text only.'; : 'No corpus search performed; summarised from document text only.';
$trace = [ $trace = [
+59 -12
View File
@@ -479,6 +479,39 @@ function dbnToolsDefaultPersonaSlug(): string
return $v !== '' ? $v : 'family'; return $v !== '' ? $v : 'family';
} }
/** Product display name for tool-facing copy. Override via .env DBN_PRODUCT_NAME. */
function dbnToolsProductName(): string
{
$v = trim((string)(dbnToolsEnv('DBN_PRODUCT_NAME', 'Do Better Legal') ?? 'Do Better Legal'));
return $v !== '' ? $v : 'Do Better Legal';
}
/**
* Engine track for a persona slug:
* 'family' family + child-welfare, served/verified by the fine-tuned Qwen.
* 'general' all other Norwegian-law personas, served/verified by the interim engine.
*/
function dbnToolsPersonaTrack(?string $slug): string
{
$slug = strtolower(trim((string)$slug));
return in_array($slug, ['family', 'child-welfare'], true) ? 'family' : 'general';
}
/**
* Reviewer/verification model for a persona's track.
* Family track fine-tuned Qwen (dbn-legal-agent-v3).
* General track interim gpt-4o; swap to the 2nd fine-tune later via .env
* DBN_REVIEW_MODEL_GENERAL with no code change.
*/
function dbnToolsReviewerModel(?string $slug = null): string
{
if (dbnToolsPersonaTrack($slug) === 'family') {
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_FAMILY', 'dbn-legal-agent-v3') ?? 'dbn-legal-agent-v3'))
?: 'dbn-legal-agent-v3';
}
return trim((string)(dbnToolsEnv('DBN_REVIEW_MODEL_GENERAL', 'gpt-4o') ?? 'gpt-4o')) ?: 'gpt-4o';
}
/** /**
* Resolve a DBN persona (a caveauAI chat profile for client 57) into a normalized * Resolve a DBN persona (a caveauAI chat profile for client 57) into a normalized
* runtime bundle the tools can act on: * runtime bundle the tools can act on:
@@ -544,7 +577,7 @@ function dbnToolsResolvePersona(int $clientId, ?string $slug = null): array
dbnToolsAbort('No persona profile and the family-legal corpus package is not active.', 503, 'package_unavailable'); dbnToolsAbort('No persona profile and the family-legal corpus package is not active.', 503, 'package_unavailable');
} }
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) { if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
dbnToolsAbort('Do Better Norge does not have an active corpus subscription.', 503, 'subscription_missing'); dbnToolsAbort(dbnToolsProductName() . ' does not have an active corpus subscription.', 503, 'subscription_missing');
} }
$resolved = [ $resolved = [
'slug' => 'family', 'slug' => 'family',
@@ -938,7 +971,7 @@ function dbnToolsRequireClient(): array
{ {
$client = dbnToolsFetchClient(); $client = dbnToolsFetchClient();
if (!$client || empty($client['is_active'])) { if (!$client || empty($client['is_active'])) {
dbnToolsAbort('Do Better Norge client tenant is not active or was not found.', 503, 'client_unavailable'); dbnToolsAbort(dbnToolsProductName() . ' client tenant is not active or was not found.', 503, 'client_unavailable');
} }
return $client; return $client;
} }
@@ -1304,26 +1337,33 @@ function dbnToolsLiteLLMEmbedBatch(array $texts, string $model = 'nomic-embed-te
// Strategy: ask ONE targeted question per document type, cap tokens at 350 to cut // 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. // off before the tool-planning loop kicks in, parse the answer for legal findings.
function dbnToolsRunLegalCheck(string $brief, string $docType, ?string $personaPrompt = null): array function dbnToolsRunLegalCheck(string $brief, string $docType, ?string $personaPrompt = null, ?string $personaSlug = null): array
{ {
$question = dbnToolsSelectCheckQuestion($docType, $brief); $track = dbnToolsPersonaTrack($personaSlug);
$model = dbnToolsReviewerModel($personaSlug);
$question = dbnToolsSelectCheckQuestion($docType, $brief, $track);
if ($question === null) { if ($question === null) {
return []; return [];
} }
$opts = [ $opts = [
'model' => 'dbn-legal-agent-v3', 'model' => $model,
'temperature' => 0.1, 'temperature' => 0.1,
'max_tokens' => 350, 'max_tokens' => 350,
'timeout' => 45, 'timeout' => 45,
// No 'json' key — plain narrative, no response_format flag // No 'json' key — plain narrative, no response_format flag
]; ];
// Base prompt from the resolved persona (default = Child-welfare/barnevern). // Base prompt: resolved persona prompt wins; otherwise a track-appropriate default.
$personaPrompt = is_string($personaPrompt) ? trim($personaPrompt) : ''; $personaPrompt = is_string($personaPrompt) ? trim($personaPrompt) : '';
$sysMsg = $personaPrompt !== '' if ($personaPrompt !== '') {
? $personaPrompt $sysMsg = $personaPrompt;
: '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.'; } elseif ($track === 'family') {
$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.';
} else {
$sysMsg = 'Du er en erfaren norsk jurist med bred kompetanse i norsk rett (forvaltningsrett, utlendingsrett, arbeidsrett, husleie- og forbrukerrett). Svar alltid på norsk med korrekt juridisk terminologi. Aldri oppfinn paragrafnumre, saksnumre, dommernavn eller kilder. Hvis du er usikker, si det tydelig.';
}
// Prepend a short snippet of the actual synthesis text so v3 answers in context, // Prepend a short snippet of the actual synthesis text so v3 answers in context,
// not just as a general law quiz. Strip HTML tags and cap at 350 chars. // not just as a general law quiz. Strip HTML tags and cap at 350 chars.
@@ -1360,16 +1400,23 @@ function dbnToolsRunLegalCheck(string $brief, string $docType, ?string $personaP
'severity' => dbnToolsInferCheckSeverity($clean), 'severity' => dbnToolsInferCheckSeverity($clean),
'legal_basis' => dbnToolsExtractCheckLegalBasis($clean), 'legal_basis' => dbnToolsExtractCheckLegalBasis($clean),
'source_refs' => [], 'source_refs' => [],
'what_to_check'=> 'Verifiser med norsk familieretsadvokat', 'what_to_check'=> $track === 'family' ? 'Verifiser med norsk familieretsadvokat' : 'Verifiser med norsk advokat',
'check_model' => 'dbn-legal-agent-v3', 'check_model' => $model,
]]; ]];
} }
function dbnToolsSelectCheckQuestion(string $docType, string $brief): ?string function dbnToolsSelectCheckQuestion(string $docType, string $brief, string $track = 'family'): ?string
{ {
$t = mb_strtolower($docType); $t = mb_strtolower($docType);
$b = mb_strtolower($brief); $b = mb_strtolower($brief);
// Track 2 (other Norwegian law): neutral verification question, always fires.
if ($track !== 'family') {
return 'Vurder teksten ovenfor opp mot gjeldende norsk rett: er de rettslige påstandene korrekte, '
. 'er det vist til riktige lover og paragrafer, og mangler det viktige rettslige vilkår, frister '
. 'eller prosessuelle krav som burde vært nevnt?';
}
if (str_contains($t, 'akutt') || str_contains($t, 'emergency')) { if (str_contains($t, 'akutt') || str_contains($t, 'emergency')) {
return 'Hva er den korrekte rettslige terskelen for midlertidig plassering utenfor hjemmet ' 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) ' . 'etter barnevernsloven § 4-25 (2021-loven)? Er det forskjell mellom § 4-6 (1992-loven) '
+16 -16
View File
@@ -470,11 +470,11 @@ function dbnToolsTranslations(): array
'doc_picker_modal_title' => 'Select from My Docs', 'doc_picker_modal_title' => 'Select from My Docs',
// MCP setup page + tool detail pages // MCP setup page + tool detail pages
'mcp_page_title' => 'MCP — Do Better Norge', 'mcp_page_title' => 'MCP — Do Better Legal',
'mcp_meta_desc' => 'Connect Claude, Cursor, and other AI tools to all 19 DBN legal preparation tools via MCP.', 'mcp_meta_desc' => 'Connect Claude, Cursor, and other AI tools to all Do Better Legal Norwegian-law preparation tools via MCP.',
'mcp_hero_badge' => '✦ Plus & Pro', 'mcp_hero_badge' => '✦ Plus & Pro',
'mcp_hero_h1' => 'Use DBN tools from Claude, Cursor & Copilot', 'mcp_hero_h1' => 'Use Do Better Legal tools from Claude, Cursor & Copilot',
'mcp_hero_sub' => 'Connect any MCP client to all 19 Do Better Norge tools — transcription, legal analysis, timelines, redaction, and more.', 'mcp_hero_sub' => 'Connect any MCP client to the full Do Better Legal toolset — transcription, legal analysis, timelines, redaction, and more.',
'mcp_token_section_title' => 'Your MCP token', 'mcp_token_section_title' => 'Your MCP token',
'mcp_gate_guest_p' => 'Sign in to create your personal MCP token. Available to Plus and Pro members.', 'mcp_gate_guest_p' => 'Sign in to create your personal MCP token. Available to Plus and Pro members.',
'mcp_gate_guest_btn' => 'Sign in', 'mcp_gate_guest_btn' => 'Sign in',
@@ -928,11 +928,11 @@ function dbnToolsTranslations(): array
'doc_picker_modal_title' => 'Velg fra Mine dokumenter', 'doc_picker_modal_title' => 'Velg fra Mine dokumenter',
// MCP setup page + tool detail pages // MCP setup page + tool detail pages
'mcp_page_title' => 'MCP — Do Better Norge', 'mcp_page_title' => 'MCP — Do Better Legal',
'mcp_meta_desc' => 'Koble Claude, Cursor og andre AI-verktøy til alle 19 DBN juridiske forberedelsesverktøy via MCP.', 'mcp_meta_desc' => 'Koble Claude, Cursor og andre AI-verktøy til alle Do Better Legal-verktøy for norsk juridisk forberedelse via MCP.',
'mcp_hero_badge' => '✦ Plus & Pro', 'mcp_hero_badge' => '✦ Plus & Pro',
'mcp_hero_h1' => 'Bruk DBN-verktøy fra Claude, Cursor & Copilot', 'mcp_hero_h1' => 'Bruk Do Better Legal-verktøy fra Claude, Cursor & Copilot',
'mcp_hero_sub' => 'Koble enhver MCP-klient til alle 19 Do Better Norge-verktøy — transkripsjon, juridisk analyse, tidslinjer, redigering og mer.', 'mcp_hero_sub' => 'Koble enhver MCP-klient til hele Do Better Legal-verktøysettet — transkripsjon, juridisk analyse, tidslinjer, redigering og mer.',
'mcp_token_section_title' => 'Din MCP-token', 'mcp_token_section_title' => 'Din MCP-token',
'mcp_gate_guest_p' => 'Logg inn for å opprette din personlige MCP-token. Tilgjengelig for Plus- og Pro-medlemmer.', 'mcp_gate_guest_p' => 'Logg inn for å opprette din personlige MCP-token. Tilgjengelig for Plus- og Pro-medlemmer.',
'mcp_gate_guest_btn' => 'Logg inn', 'mcp_gate_guest_btn' => 'Logg inn',
@@ -1386,11 +1386,11 @@ function dbnToolsTranslations(): array
'doc_picker_modal_title' => 'Вибрати з Моїх документів', 'doc_picker_modal_title' => 'Вибрати з Моїх документів',
// MCP setup page + tool detail pages // MCP setup page + tool detail pages
'mcp_page_title' => 'MCP — Do Better Norge', 'mcp_page_title' => 'MCP — Do Better Legal',
'mcp_meta_desc' => 'Підключіть Claude, Cursor та інші інструменти ШІ до всіх 19 юридичних підготовчих інструментів DBN через MCP.', 'mcp_meta_desc' => 'Підключіть Claude, Cursor та інші інструменти ШІ до всіх інструментів Do Better Legal для підготовки за нормами норвезького права через MCP.',
'mcp_hero_badge' => '✦ Плюс та Профі', 'mcp_hero_badge' => '✦ Плюс та Профі',
'mcp_hero_h1' => 'Використовуйте інструменти DBN від Claude, Cursor та Copilot', 'mcp_hero_h1' => 'Використовуйте інструменти Do Better Legal від Claude, Cursor та Copilot',
'mcp_hero_sub' => 'Підключіть будь-якого клієнта MCP до всіх 19 інструментів Do Better Norge — транскрипція, юридичний аналіз, часові лінії, редагування та інше.', 'mcp_hero_sub' => 'Підключіть будь-якого клієнта MCP до повного набору інструментів Do Better Legal — транскрипція, юридичний аналіз, часові лінії, редагування та інше.',
'mcp_token_section_title' => 'Ваш токен MCP', 'mcp_token_section_title' => 'Ваш токен MCP',
'mcp_gate_guest_p' => 'Увійдіть, щоб створити свій особистий токен MCP. Доступно для учасників Плюс та Профі.', 'mcp_gate_guest_p' => 'Увійдіть, щоб створити свій особистий токен MCP. Доступно для учасників Плюс та Профі.',
'mcp_gate_guest_btn' => 'Увійти', 'mcp_gate_guest_btn' => 'Увійти',
@@ -1844,11 +1844,11 @@ function dbnToolsTranslations(): array
'doc_picker_modal_title' => 'Wybierz z Moich dokumentów', 'doc_picker_modal_title' => 'Wybierz z Moich dokumentów',
// MCP setup page + tool detail pages // MCP setup page + tool detail pages
'mcp_page_title' => 'MCP — Do Better Norge', 'mcp_page_title' => 'MCP — Do Better Legal',
'mcp_meta_desc' => 'Połącz Claude, Cursor i inne narzędzia AI z wszystkimi 19 narzędziami przygotowania prawnego DBN za pośrednictwem MCP.', 'mcp_meta_desc' => 'Połącz Claude, Cursor i inne narzędzia AI z wszystkimi narzędziami Do Better Legal do przygotowania w zakresie prawa norweskiego za pośrednictwem MCP.',
'mcp_hero_badge' => '✦ Plus & Pro', 'mcp_hero_badge' => '✦ Plus & Pro',
'mcp_hero_h1' => 'Użyj narzędzi DBN z Claude, Cursor i Copilot', 'mcp_hero_h1' => 'Użyj narzędzi Do Better Legal z Claude, Cursor i Copilot',
'mcp_hero_sub' => 'Połącz dowolnego klienta MCP ze wszystkimi 19 narzędziami Do Better Norge — transkrypcja, analiza prawna, harmonogramy, redakcja i inne.', 'mcp_hero_sub' => 'Połącz dowolnego klienta MCP z pełnym zestawem narzędzi Do Better Legal — transkrypcja, analiza prawna, harmonogramy, redakcja i inne.',
'mcp_token_section_title' => 'Twój token MCP', 'mcp_token_section_title' => 'Twój token MCP',
'mcp_gate_guest_p' => 'Zaloguj się, aby utworzyć swój osobisty token MCP. Dostępny dla członków Plus i Pro.', 'mcp_gate_guest_p' => 'Zaloguj się, aby utworzyć swój osobisty token MCP. Dostępny dla członków Plus i Pro.',
'mcp_gate_guest_btn' => 'Zaloguj się', 'mcp_gate_guest_btn' => 'Zaloguj się',
+1 -1
View File
@@ -411,7 +411,7 @@ export DBN_MCP_TOKEN=dbn_user_mcp_...</code></pre>
{ {
"type": "promptString", "type": "promptString",
"id": "dbn-token", "id": "dbn-token",
"description": "Do Better Norge MCP token", "description": "Do Better Legal MCP token",
"password": true "password": true
} }
], ],