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:
+33
-21
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user