Add Case Advocate tab — partisan brief grounded in Norwegian law

New /advocate.php tab: user selects who they represent (biological
father, mother, foster carer, CWS, etc.) and the agent takes their
side entirely. Adversarial sub-questions target supporting Lovdata
statutes + ECHR precedents; synthesis returns client_strengths[] and
opposing_weaknesses[] alongside the advocate brief.

- DeepResearchAgent: add advocateRole param to run(), interpretSeed(),
  expandQueries(), synthesise(). Neutral path unchanged (empty string).
- api/deep-research.php: extract + validate advocate_role from payload;
  telemetry logs tool='advocate' vs 'deep_research'.
- advocate.php: new page with role dropdown (presets + custom), same
  corpus slices/engine/controls/upload zone as deep research.
- assets/js/advocate.js: page-scoped JS; renders advocate banner,
  client strengths card (teal), advocate brief, opposing weaknesses
  card (amber), sub-Q cards, sources, uncertainty, next step.
- assets/css/tools.css: append .adv-* rules (~120 lines).
- includes/layout.php: add Advocate nav tab between Deep research and
  Summarize.
- index.php: add Advocate cap-card tile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 12:26:05 +02:00
parent 85a6bc8134
commit 640778454f
7 changed files with 1154 additions and 26 deletions
+91 -11
View File
@@ -30,7 +30,8 @@ final class DbnDeepResearchAgent
string $engine,
string $language,
array $controls,
?callable $emit = null
?callable $emit = null,
string $advocateRole = ''
): array {
$seedQuery = trim($seedQuery);
$pastedText = trim($pastedText);
@@ -81,14 +82,14 @@ final class DbnDeepResearchAgent
// STEP 1: Query interpretation
$emitRunning('interpretation', 'Query interpretation', 'Summarising the seed input…');
$stepStart = microtime(true);
$interpretation = $this->interpretSeed($seedDescription, $language);
$interpretation = $this->interpretSeed($seedDescription, $language, $advocateRole);
$this->stepTimings['interpretation'] = $this->elapsedMs($stepStart);
$emitStep('interpretation', 'Query interpretation', $interpretation['detail'], 'complete');
// STEP 2: Query expansion
$emitRunning('expansion', 'Query expansion', 'Generating sub-questions…');
$stepStart = microtime(true);
$expansion = $this->expandQueries($seedDescription, $interpretation['brief'], $controls['sub_q_count'], $language);
$expansion = $this->expandQueries($seedDescription, $interpretation['brief'], $controls['sub_q_count'], $language, $advocateRole);
$this->stepTimings['expansion'] = $this->elapsedMs($stepStart);
$subQuestions = $expansion['questions'];
$expansionStatus = $expansion['fallback'] ? 'warning' : 'complete';
@@ -290,7 +291,8 @@ final class DbnDeepResearchAgent
$numberedSources,
$engine,
$language,
$controls['temperature']
$controls['temperature'],
$advocateRole
);
$this->stepTimings['synthesis'] = $this->elapsedMs($stepStart);
$emitStep(
@@ -335,10 +337,14 @@ final class DbnDeepResearchAgent
];
}
$isAdvocate = $advocateRole !== '';
return [
'tool' => 'deep_research',
'tool' => $isAdvocate ? 'advocate' : 'deep_research',
'language' => $language,
'advocate_role' => $isAdvocate ? $advocateRole : null,
'brief_markdown' => (string)($synthesis['json']['brief_markdown'] ?? $synthesis['json']['answer'] ?? ''),
'client_strengths' => $isAdvocate ? ($synthesis['json']['client_strengths'] ?? []) : null,
'opposing_weaknesses' => $isAdvocate ? ($synthesis['json']['opposing_weaknesses'] ?? []) : null,
'sub_questions' => $subQOut,
'sources' => $numberedSources,
'what_we_found' => (string)($synthesis['json']['what_we_found'] ?? ''),
@@ -405,11 +411,14 @@ final class DbnDeepResearchAgent
return implode("\n\n", $parts);
}
private function interpretSeed(string $seedDescription, string $language): array
private function interpretSeed(string $seedDescription, string $language, string $advocateRole = ''): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$rolePrefix = $advocateRole !== ''
? "You are preparing a case-research brief for: {$advocateRole}. Frame your interpretation to identify the strongest legal angles for this party.\n\n"
: '';
$prompt = <<<PROMPT
You are reviewing the input below to set up a deep legal research pass against the Do Better Norge family-law corpus.
{$rolePrefix}You are reviewing the input below to set up a deep legal research pass against the Do Better Norge family-law corpus.
Input:
{$seedDescription}
@@ -445,10 +454,41 @@ PROMPT;
];
}
private function expandQueries(string $seedDescription, string $brief, int $targetCount, string $language): array
private function expandQueries(string $seedDescription, string $brief, int $targetCount, string $language, string $advocateRole = ''): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$prompt = <<<PROMPT
if ($advocateRole !== '') {
$prompt = <<<PROMPT
You are a Norwegian family-law research assistant building a case for: {$advocateRole}.
Generate exactly {$targetCount} targeted sub-questions designed to find:
1. Lovdata statutes and ECHR/Hague precedents that support {$advocateRole}'s position.
2. Procedural rights and obligations the opposing party must satisfy — failures here help {$advocateRole}.
3. Case law that exposes weaknesses in the opposing party's likely arguments.
4. Specific articles, paragraphs, or judgments {$advocateRole}'s representative should cite.
Research brief:
{$brief}
Raw input:
{$seedDescription}
Return JSON only in {$locale}:
{
"sub_questions": [
{"id":"q1","question":"...","rationale":"how finding this strengthens {$advocateRole}'s case (≤ 140 chars)"}
]
}
Rules:
- Exactly {$targetCount} sub-questions, no more, no fewer.
- Every question must be answerable from Norwegian family-law, child-welfare, or ECHR/Hague sources.
- Each question must cover a DIFFERENT angle (supporting statute, procedural right, opposing weakness, ECHR precedent, evidentiary frame).
- Sub-questions must be self-contained — readable without the raw input.
- Write the questions in {$locale}.
PROMPT;
} else {
$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).
Research brief:
@@ -471,6 +511,7 @@ Rules:
- Sub-questions must be self-contained — readable without seeing the seed text.
- Write the questions in {$locale}.
PROMPT;
}
try {
$raw = $this->azure->chatText([
@@ -790,7 +831,8 @@ PROMPT;
array $numberedSources,
string $engine,
string $language,
float $temperature
float $temperature,
string $advocateRole = ''
): array {
$locale = $language === 'no' ? 'Norwegian' : 'English';
@@ -839,7 +881,44 @@ PROMPT;
? '400-900 words, minimum 4 paragraphs, with clear paragraph breaks. Cover EACH sub-question above in its own paragraph.'
: '250-450 words, 2-3 short paragraphs. Note when evidence is thin.';
$prompt = <<<PROMPT
if ($advocateRole !== '') {
$prompt = <<<PROMPT
You are Do Better Norge Legal Tools producing a legal preparation brief in {$locale}.
Your client: {$advocateRole}
You MUST ground every claim in the numbered sources below using inline `[n]` citation markers. Do NOT invent statutes, paragraph numbers, case names, dates, or parties.
User input:
{$seedDescription}
Research brief:
{$brief}
{$subQText}
Sources ({$sourceCount} numbered):
{$sourcesText}
Return JSON only in {$locale}:
{
"brief_markdown": "Partisan but factually grounded advocate brief. {$lengthGuidance} Structure: (1) {$advocateRole}'s core legal position, (2) Strongest supporting arguments with [n] citations, (3) Identified weaknesses in the opposing party's position with [n] citations, (4) Procedural rights and obligations {$advocateRole} should assert. End with a one-line caveat that this is legal preparation support, not final legal advice.",
"client_strengths": ["3-6 strings — the strongest factual/legal points for {$advocateRole}, each anchored to at least one [n] source"],
"opposing_weaknesses": ["2-5 strings — vulnerabilities in the opposing position supported by retrieved sources. Omit this array entirely if evidence is thin — do NOT invent weaknesses."],
"what_we_found": "2-sentence summary of the most relevant retrieved authority for {$advocateRole}",
"what_remains_uncertain": ["3-5 gaps where evidence is insufficient or law is unclear — be honest"],
"next_practical_step": "one concrete action for {$advocateRole} to take next (legal filing, evidence gathering, consultation type, etc.)"
}
Rules:
- Every factual claim in `brief_markdown` must end with one or more `[n]` markers.
- If no source supports a point, omit the point — DO NOT speculate.
- Prefer citing statute sections (e.g. "Barneloven §43") and case names verbatim from source excerpts.
- When multiple sources support the same point, cite all of them (e.g. `[2,4]`).
- `opposing_weaknesses` must be omitted or empty when no retrieved source actually supports the identified weakness.
- Respond in {$locale}.
- Output valid JSON only — no markdown fences around the JSON object itself.
PROMPT;
} else {
$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.
User input:
@@ -868,6 +947,7 @@ Rules:
- Respond in {$locale}.
- Output valid JSON only — no markdown fences around the JSON object itself.
PROMPT;
}
$messages = [
['role' => 'system', 'content' => 'You return valid JSON only. No markdown fences.'],