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'])): ?>
|
||||
· <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 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 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) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
|
||||
|
||||
@@ -48,6 +48,14 @@ switch ($action) {
|
||||
dbnToolsRespond(['ok' => 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');
|
||||
}
|
||||
|
||||
+21
-1
@@ -167,6 +167,10 @@ try {
|
||||
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
|
||||
$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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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],
|
||||
], [
|
||||
|
||||
@@ -70,6 +70,14 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<label><input type="radio" name="korrTone" value="warm"> Conciliatory-warm</label>
|
||||
</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 -->
|
||||
<div class="dr-control-grid">
|
||||
<div class="dr-control-card">
|
||||
|
||||
Reference in New Issue
Block a user