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
+33 -21
View File
@@ -39,8 +39,9 @@ final class DbnLegalToolsService
'shared' => 'Legal Library only',
default => 'Legal Library + personal corpus',
};
$product = dbnToolsProductName();
$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'),
];
@@ -252,12 +253,9 @@ Return JSON only with these keys:
}
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).
$system = $this->legalJsonSystemPrompt($language);
if (!empty($personaResolved['system_prompt'])) {
$system = $personaResolved['system_prompt'] . "\n\n" . $system;
}
$system = $this->legalJsonSystemPrompt($language, $personaResolved['system_prompt'] ?? null);
$askDeployment = $personaModel;
$raw = $gateway->withDeployment($askDeployment)->chatText([
['role' => 'system', 'content' => $system],
@@ -1166,7 +1164,7 @@ PROMPT;
dbnToolsAbort('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 family-legal subscription.', 503, 'subscription_missing');
dbnToolsAbort(dbnToolsProductName() . ' does not have an active family-legal subscription.', 503, 'subscription_missing');
}
return $package;
}
@@ -1191,30 +1189,44 @@ PROMPT;
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([
['role' => 'system', 'content' => $this->legalJsonSystemPrompt($language)],
// With a persona, route to its pinned engine (Track-1 → tuned Qwen, Track-2 → gpt-4o)
// 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],
], [
'json' => true,
'temperature' => 0.1,
'max_tokens' => $maxTokens,
]);
$json = $this->azure->decodeJsonObject($raw);
$json = $gateway->decodeJsonObject($raw);
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;
}
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
You are Do Better Norge Legal Tools — a source-grounded Norwegian legal preparation assistant.
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).
Use the DBN legal guardrails:
You are {$product} Tools — a source-grounded Norwegian legal preparation assistant covering all areas of Norwegian law.
{$personaBlock}Legal guardrails:
- Answer only from provided source excerpts or pasted text.
- 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.
@@ -1260,7 +1272,7 @@ PROMPT;
'title' => $title,
'excerpt' => $docSummary ?? $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,
'document_id' => isset($chunk['document_id']) ? (int)$chunk['document_id'] : null,
'chunk_id' => isset($chunk['id']) ? (int)$chunk['id'] : null,
@@ -1354,7 +1366,7 @@ PROMPT;
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$row) {
$row['similarity'] = 0.25;
$row['source_name'] = 'Do Better Norge private corpus';
$row['source_name'] = dbnToolsProductName() . ' private corpus';
$row['source_type'] = 'private';
}
return $rows;
@@ -1819,7 +1831,7 @@ PROMPT;
$enriched = $text;
$corpusUsed = $corpusContext !== '';
if ($corpusUsed) {
$enriched = "[Relevant legal context from Do Better Norge corpus]\n"
$enriched = '[Relevant legal context from ' . dbnToolsProductName() . " corpus]\n"
. $corpusContext
. "\n\n---\n\nDocument to summarise:\n"
. $text;
@@ -1874,7 +1886,7 @@ PROMPT;
}
$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.';
$trace = [