korrespond v1 premiere: Bedrock routing, engine picker, journal auto-save + status
- 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 <noreply@anthropic.com>
This commit is contained in:
+18
@@ -935,6 +935,14 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
|||||||
<?php if (!empty($r['used_case_context'])): ?>
|
<?php if (!empty($r['used_case_context'])): ?>
|
||||||
· <span style="background:#dbeafe;color:#1e3a8a;padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;"><?= htmlspecialchars($l['used_case_badge']) ?></span>
|
· <span style="background:#dbeafe;color:#1e3a8a;padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;"><?= htmlspecialchars($l['used_case_badge']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if ($r['tool'] === 'korrespond'): ?>
|
||||||
|
· <select class="korr-status-select" data-id="<?= (int)$r['id'] ?>" style="font-size:.8rem;padding:1px 4px;border-radius:4px;border:1px solid #d1d5db;background:#fff;cursor:pointer;">
|
||||||
|
<option value="draft" <?= ($r['korr_status'] ?? 'draft') === 'draft' ? 'selected' : '' ?>>Draft</option>
|
||||||
|
<option value="sent" <?= ($r['korr_status'] ?? '') === 'sent' ? 'selected' : '' ?>>Sent</option>
|
||||||
|
<option value="replied" <?= ($r['korr_status'] ?? '') === 'replied' ? 'selected' : '' ?>>Reply received</option>
|
||||||
|
<option value="resolved" <?= ($r['korr_status'] ?? '') === 'resolved' ? 'selected' : '' ?>>Resolved</option>
|
||||||
|
</select>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="acct-result-actions">
|
<div class="acct-result-actions">
|
||||||
@@ -1164,6 +1172,16 @@ window.ACCT_L = <?= json_encode($l, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLAS
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.korr-status-select').forEach(function (sel) {
|
||||||
|
sel.addEventListener('change', function () {
|
||||||
|
fetch('/api/case/result-action.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'set_status', id: parseInt(sel.getAttribute('data-id'), 10), status: sel.value })
|
||||||
|
}).catch(function () {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.acct-result-delete').forEach(function (btn) {
|
document.querySelectorAll('.acct-result-delete').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
|
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ switch ($action) {
|
|||||||
dbnToolsRespond(['ok' => true]);
|
dbnToolsRespond(['ok' => true]);
|
||||||
break;
|
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:
|
default:
|
||||||
dbnToolsError('Unknown action.', 422, 'unknown_action');
|
dbnToolsError('Unknown action.', 422, 'unknown_action');
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -165,8 +165,12 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
|
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
|
||||||
$ftUid = dbnToolsFreeTierCheck('korrespond');
|
$ftUid = dbnToolsFreeTierCheck('korrespond');
|
||||||
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
|
$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');
|
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
|
||||||
$creditDeducted = true;
|
$creditDeducted = true;
|
||||||
|
|
||||||
@@ -184,12 +188,28 @@ try {
|
|||||||
'ok' => true,
|
'ok' => true,
|
||||||
'latency_ms' => $result['latency_ms'],
|
'latency_ms' => $result['latency_ms'],
|
||||||
'source_count' => is_array($result['cited_law'] ?? null) ? count($result['cited_law']) : 0,
|
'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]);
|
$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) {
|
} catch (DbnToolsHttpException $e) {
|
||||||
$latency = (int)round((microtime(true) - $startTime) * 1000);
|
$latency = (int)round((microtime(true) - $startTime) * 1000);
|
||||||
dbnToolsLogMetadata([
|
dbnToolsLogMetadata([
|
||||||
|
|||||||
@@ -276,6 +276,7 @@
|
|||||||
clarifications: pendingClarifications,
|
clarifications: pendingClarifications,
|
||||||
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'),
|
||||||
};
|
};
|
||||||
if (korrDocIds.length) payload.doc_ids = korrDocIds;
|
if (korrDocIds.length) payload.doc_ids = korrDocIds;
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ final class CaseResults
|
|||||||
$db = dbnmDb();
|
$db = dbnmDb();
|
||||||
$stmt = $db->prepare(
|
$stmt = $db->prepare(
|
||||||
'SELECT id, user_id, owner_user_id, tool, title, used_case_context,
|
'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
|
FROM case_tool_results
|
||||||
WHERE owner_user_id = ? AND deleted_at IS NULL
|
WHERE owner_user_id = ? AND deleted_at IS NULL
|
||||||
ORDER BY pinned DESC, created_at DESC
|
ORDER BY pinned DESC, created_at DESC
|
||||||
@@ -226,6 +226,27 @@ final class CaseResults
|
|||||||
return $stmt->rowCount() > 0;
|
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. */
|
/** Human-readable Norwegian title hint per tool. */
|
||||||
public static function toolLabel(string $tool): string
|
public static function toolLabel(string $tool): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ final class DbnKorrespondAgent
|
|||||||
private const MAX_CONTEXT_CHARS = 24000;
|
private const MAX_CONTEXT_CHARS = 24000;
|
||||||
private const MAX_DRAFT_TOKENS = 2000;
|
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. */
|
/** Recipient-body presets → default slice toggles for law retrieval. */
|
||||||
private const BODY_PRESETS = [
|
private const BODY_PRESETS = [
|
||||||
'barnehage' => ['family_core', 'bufdir_guidance'],
|
'barnehage' => ['family_core', 'bufdir_guidance'],
|
||||||
@@ -215,7 +223,7 @@ PROMPT;
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
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' => 'system', 'content' => 'You return valid JSON only. No markdown fences.'],
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
], ['json' => true, 'temperature' => 0.1, 'max_tokens' => 800, 'timeout' => 30]);
|
], ['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
|
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';
|
$body = $intake['recipient_body'] ?? 'other';
|
||||||
$outputType = $intake['output_type'] ?? 'email';
|
$outputType = $intake['output_type'] ?? 'email';
|
||||||
$tone = $intake['tone'] ?? 'neutral';
|
$tone = $intake['tone'] ?? 'neutral';
|
||||||
@@ -667,7 +679,7 @@ Norwegian source:
|
|||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
try {
|
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' => 'system', 'content' => 'You are a precise legal translator.'],
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
], [
|
], [
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ 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>
|
||||||
|
|
||||||
|
<!-- Engine -->
|
||||||
|
<div class="control-row" id="korrEngineControl">
|
||||||
|
<span class="control-label">Engine</span>
|
||||||
|
<label><input type="radio" name="korrEngine" value="azure_mini" checked> ☁️ Quick <small class="control-hint">(~40-70s)</small></label>
|
||||||
|
<label><input type="radio" name="korrEngine" value="claude_sonnet"> ☁️ Thorough ★★ <small class="control-hint">(~70-120s)</small></label>
|
||||||
|
</div>
|
||||||
|
<p class="upload-hint">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.</p>
|
||||||
|
|
||||||
<!-- Case context fields -->
|
<!-- Case context fields -->
|
||||||
<div class="dr-control-grid">
|
<div class="dr-control-grid">
|
||||||
<div class="dr-control-card">
|
<div class="dr-control-card">
|
||||||
|
|||||||
Reference in New Issue
Block a user