From b2e1bf268d981c35cdf3494fe38b7b5ad479ad70 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Mon, 25 May 2026 21:52:09 +0200 Subject: [PATCH] korrespond v1 premiere: Bedrock routing, engine picker, journal auto-save + status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KorrespondAgent: add resolveDeployment() helper; fix classify/translate to use Haiku via Bedrock, draft to use Haiku (quick) or Sonnet (thorough) — fixes broken withDeployment('gpt-4o-mini') calls when DBN_BEDROCK_ENABLED=true - korrespond.php: add Quick/Thorough engine picker (case_toggle already present) - korrespond.js: pass engine in request payload - api/korrespond.php: accept user-selected engine, auto-save to case_tool_results for paid users after each successful run, update deployment log label - CaseResults: add korr_status to listForUser SELECT, add updateStatus() method - result-action.php: add set_status action for correspondence journal - account.php: show status dropdown (Draft/Sent/Reply received/Resolved) for korrespond entries in #analyses, wire JS change handler to result-action.php Co-Authored-By: Claude Sonnet 4.6 --- account.php | 18 ++++++++++++++++++ api/case/result-action.php | 8 ++++++++ api/korrespond.php | 24 ++++++++++++++++++++++-- assets/js/korrespond.js | 1 + includes/CaseResults.php | 23 ++++++++++++++++++++++- includes/KorrespondAgent.php | 18 +++++++++++++++--- korrespond.php | 8 ++++++++ 7 files changed, 94 insertions(+), 6 deletions(-) diff --git a/account.php b/account.php index 00ce7cd..9f6f95a 100644 --- a/account.php +++ b/account.php @@ -935,6 +935,14 @@ window.DBN_TOOLS_LANG = ; · + + · +
@@ -1164,6 +1172,16 @@ window.ACCT_L = true]); break; + case 'set_status': + $status = (string)($input['status'] ?? ''); + if (!CaseResults::updateStatus($userId, $id, $status)) { + dbnToolsError('Could not update status — invalid value or result not found.', 422, 'status_failed'); + } + dbnToolsRespond(['ok' => true, 'status' => $status]); + break; + default: dbnToolsError('Unknown action.', 422, 'unknown_action'); } diff --git a/api/korrespond.php b/api/korrespond.php index e3ff488..4a8069b 100644 --- a/api/korrespond.php +++ b/api/korrespond.php @@ -165,8 +165,12 @@ try { } // ── Deduct credit now (Pass 2 starts) ─────────────────────────────────────── - $ftUid = dbnToolsFreeTierCheck('korrespond'); + $ftUid = dbnToolsFreeTierCheck('korrespond'); $engine = ToolModels::engineForUser($ftUid, 'azure_mini'); + $inputEngine = (string)($input['engine'] ?? ''); + if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) { + $engine = $inputEngine; + } $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond'); $creditDeducted = true; @@ -184,12 +188,28 @@ try { 'ok' => true, 'latency_ms' => $result['latency_ms'], 'source_count' => is_array($result['cited_law'] ?? null) ? count($result['cited_law']) : 0, - 'deployment' => ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini', + 'deployment' => $engine, ]); $emit('final', ['result' => $result]); + // Auto-save for paid users — non-critical, silent on failure + try { + require_once __DIR__ . '/../includes/CaseResults.php'; + require_once __DIR__ . '/../includes/CaseStore.php'; + $authUser = dbnToolsAuthenticatedUser(); + $uid = (int)($authUser['user_id'] ?? 0); + $oid = CaseStore::caseResolveClientId($uid); + CaseResults::save($uid, $oid, 'korrespond', $intake, $result, [ + 'used_case_context' => !empty($intake['use_my_case']) ? 1 : 0, + 'case_doc_ids' => $GLOBALS['dbn_last_case_doc_ids'] ?? [], + 'model' => $engine, + 'latency_ms' => $result['latency_ms'], + 'credits_charged' => 1, + ]); + } catch (Throwable) { /* non-critical */ } + } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ diff --git a/assets/js/korrespond.js b/assets/js/korrespond.js index 16b8800..e706aaf 100644 --- a/assets/js/korrespond.js +++ b/assets/js/korrespond.js @@ -276,6 +276,7 @@ clarifications: pendingClarifications, force_draft: !!forceDraft, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, + engine: (document.querySelector('[name="korrEngine"]:checked')?.value ?? 'azure_mini'), }; if (korrDocIds.length) payload.doc_ids = korrDocIds; return payload; diff --git a/includes/CaseResults.php b/includes/CaseResults.php index 5434dfc..b0db033 100644 --- a/includes/CaseResults.php +++ b/includes/CaseResults.php @@ -124,7 +124,7 @@ final class CaseResults $db = dbnmDb(); $stmt = $db->prepare( 'SELECT id, user_id, owner_user_id, tool, title, used_case_context, - model, latency_ms, credits_charged, pinned, created_at + model, latency_ms, credits_charged, pinned, korr_status, created_at FROM case_tool_results WHERE owner_user_id = ? AND deleted_at IS NULL ORDER BY pinned DESC, created_at DESC @@ -226,6 +226,27 @@ final class CaseResults return $stmt->rowCount() > 0; } + /** Update the correspondence status (korrespond tool only). */ + public static function updateStatus(int $userId, int $id, string $status): bool + { + $allowed = ['draft', 'sent', 'replied', 'resolved']; + if (!in_array($status, $allowed, true)) { + return false; + } + if (!self::tableReady()) { + return false; + } + $ownerId = CaseStore::caseResolveClientId($userId); + $db = dbnmDb(); + $stmt = $db->prepare( + "UPDATE case_tool_results + SET korr_status = ? + WHERE id = ? AND owner_user_id = ? AND tool = 'korrespond' AND deleted_at IS NULL" + ); + $stmt->execute([$status, $id, $ownerId]); + return $stmt->rowCount() > 0; + } + /** Human-readable Norwegian title hint per tool. */ public static function toolLabel(string $tool): string { diff --git a/includes/KorrespondAgent.php b/includes/KorrespondAgent.php index c4729d6..34a5efd 100644 --- a/includes/KorrespondAgent.php +++ b/includes/KorrespondAgent.php @@ -27,6 +27,14 @@ final class DbnKorrespondAgent private const MAX_CONTEXT_CHARS = 24000; private const MAX_DRAFT_TOKENS = 2000; + private function resolveDeployment(string $azureDeployment, bool $heavy = false): string + { + if (!($this->azure instanceof DbnBedrockGateway)) { + return $azureDeployment; + } + return $heavy ? DbnBedrockModelRouter::LITELLM_SONNET : DbnBedrockModelRouter::LITELLM_HAIKU; + } + /** Recipient-body presets → default slice toggles for law retrieval. */ private const BODY_PRESETS = [ 'barnehage' => ['family_core', 'bufdir_guidance'], @@ -215,7 +223,7 @@ PROMPT; ]; try { - $raw = $this->azure->withDeployment(self::CLASSIFY_DEPLOYMENT)->chatText([ + $raw = $this->azure->withDeployment($this->resolveDeployment(self::CLASSIFY_DEPLOYMENT))->chatText([ ['role' => 'system', 'content' => 'You return valid JSON only. No markdown fences.'], ['role' => 'user', 'content' => $prompt], ], ['json' => true, 'temperature' => 0.1, 'max_tokens' => 800, 'timeout' => 30]); @@ -237,7 +245,11 @@ PROMPT; */ public function generate(array $intake, array $classify, ?callable $emit = null, string $engine = 'azure_mini'): array { - $draftDeployment = ($engine === 'azure_full') ? 'gpt-4o' : 'gpt-4o-mini'; + $draftDeployment = ($this->azure instanceof DbnBedrockGateway) + ? (($engine === 'claude_sonnet' || $engine === 'azure_full') + ? DbnBedrockModelRouter::LITELLM_SONNET + : DbnBedrockModelRouter::LITELLM_HAIKU) + : (($engine === 'azure_full') ? self::DRAFT_DEPLOYMENT : 'gpt-4o-mini'); $body = $intake['recipient_body'] ?? 'other'; $outputType = $intake['output_type'] ?? 'email'; $tone = $intake['tone'] ?? 'neutral'; @@ -667,7 +679,7 @@ Norwegian source: PROMPT; try { - return $this->azure->withDeployment(self::SELFCHECK_DEPLOYMENT)->chatText([ + return $this->azure->withDeployment($this->resolveDeployment(self::SELFCHECK_DEPLOYMENT))->chatText([ ['role' => 'system', 'content' => 'You are a precise legal translator.'], ['role' => 'user', 'content' => $prompt], ], [ diff --git a/korrespond.php b/korrespond.php index 41bcd69..66907cf 100644 --- a/korrespond.php +++ b/korrespond.php @@ -70,6 +70,14 @@ require_once __DIR__ . '/includes/layout.php';
+ +
+ Engine + + +
+

Quick uses Claude Haiku 4.5 for drafting — fast and solid for standard correspondence. Thorough uses Claude Sonnet 4.6 — better at multi-statute cases, complex appeal grounds, and ECHR framing.

+