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)) {
$engine = $inputEngine;
}
$length = in_array($input['length'] ?? '', ['concise', 'standard', 'detailed'], true)
? (string)$input['length'] : 'standard';
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
$creditDeducted = true;
@@ -179,7 +181,7 @@ try {
: null;
// ── 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['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000);
if ($ftRemaining >= 0) {
+3 -1
View File
@@ -11,6 +11,8 @@ $ftUid = dbnToolsFreeTierCheck('summarize');
$input = dbnToolsJsonInput(400000);
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
$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'])) : [];
// Streaming headers — flush each NDJSON line as it's written
@@ -68,7 +70,7 @@ try {
'detail' => 'Generating summary…',
]);
$result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext);
$result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext, $depth);
if ($ftUid > 0) {
$balance = dbnToolsFreeTierDeduct($ftUid, 'summarize');
+1
View File
@@ -281,6 +281,7 @@
force_draft: !!forceDraft,
use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false,
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 (persona) payload.profile = persona;
+1
View File
@@ -186,6 +186,7 @@
text: combined,
language: _currentLang,
engine: engine,
depth: (document.querySelector('input[name="sumDepth"]:checked') || {}).value || 'standard',
slices: slices,
};
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).
*/
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)
? (($engine === 'claude_sonnet' || $engine === 'azure_full')
@@ -270,7 +270,7 @@ PROMPT;
// ── Draft in Norwegian bokmål ───────────────────────────────────────────
if ($emit) { $emit('progress', ['detail' => self::L('drafting_no', $userLang)]); }
$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 ──────────────────
@@ -550,7 +550,8 @@ PROMPT;
private function draftNorwegian(
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 {
$context = $this->buildContextBlob($intake);
$toneLabel = $this->toneLabelNorsk($tone);
@@ -574,9 +575,16 @@ PROMPT;
$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
Du skriver utkast til korrespondanse til {$bodyLabel}. Skriv på norsk bokmål.
Tone: {$toneLabel}.
Lengde: {$lengthLabel}
{$goalLine}
@@ -588,6 +596,8 @@ REGLER FOR LOVHENVISNINGER (kritisk):
fra passasjelisten. Eksempel: "etter forvaltningsloven § 17 [CITE:2]".
- Hvis du ikke finner dekning i passasjene, skriv UTEN §-henvisning.
- 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}
+23 -4
View File
@@ -1854,7 +1854,8 @@ PROMPT;
string $text,
string $language = 'en',
string $engine = 'azure_mini',
string $corpusContext = ''
string $corpusContext = '',
string $depth = 'standard'
): array {
$text = $this->requirePasteText($text);
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
@@ -1870,16 +1871,34 @@ PROMPT;
. $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
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}
Return this JSON structure:
{
"what_we_found": "plain-language summary (2-4 sentences)",
"key_facts": ["fact 1", "fact 2"],
"what_we_found": "{$depthInstructions['what_we_found']}",
"key_facts": ["{$depthInstructions['key_facts']}"],
"dates": ["date or event phrase"],
"parties": ["party or role"],
"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>
</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 -->
<div class="control-row" id="korrEngineControl">
<span class="control-label">Engine</span>
+8
View File
@@ -24,6 +24,14 @@ require_once __DIR__ . '/includes/layout.php';
</div>
<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">
<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>