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
+59 -12
View File
@@ -479,6 +479,39 @@ function dbnToolsDefaultPersonaSlug(): string
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
* 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');
}
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 = [
'slug' => 'family',
@@ -938,7 +971,7 @@ function dbnToolsRequireClient(): array
{
$client = dbnToolsFetchClient();
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;
}
@@ -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
// 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) {
return [];
}
$opts = [
'model' => 'dbn-legal-agent-v3',
'model' => $model,
'temperature' => 0.1,
'max_tokens' => 350,
'timeout' => 45,
// 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) : '';
$sysMsg = $personaPrompt !== ''
? $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.';
if ($personaPrompt !== '') {
$sysMsg = $personaPrompt;
} 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,
// 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),
'legal_basis' => dbnToolsExtractCheckLegalBasis($clean),
'source_refs' => [],
'what_to_check'=> 'Verifiser med norsk familieretsadvokat',
'check_model' => 'dbn-legal-agent-v3',
'what_to_check'=> $track === 'family' ? 'Verifiser med norsk familieretsadvokat' : 'Verifiser med norsk advokat',
'check_model' => $model,
]];
}
function dbnToolsSelectCheckQuestion(string $docType, string $brief): ?string
function dbnToolsSelectCheckQuestion(string $docType, string $brief, string $track = 'family'): ?string
{
$t = mb_strtolower($docType);
$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')) {
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) '