feat(tools): add letter length + summary depth controls; harden korrespond §-discipline

- Summarize: new depth param (brief/standard/detailed) with depth-aware prompt
  instructions and coverage mandate; wired through API + JS
- Korrespond: new letter length param (concise/standard/detailed) injected as
  Lengde: instruction in draft pass; wired through API + JS
- Korrespond draft prompt: add §-discipline rule (cite only directly relevant §§)
  plus Opphevet guard (aligned with dobetterlegal-tools)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:44:02 +02:00
parent 8b99ceec3b
commit 3f7d4eef13
8 changed files with 61 additions and 9 deletions
+3 -1
View File
@@ -171,6 +171,8 @@ try {
if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) { if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) {
$engine = $inputEngine; $engine = $inputEngine;
} }
$length = in_array($input['length'] ?? '', ['concise', 'standard', 'detailed'], true)
? (string)$input['length'] : 'standard';
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond'); $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
$creditDeducted = true; $creditDeducted = true;
@@ -179,7 +181,7 @@ try {
: null; : null;
// ── Pass 2: retrieve law → draft → self-check → translate ────────────────── // ── Pass 2: retrieve law → draft → self-check → translate ──────────────────
$result = $agent->generate($intake, $classify, $emit, $engine, $personaSlug); $result = $agent->generate($intake, $classify, $emit, $engine, $personaSlug, $length);
$result['ok'] = true; $result['ok'] = true;
$result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000);
if ($ftRemaining >= 0) { if ($ftRemaining >= 0) {
+3 -1
View File
@@ -11,6 +11,8 @@ $ftUid = dbnToolsFreeTierCheck('summarize');
$input = dbnToolsJsonInput(400000); $input = dbnToolsJsonInput(400000);
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
$depth = in_array($input['depth'] ?? '', ['brief', 'standard', 'detailed'], true)
? (string)$input['depth'] : 'standard';
$slices = is_array($input['slices'] ?? null) ? array_values(array_filter($input['slices'])) : []; $slices = is_array($input['slices'] ?? null) ? array_values(array_filter($input['slices'])) : [];
// Streaming headers — flush each NDJSON line as it's written // Streaming headers — flush each NDJSON line as it's written
@@ -68,7 +70,7 @@ try {
'detail' => 'Generating summary…', 'detail' => 'Generating summary…',
]); ]);
$result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext); $result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext, $depth);
if ($ftUid > 0) { if ($ftUid > 0) {
$balance = dbnToolsFreeTierDeduct($ftUid, 'summarize'); $balance = dbnToolsFreeTierDeduct($ftUid, 'summarize');
+1
View File
@@ -281,6 +281,7 @@
force_draft: !!forceDraft, force_draft: !!forceDraft,
use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false,
engine: (document.querySelector('[name="korrEngine"]:checked')?.value ?? 'azure_mini'), engine: (document.querySelector('[name="korrEngine"]:checked')?.value ?? 'azure_mini'),
length: (document.querySelector('[name="korrLength"]:checked')?.value ?? 'standard'),
}; };
if (korrDocIds.length) payload.doc_ids = korrDocIds; if (korrDocIds.length) payload.doc_ids = korrDocIds;
if (persona) payload.profile = persona; if (persona) payload.profile = persona;
+1
View File
@@ -186,6 +186,7 @@
text: combined, text: combined,
language: _currentLang, language: _currentLang,
engine: engine, engine: engine,
depth: (document.querySelector('input[name="sumDepth"]:checked') || {}).value || 'standard',
slices: slices, slices: slices,
}; };
if (docIds.length) payload.doc_ids = docIds; if (docIds.length) payload.doc_ids = docIds;
+13 -3
View File
@@ -243,7 +243,7 @@ PROMPT;
* *
* @return array Final result payload (matches NDJSON 'final' event shape). * @return array Final result payload (matches NDJSON 'final' event shape).
*/ */
public function generate(array $intake, array $classify, ?callable $emit = null, string $engine = 'azure_mini', ?string $persona = null): array public function generate(array $intake, array $classify, ?callable $emit = null, string $engine = 'azure_mini', ?string $persona = null, string $length = 'standard'): array
{ {
$draftDeployment = ($this->azure instanceof DbnBedrockGateway) $draftDeployment = ($this->azure instanceof DbnBedrockGateway)
? (($engine === 'claude_sonnet' || $engine === 'azure_full') ? (($engine === 'claude_sonnet' || $engine === 'azure_full')
@@ -270,7 +270,7 @@ PROMPT;
// ── Draft in Norwegian bokmål ─────────────────────────────────────────── // ── Draft in Norwegian bokmål ───────────────────────────────────────────
if ($emit) { $emit('progress', ['detail' => self::L('drafting_no', $userLang)]); } if ($emit) { $emit('progress', ['detail' => self::L('drafting_no', $userLang)]); }
$draftNo = $this->draftNorwegian( $draftNo = $this->draftNorwegian(
$intake, $classify, $retrieval['sources'], $bodyLabel, $outputType, $tone, $goal, $draftDeployment $intake, $classify, $retrieval['sources'], $bodyLabel, $outputType, $tone, $goal, $draftDeployment, $length
); );
// ── Self-check: verify citations, deadline, goal, tone ────────────────── // ── Self-check: verify citations, deadline, goal, tone ──────────────────
@@ -550,7 +550,8 @@ PROMPT;
private function draftNorwegian( private function draftNorwegian(
array $intake, array $classify, array $sources, string $bodyLabel, array $intake, array $classify, array $sources, string $bodyLabel,
string $outputType, string $tone, string $goal, string $draftDeployment = self::DRAFT_DEPLOYMENT string $outputType, string $tone, string $goal, string $draftDeployment = self::DRAFT_DEPLOYMENT,
string $length = 'standard'
): string { ): string {
$context = $this->buildContextBlob($intake); $context = $this->buildContextBlob($intake);
$toneLabel = $this->toneLabelNorsk($tone); $toneLabel = $this->toneLabelNorsk($tone);
@@ -574,9 +575,16 @@ PROMPT;
$goalLine = $goal !== '' ? ('Brukerens mål: ' . $goal) : 'Brukerens mål: ikke spesifisert — utled fra konteksten.'; $goalLine = $goal !== '' ? ('Brukerens mål: ' . $goal) : 'Brukerens mål: ikke spesifisert — utled fra konteksten.';
$lengthLabel = match($length) {
'concise' => 'Kort og poengtert — maks 2-3 avsnitt. Kom raskt til poenget.',
'detailed' => 'Utfyllende — inkluder full bakgrunn, alle argumenter og rettslig begrunnelse.',
default => 'Standard lengde — 4-6 avsnitt.',
};
$prompt = <<<PROMPT $prompt = <<<PROMPT
Du skriver utkast til korrespondanse til {$bodyLabel}. Skriv på norsk bokmål. Du skriver utkast til korrespondanse til {$bodyLabel}. Skriv på norsk bokmål.
Tone: {$toneLabel}. Tone: {$toneLabel}.
Lengde: {$lengthLabel}
{$goalLine} {$goalLine}
@@ -588,6 +596,8 @@ REGLER FOR LOVHENVISNINGER (kritisk):
fra passasjelisten. Eksempel: "etter forvaltningsloven § 17 [CITE:2]". fra passasjelisten. Eksempel: "etter forvaltningsloven § 17 [CITE:2]".
- Hvis du ikke finner dekning i passasjene, skriv UTEN §-henvisning. - Hvis du ikke finner dekning i passasjene, skriv UTEN §-henvisning.
- IKKE finn på §-nummer eller artikkelnumre. - IKKE finn på §-nummer eller artikkelnumre.
- IKKE siter §-er som er markert "(Opphevet)" eller "Opphevet ved lov" i passasjene — de gjelder ikke lenger.
- Siter kun §-er som er DIREKTE RELEVANTE for den juridiske saken i brevet. Ikke siter §-er fra lovteksten som ikke brukes i argumentasjonen.
{$sourcesBlock} {$sourcesBlock}
+23 -4
View File
@@ -1854,7 +1854,8 @@ PROMPT;
string $text, string $text,
string $language = 'en', string $language = 'en',
string $engine = 'azure_mini', string $engine = 'azure_mini',
string $corpusContext = '' string $corpusContext = '',
string $depth = 'standard'
): array { ): array {
$text = $this->requirePasteText($text); $text = $this->requirePasteText($text);
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini'; $engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
@@ -1870,16 +1871,34 @@ PROMPT;
. $text; . $text;
} }
$depthInstructions = match($depth) {
'brief' => [
'what_we_found' => '1-2 sentence executive summary covering the main outcome',
'key_facts' => 'up to 3 most important facts',
'coverage' => '',
],
'detailed' => [
'what_we_found' => 'comprehensive 6-10 sentence summary covering every fact, decision, party, date, and legal implication',
'key_facts' => 'every fact, decision, and legal reference mentioned in the document',
'coverage' => "\nCover ALL parties, ALL key decisions, ALL dates, and ALL legal references present in the document. Do not omit facts because they seem minor — err on the side of inclusion.",
],
default => [
'what_we_found' => '3-5 sentence summary covering all key outcomes, parties, and decisions',
'key_facts' => 'all key facts',
'coverage' => "\nCover ALL parties, ALL key decisions, ALL dates, and ALL legal references present in the document.",
],
};
$prompt = <<<PROMPT $prompt = <<<PROMPT
Summarise the following document in {$locale}. Do not invent facts not present in the text. Summarise the following document in {$locale}. Do not invent facts not present in the text.
Return JSON only — no extra text before or after the JSON object. Return JSON only — no extra text before or after the JSON object.{$depthInstructions['coverage']}
{$enriched} {$enriched}
Return this JSON structure: Return this JSON structure:
{ {
"what_we_found": "plain-language summary (2-4 sentences)", "what_we_found": "{$depthInstructions['what_we_found']}",
"key_facts": ["fact 1", "fact 2"], "key_facts": ["{$depthInstructions['key_facts']}"],
"dates": ["date or event phrase"], "dates": ["date or event phrase"],
"parties": ["party or role"], "parties": ["party or role"],
"legal_references_detected": ["statute, article, or case name"], "legal_references_detected": ["statute, article, or case name"],
+9
View File
@@ -70,6 +70,15 @@ require_once __DIR__ . '/includes/layout.php';
<label><input type="radio" name="korrTone" value="warm"> Conciliatory-warm</label> <label><input type="radio" name="korrTone" value="warm"> Conciliatory-warm</label>
</div> </div>
<!-- Letter length -->
<div class="control-row" id="korrLengthControl">
<span class="control-label">Letter length</span>
<label><input type="radio" name="korrLength" value="concise"> Concise</label>
<label><input type="radio" name="korrLength" value="standard" checked> Standard &#9733;</label>
<label><input type="radio" name="korrLength" value="detailed"> Detailed</label>
</div>
<p class="upload-hint">Concise: short and to-the-point (2-3 paragraphs). Standard: balanced correspondence. Detailed: full background, all arguments, and complete legal reasoning.</p>
<!-- Engine --> <!-- Engine -->
<div class="control-row" id="korrEngineControl"> <div class="control-row" id="korrEngineControl">
<span class="control-label">Engine</span> <span class="control-label">Engine</span>
+8
View File
@@ -24,6 +24,14 @@ require_once __DIR__ . '/includes/layout.php';
</div> </div>
<p class="upload-hint">Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy on the GPU server.</p> <p class="upload-hint">Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy on the GPU server.</p>
<div class="control-row" id="sumDepthControl">
<span class="control-label">Summary depth</span>
<label><input type="radio" name="sumDepth" value="brief"> Brief</label>
<label><input type="radio" name="sumDepth" value="standard" checked> Standard &#9733;</label>
<label><input type="radio" name="sumDepth" value="detailed"> Detailed</label>
</div>
<p class="upload-hint">Brief: 1-2 sentence executive summary. Standard: balanced summary with all key facts. Detailed: comprehensive &#8212; covers every fact, party, date, and legal reference.</p>
<details class="advanced-panel" id="sumSlicesPanel"> <details class="advanced-panel" id="sumSlicesPanel">
<summary class="advanced-toggle">Legal corpus enrichment <small class="control-hint">(optional)</small></summary> <summary class="advanced-toggle">Legal corpus enrichment <small class="control-hint">(optional)</small></summary>
<p class="upload-hint">When one or more slices are enabled, the tool searches the Do Better Norge legal corpus for relevant passages and prepends them to the prompt. All slices are off by default — enable only what applies to your document.</p> <p class="upload-hint">When one or more slices are enabled, the tool searches the Do Better Norge legal corpus for relevant passages and prepends them to the prompt. All slices are off by default — enable only what applies to your document.</p>