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:
@@ -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.'],
|
||||
|
||||
Reference in New Issue
Block a user