feat(tools): converge two-tier Quick/Pro selector onto .no fork
Port the dobetterlegal-tools two-tier quality stack to dobetternorge.no: QUALITY_TIERS registry + resolveTier (ToolModels), dbnToolsResolveToolRun (bootstrap), tier read+charge in the 6 analytical endpoints, Quick/Pro UI + payload.tier on the 6 tool pages/JS, and the bounded corpusContextForSummarize RAG fix (per-passage trim + total budget + reranker_enabled). Back-compat: requests without `tier` keep legacy engine behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -50,12 +50,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<small id="advInputCount" class="adv-char-count">0 / 4,000</small>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="advEngineControl">
|
||||
<span class="control-label">Engine</span>
|
||||
<label><input type="radio" name="advEngine" value="azure_mini" checked> ☁️ Claude Haiku 4.5 <small class="control-hint">(fast · ~2-4 min)</small></label>
|
||||
<label><input type="radio" name="advEngine" value="claude_sonnet"> ☁️ Claude Sonnet 4.6 ★★ <small class="control-hint">(thorough · ~3-5 min)</small></label>
|
||||
<div class="control-row" id="advTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="advTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 3 credits)</small></label>
|
||||
<label><input type="radio" name="advTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 6 credits)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint">Both engines run on AWS Bedrock via Claude. Most of the time is spent on multiple question-answering passes — 6–10 sub-questions each requiring a full retrieval and answer cycle. Haiku is faster and handles most cases well. Sonnet produces a more thorough brief with deeper ECHR precedent analysis and stronger multi-party argumentation.</p>
|
||||
<p class="upload-hint">Most of the time is spent on multiple question-answering passes — 6–10 sub-questions each requiring a full retrieval and answer cycle. Quick uses Claude Haiku 4.5 — faster and handles most cases well (3 credits). Pro uses Claude Sonnet 4.6 — a more thorough brief with deeper ECHR precedent analysis and stronger multi-party argumentation (6 credits).</p>
|
||||
|
||||
<div class="dr-slice-section">
|
||||
<p class="control-label">Corpus slices</p>
|
||||
|
||||
+4
-3
@@ -6,10 +6,11 @@ require_once __DIR__ . '/../includes/ToolModels.php';
|
||||
|
||||
dbnToolsRequireMethod('POST');
|
||||
dbnToolsRequireAuth();
|
||||
$ftUid = dbnToolsFreeTierCheck('ask');
|
||||
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
|
||||
$input = dbnToolsJsonInput(25000);
|
||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||
$run = dbnToolsResolveToolRun('ask', $input);
|
||||
$ftUid = $run['ftUid'];
|
||||
$engine = $run['engine'];
|
||||
|
||||
dbnToolsWithChargedTelemetry('ask', $language, $ftUid, function () use ($input, $language, $engine): array {
|
||||
$question = dbnToolsInjectDocContent($input, dbnToolsString($input, 'question', 4000, false));
|
||||
@@ -20,4 +21,4 @@ dbnToolsWithChargedTelemetry('ask', $language, $ftUid, function () use ($input,
|
||||
? trim($input['profile'])
|
||||
: null;
|
||||
return (new DbnLegalToolsService())->ask($question, $language, $engine, $persona);
|
||||
});
|
||||
}, $run['credits'], $run['metadata']);
|
||||
|
||||
+19
-1
@@ -53,7 +53,23 @@ try {
|
||||
|
||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||
$advocateRole = trim((string)($input['advocate_role'] ?? ''));
|
||||
if (isset($input['tier'])) {
|
||||
$run = ToolModels::resolveTier(dbnToolsFreeTierUid(), 'barnevernet', (string)$input['tier']);
|
||||
$engine = $run['engine'];
|
||||
$tierCredits = $run['credits'];
|
||||
$tierMeta = ['tier' => $run['tier'], 'engine' => $engine];
|
||||
if ($ftUid > 0) {
|
||||
$gate = FreeTier::checkAmount($ftUid, 'barnevernet', $tierCredits);
|
||||
if (empty($gate['ok'])) {
|
||||
$emit('error', ['code' => $gate['reason'] ?? 'no_credits', 'message' => 'Insufficient credits for the selected tier.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
|
||||
$tierCredits = null;
|
||||
$tierMeta = [];
|
||||
}
|
||||
$sliceInput = $input['slices'] ?? [];
|
||||
$controls = is_array($input['controls'] ?? null) ? $input['controls'] : [];
|
||||
$additionalNotes = mb_substr(dbnToolsInjectDocContent($input, trim((string)($input['additional_notes'] ?? ''))), 0, 8000, 'UTF-8');
|
||||
@@ -154,7 +170,9 @@ try {
|
||||
'bvj_doc_type' => $result['doc_meta']['doc_type'] ?? null,
|
||||
]);
|
||||
|
||||
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'barnevernet');
|
||||
$ftRemaining = $tierCredits === null
|
||||
? dbnToolsFreeTierDeduct($ftUid, 'barnevernet')
|
||||
: dbnToolsFreeTierDeductAmount($ftUid, 'barnevernet', $tierCredits, $tierMeta);
|
||||
if ($ftRemaining >= 0) {
|
||||
$result['balance'] = $ftRemaining;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,9 @@ try {
|
||||
throw new DbnToolsHttpException('advocate_role is too long.', 422, 'advocate_role_too_long');
|
||||
}
|
||||
$chargeTool = $advocateRole !== '' ? 'advocate' : 'deep-research';
|
||||
$ftUid = dbnToolsFreeTierCheck($chargeTool);
|
||||
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
|
||||
$run = dbnToolsResolveToolRun($chargeTool, $input);
|
||||
$ftUid = $run['ftUid'];
|
||||
$engine = $run['engine'];
|
||||
$priorContext = is_array($input['prior_context'] ?? null) ? $input['prior_context'] : null;
|
||||
$branchNotes = mb_substr(trim((string)($input['branch_notes'] ?? '')), 0, 1000, 'UTF-8');
|
||||
$subQsOverride = is_array($input['sub_questions_override'] ?? null) ? $input['sub_questions_override'] : [];
|
||||
@@ -160,7 +161,9 @@ try {
|
||||
'advocate_role' => $advocateRole !== '' ? $advocateRole : null,
|
||||
]);
|
||||
|
||||
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, $chargeTool);
|
||||
$ftRemaining = $run['credits'] === null
|
||||
? dbnToolsFreeTierDeduct($ftUid, $chargeTool)
|
||||
: dbnToolsFreeTierDeductAmount($ftUid, $chargeTool, $run['credits'], $run['metadata']);
|
||||
if ($ftRemaining >= 0) {
|
||||
$result['balance'] = $ftRemaining;
|
||||
}
|
||||
|
||||
+19
-1
@@ -41,7 +41,23 @@ try {
|
||||
}
|
||||
|
||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||
if (isset($input['tier'])) {
|
||||
$run = ToolModels::resolveTier(dbnToolsFreeTierUid(), 'discrepancy', (string)$input['tier']);
|
||||
$engine = $run['engine'];
|
||||
$tierCredits = $run['credits'];
|
||||
$tierMeta = ['tier' => $run['tier'], 'engine' => $engine];
|
||||
if ($ftUid > 0) {
|
||||
$gate = FreeTier::checkAmount($ftUid, 'discrepancy', $tierCredits);
|
||||
if (empty($gate['ok'])) {
|
||||
$emit('error', ['code' => $gate['reason'] ?? 'no_credits', 'message' => 'Insufficient credits for the selected tier.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
|
||||
$tierCredits = null;
|
||||
$tierMeta = [];
|
||||
}
|
||||
$sliceInput = $input['slices'] ?? [];
|
||||
|
||||
// Extract file A
|
||||
@@ -144,7 +160,9 @@ try {
|
||||
'deployment' => $result['trace_metadata']['deployment'] ?? null,
|
||||
]);
|
||||
|
||||
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'discrepancy');
|
||||
$ftRemaining = $tierCredits === null
|
||||
? dbnToolsFreeTierDeduct($ftUid, 'discrepancy')
|
||||
: dbnToolsFreeTierDeductAmount($ftUid, 'discrepancy', $tierCredits, $tierMeta);
|
||||
if ($ftRemaining >= 0) {
|
||||
$result['balance'] = $ftRemaining;
|
||||
}
|
||||
|
||||
+11
-2
@@ -165,15 +165,24 @@ try {
|
||||
}
|
||||
|
||||
// ── Deduct credit now (Pass 2 starts) ───────────────────────────────────────
|
||||
if (isset($input['tier'])) {
|
||||
$run = dbnToolsResolveToolRun('korrespond', $input);
|
||||
$ftUid = $run['ftUid'];
|
||||
$engine = $run['engine'];
|
||||
} else {
|
||||
$ftUid = dbnToolsFreeTierCheck('korrespond');
|
||||
$engine = ToolModels::engineForUser($ftUid, 'azure_mini');
|
||||
$inputEngine = (string)($input['engine'] ?? '');
|
||||
if (in_array($inputEngine, ['azure_mini', 'claude_sonnet'], true)) {
|
||||
$engine = $inputEngine;
|
||||
}
|
||||
$run = ['credits' => null, 'metadata' => []];
|
||||
}
|
||||
$length = in_array($input['length'] ?? '', ['concise', 'standard', 'detailed'], true)
|
||||
? (string)$input['length'] : 'standard';
|
||||
$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond');
|
||||
$ftRemaining = $run['credits'] === null
|
||||
? dbnToolsFreeTierDeduct($ftUid, 'korrespond')
|
||||
: dbnToolsFreeTierDeductAmount($ftUid, 'korrespond', $run['credits'], $run['metadata']);
|
||||
$creditDeducted = true;
|
||||
|
||||
$personaSlug = (isset($input['profile']) && is_string($input['profile']) && trim($input['profile']) !== '')
|
||||
@@ -212,7 +221,7 @@ try {
|
||||
'case_doc_ids' => $GLOBALS['dbn_last_case_doc_ids'] ?? [],
|
||||
'model' => $engine,
|
||||
'latency_ms' => $result['latency_ms'],
|
||||
'credits_charged' => 1,
|
||||
'credits_charged' => $run['credits'] ?? 1,
|
||||
]);
|
||||
} catch (Throwable) { /* non-critical */ }
|
||||
|
||||
|
||||
+6
-3
@@ -6,11 +6,12 @@ require_once __DIR__ . '/../includes/ToolModels.php';
|
||||
|
||||
dbnToolsRequireMethod('POST');
|
||||
dbnToolsRequireAuth();
|
||||
$ftUid = dbnToolsFreeTierCheck('summarize');
|
||||
|
||||
$input = dbnToolsJsonInput(400000);
|
||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
|
||||
$run = dbnToolsResolveToolRun('summarize', $input);
|
||||
$ftUid = $run['ftUid'];
|
||||
$engine = $run['engine'];
|
||||
$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'])) : [];
|
||||
@@ -73,7 +74,9 @@ try {
|
||||
$result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext, $depth);
|
||||
|
||||
if ($ftUid > 0) {
|
||||
$balance = dbnToolsFreeTierDeduct($ftUid, 'summarize');
|
||||
$balance = $run['credits'] === null
|
||||
? dbnToolsFreeTierDeduct($ftUid, 'summarize')
|
||||
: dbnToolsFreeTierDeductAmount($ftUid, 'summarize', $run['credits'], $run['metadata']);
|
||||
$result['balance'] = $balance;
|
||||
}
|
||||
|
||||
|
||||
+12
-12
@@ -64,7 +64,7 @@
|
||||
roleCustom: document.getElementById('advRoleCustom'),
|
||||
slices: Array.from(document.querySelectorAll('.adv-slice')),
|
||||
langButtons: Array.from(document.querySelectorAll('#advLangSwitcher .lang-btn')),
|
||||
engineRadios: Array.from(document.querySelectorAll('input[name="advEngine"]')),
|
||||
tierRadios: Array.from(document.querySelectorAll('input[name="advTier"]')),
|
||||
subQ: document.getElementById('advSubQ'),
|
||||
subQVal: document.getElementById('advSubQValue'),
|
||||
chunkLimit: document.getElementById('advChunkLimit'),
|
||||
@@ -337,9 +337,9 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function getEngine() {
|
||||
const checked = els.engineRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'azure_mini';
|
||||
function getTier() {
|
||||
const checked = els.tierRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'quick';
|
||||
}
|
||||
|
||||
function getControls() {
|
||||
@@ -371,10 +371,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = getEngine();
|
||||
const expectedDuration = engine === 'azure_full'
|
||||
? '60–180 seconds with Azure gpt-4o'
|
||||
: (engine === 'gpu' ? '30–90 seconds on GPU' : '15–45 seconds with Azure gpt-4o-mini');
|
||||
const tier = getTier();
|
||||
const expectedDuration = tier === 'pro'
|
||||
? '3–5 minutes with Claude Sonnet'
|
||||
: '2–4 minutes with Claude Haiku';
|
||||
|
||||
setStatus(`Building advocate brief for ${advocateRole}… (${expectedDuration})`, 'busy');
|
||||
els.runButton.disabled = true;
|
||||
@@ -387,7 +387,7 @@
|
||||
query,
|
||||
paste_text: '',
|
||||
slices,
|
||||
engine,
|
||||
tier,
|
||||
language: lang,
|
||||
controls: getControls(),
|
||||
advocate_role: advocateRole,
|
||||
@@ -505,7 +505,7 @@
|
||||
els.runButton.disabled = false;
|
||||
renderTrace(finalResult.trace || []);
|
||||
renderResults(finalResult);
|
||||
saveToCache(finalResult, { query, role: advocateRole, engine, slices, lang });
|
||||
saveToCache(finalResult, { query, role: advocateRole, tier, slices, lang });
|
||||
|
||||
function handleStreamEvent(evt) {
|
||||
if (!evt || !evt.event) return;
|
||||
@@ -957,7 +957,7 @@
|
||||
els.input.value = formState.query || '';
|
||||
updateCharCount();
|
||||
if (formState.role) els.roleSelect.value = formState.role;
|
||||
const radio = els.engineRadios.find((r) => r.value === formState.engine);
|
||||
const radio = els.tierRadios.find((r) => r.value === formState.tier);
|
||||
if (radio) radio.checked = true;
|
||||
if (formState.slices) {
|
||||
els.slices.forEach((btn) => {
|
||||
@@ -1074,7 +1074,7 @@
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
language: lang,
|
||||
engine: getEngine(),
|
||||
engine: getTier() === 'pro' ? 'claude_sonnet' : 'claude_haiku',
|
||||
controls: getControls(),
|
||||
advocate_role: advocateRole,
|
||||
}),
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
roleCustom: document.getElementById('bvjRoleCustom'),
|
||||
slices: Array.from(document.querySelectorAll('.adv-slice')),
|
||||
langButtons: Array.from(document.querySelectorAll('#bvjLangSwitcher .lang-btn')),
|
||||
engineRadios: Array.from(document.querySelectorAll('input[name="bvjEngine"]')),
|
||||
tierRadios: Array.from(document.querySelectorAll('input[name="bvjTier"]')),
|
||||
subQ: document.getElementById('bvjSubQ'),
|
||||
subQVal: document.getElementById('bvjSubQValue'),
|
||||
chunkLimit: document.getElementById('bvjChunkLimit'),
|
||||
@@ -186,9 +186,9 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getEngine() {
|
||||
const checked = els.engineRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'azure_mini';
|
||||
function getTier() {
|
||||
const checked = els.tierRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'quick';
|
||||
}
|
||||
|
||||
// ── File upload ────────────────────────────────────────────────────────────
|
||||
@@ -385,13 +385,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = getEngine();
|
||||
const tier = getTier();
|
||||
const additionalNotes = (els.notes ? els.notes.value : '').trim();
|
||||
const expectedDuration = engine === 'azure_full'
|
||||
? '90–180 seconds with Azure gpt-4o'
|
||||
: (engine === 'gpu' ? '45–90 seconds on GPU'
|
||||
: (engine === 'dbn_legal' ? '60–120 seconds with Norwegian specialist'
|
||||
: '30–60 seconds with Azure gpt-4o-mini'));
|
||||
const expectedDuration = tier === 'pro'
|
||||
? '90–180 seconds with Claude Sonnet'
|
||||
: '30–60 seconds with Claude Haiku';
|
||||
|
||||
setStatus(`Analysing document for ${advocateRole}… (${expectedDuration})`, 'busy');
|
||||
els.runButton.disabled = true;
|
||||
@@ -403,7 +401,7 @@
|
||||
|
||||
const payload = {
|
||||
advocate_role: advocateRole,
|
||||
engine,
|
||||
tier,
|
||||
language: lang,
|
||||
slices,
|
||||
controls: getControls(),
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
traceList: document.getElementById('traceList'),
|
||||
slices: Array.from(document.querySelectorAll('.dr-slice')),
|
||||
langButtons: Array.from(document.querySelectorAll('#drLangSwitcher .lang-btn')),
|
||||
engineRadios: Array.from(document.querySelectorAll('input[name="drEngine"]')),
|
||||
tierRadios: Array.from(document.querySelectorAll('input[name="drTier"]')),
|
||||
personaControl: document.getElementById('drPersonaControl'),
|
||||
personaSelect: document.getElementById('drPersonaSelect'),
|
||||
subQ: document.getElementById('drSubQ'),
|
||||
@@ -280,9 +280,9 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function getEngine() {
|
||||
const checked = els.engineRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'azure_mini';
|
||||
function getTier() {
|
||||
const checked = els.tierRadios.find((r) => r.checked);
|
||||
return checked ? checked.value : 'quick';
|
||||
}
|
||||
|
||||
function getControls() {
|
||||
@@ -308,10 +308,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = getEngine();
|
||||
const expectedDuration = engine === 'azure_full'
|
||||
? '60–180 seconds with Azure gpt-4o'
|
||||
: (engine === 'gpu' ? '30–90 seconds on GPU' : '15–45 seconds with Azure gpt-4o-mini');
|
||||
const tier = getTier();
|
||||
const expectedDuration = tier === 'pro'
|
||||
? '60–180 seconds with Claude Sonnet'
|
||||
: '15–45 seconds with Claude Haiku';
|
||||
|
||||
setStatus(`Running deep research… (${expectedDuration})`, 'busy');
|
||||
els.runButton.disabled = true;
|
||||
@@ -325,7 +325,7 @@
|
||||
query,
|
||||
paste_text: '',
|
||||
slices,
|
||||
engine,
|
||||
tier,
|
||||
language: lang,
|
||||
controls: getControls(),
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
results: document.getElementById('dcResults'),
|
||||
traceList: document.getElementById('traceList'),
|
||||
langButtons: Array.from(document.querySelectorAll('#dcLangSwitcher .lang-btn')),
|
||||
engineRadios: Array.from(document.querySelectorAll('input[name="dcEngine"]')),
|
||||
tierRadios: Array.from(document.querySelectorAll('input[name="dcTier"]')),
|
||||
slices: Array.from(document.querySelectorAll('.adv-slice')),
|
||||
// File A
|
||||
zoneA: document.getElementById('dcZoneA'),
|
||||
@@ -188,12 +188,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = (els.engineRadios.find((r) => r.checked) || {}).value || 'azure_mini';
|
||||
const tier = (els.tierRadios.find((r) => r.checked) || {}).value || 'quick';
|
||||
const slices = getSelectedSlices();
|
||||
|
||||
const expectedDuration = engine === 'azure_full' ? '2-3 minutes'
|
||||
: engine === 'gpu' ? '~90 seconds'
|
||||
: '60-90 seconds';
|
||||
const expectedDuration = tier === 'pro' ? '2-3 minutes' : '60-90 seconds';
|
||||
|
||||
setStatus(`Comparing documents… (${expectedDuration})`, 'busy');
|
||||
els.runButton.disabled = true;
|
||||
@@ -203,7 +201,7 @@
|
||||
renderTrace(stepState);
|
||||
|
||||
const payload = {
|
||||
engine, language: lang, slices,
|
||||
tier, language: lang, slices,
|
||||
use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false,
|
||||
};
|
||||
const form = new FormData();
|
||||
|
||||
@@ -280,7 +280,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'),
|
||||
tier: (document.querySelector('[name="korrTier"]:checked')?.value ?? 'quick'),
|
||||
length: (document.querySelector('[name="korrLength"]:checked')?.value ?? 'standard'),
|
||||
};
|
||||
if (korrDocIds.length) payload.doc_ids = korrDocIds;
|
||||
|
||||
@@ -179,13 +179,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var engine = (document.querySelector('input[name="sumEngine"]:checked') || {}).value || 'azure_mini';
|
||||
var tier = (document.querySelector('input[name="sumTier"]:checked') || {}).value || 'quick';
|
||||
var slices = activeSlices();
|
||||
|
||||
var payload = {
|
||||
text: combined,
|
||||
language: _currentLang,
|
||||
engine: engine,
|
||||
tier: tier,
|
||||
depth: (document.querySelector('input[name="sumDepth"]:checked') || {}).value || 'standard',
|
||||
slices: slices,
|
||||
};
|
||||
|
||||
+5
-7
@@ -36,14 +36,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<p class="upload-hint">The agent will analyse the document from <em>your</em> perspective — identifying supporting statutes, procedural red flags, and ECHR arguments for your position.</p>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="bvjEngineControl">
|
||||
<span class="control-label">Engine</span>
|
||||
<label><input type="radio" name="bvjEngine" value="azure_mini" checked> Azure gpt-4o-mini ★ <small class="control-hint">(~30-60s)</small></label>
|
||||
<label><input type="radio" name="bvjEngine" value="azure_full"> Azure gpt-4o <small class="control-hint">(best · ~90-180s)</small></label>
|
||||
<label><input type="radio" name="bvjEngine" value="gpu"> GPU qwen2.5:14b <small class="control-hint">(local · ~45-90s)</small></label>
|
||||
<label><input type="radio" name="bvjEngine" value="dbn_legal_v3"> 🇳🇴⚔️ DBN Legal Agent ★ <small class="control-hint">(dbn-legal-agent-v3 fine-tune · ~20-60s)</small></label>
|
||||
<div class="control-row" id="bvjTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="bvjTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 3 credits)</small></label>
|
||||
<label><input type="radio" name="bvjTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 6 credits)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint">Engine applies to the final advocacy synthesis only. Norwegian specialist v3 is the recommended choice for Barnevernet documents — it is fine-tuned on § 4-25, Strand Lobben, forvaltningsloven § 17/§ 41, and procedural red-flag detection. Classification, party extraction, and timeline always use azure-mini.</p>
|
||||
<p class="upload-hint">Quality applies to the final advocacy synthesis only. Quick uses Claude Haiku 4.5 — fast and accurate for most Barnevernet documents (3 credits). Pro uses Claude Sonnet 4.6 — better at § 4-25 threshold errors, Strand Lobben / forvaltningsloven § 17/§ 41 procedural red flags, and ECHR argumentation (6 credits). Classification, party extraction, and timeline always use the quick engine.</p>
|
||||
|
||||
<div class="dr-slice-section">
|
||||
<p class="control-label">Corpus slices</p>
|
||||
|
||||
+5
-7
@@ -16,14 +16,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="drEngineControl">
|
||||
<span class="control-label">Engine</span>
|
||||
<label><input type="radio" name="drEngine" value="azure_mini" checked> Azure gpt-4o-mini ★ <small class="control-hint">(~15-45s)</small></label>
|
||||
<label><input type="radio" name="drEngine" value="azure_full"> Azure gpt-4o <small class="control-hint">(best · ~60-180s)</small></label>
|
||||
<label><input type="radio" name="drEngine" value="gpu"> GPU (cuttlefish) <small class="control-hint">(local · ~30-90s)</small></label>
|
||||
<label><input type="radio" name="drEngine" value="dbn_legal_v3"> 🇳🇴⚔️ DBN Legal Agent ★ <small class="control-hint">(dbn-legal-agent-v3 fine-tune · ~20-60s)</small></label>
|
||||
<div class="control-row" id="drTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="drTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 6 credits)</small></label>
|
||||
<label><input type="radio" name="drTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 12 credits)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint">Azure mini is the default and finishes fastest. Azure full is the most thorough. Norwegian specialist v3 is a Qwen2.5 fine-tune optimised for barnevernsloven, ECHR, and forvaltningsloven — best for cases involving § 4-25, Strand Lobben, or procedural challenges.</p>
|
||||
<p class="upload-hint">Quick uses Claude Haiku 4.5 — fast and accurate for most research (6 credits). Pro uses Claude Sonnet 4.6 — the most thorough synthesis, best for cases involving § 4-25, Strand Lobben, or procedural challenges (12 credits).</p>
|
||||
|
||||
<div class="control-row corpus-persona is-hidden" id="drPersonaControl">
|
||||
<span class="control-label">Domain</span>
|
||||
|
||||
+5
-7
@@ -55,14 +55,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="dcEngineControl">
|
||||
<span class="control-label">Engine</span>
|
||||
<label><input type="radio" name="dcEngine" value="azure_mini" checked> Azure gpt-4o-mini ★ <small class="control-hint">(~60-90s)</small></label>
|
||||
<label><input type="radio" name="dcEngine" value="azure_full"> Azure gpt-4o <small class="control-hint">(best · ~2-3 min)</small></label>
|
||||
<label><input type="radio" name="dcEngine" value="gpu"> GPU qwen2.5:14b <small class="control-hint">(local · ~90s)</small></label>
|
||||
<label><input type="radio" name="dcEngine" value="dbn_legal_v3"> 🇳🇴⚔️ DBN Legal Agent ★ <small class="control-hint">(dbn-legal-agent-v3 fine-tune · ~30-60s)</small></label>
|
||||
<div class="control-row" id="dcTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="dcTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 4 credits)</small></label>
|
||||
<label><input type="radio" name="dcTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 8 credits)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint">Engine applies to the final synthesis only. Norwegian specialist v3 excels at identifying legally significant discrepancies in Barnevernet documents — procedural violations, threshold errors, and missing statutory justifications. Classification, party extraction, timelines, and cross-referencing always use azure-mini.</p>
|
||||
<p class="upload-hint">Quick uses Claude Haiku 4.5 — fast and accurate for most document comparisons (4 credits). Pro uses Claude Sonnet 4.6 — better at legally significant discrepancies in Barnevernet documents, procedural violations, and threshold errors (8 credits).</p>
|
||||
|
||||
<details class="advanced-panel" id="dcSlicePanel">
|
||||
<summary class="advanced-toggle">Corpus slices <span class="control-hint">(used for legal significance context)</span></summary>
|
||||
|
||||
+47
-3
@@ -1964,7 +1964,14 @@ PROMPT;
|
||||
* Search the shared legal corpus and return top-N passages as a formatted
|
||||
* context string. Returns '' on failure so the caller can degrade gracefully.
|
||||
*/
|
||||
public function corpusContextForSummarize(string $query, int $limit = 8, ?string $persona = null): string
|
||||
public function corpusContextForSummarize(
|
||||
string $query,
|
||||
int $limit = 8,
|
||||
?string $persona = null,
|
||||
int $maxCharsPerPassage = 1800,
|
||||
int $maxTotalChars = 9000,
|
||||
?array &$debug = null
|
||||
): string
|
||||
{
|
||||
try {
|
||||
$client = dbnToolsRequireClient();
|
||||
@@ -1989,14 +1996,51 @@ PROMPT;
|
||||
'search_method' => $searchMethod,
|
||||
'min_private' => 0,
|
||||
'include_beta_website' => true,
|
||||
'reranker_enabled' => true,
|
||||
]));
|
||||
// Bound the injected context: trim each passage and cap the total so a single
|
||||
// oversized chunk cannot eat the budget and starve relevant lower-ranked passages.
|
||||
$parts = [];
|
||||
$total = 0;
|
||||
foreach ($chunks as $c) {
|
||||
$title = (string)($c['title'] ?? ($c['source'] ?? 'Legal source'));
|
||||
$content = (string)($c['content'] ?? ($c['text'] ?? ''));
|
||||
if ($content !== '') {
|
||||
$parts[] = "=== {$title} ===\n{$content}";
|
||||
$rawChars = mb_strlen($content, 'UTF-8');
|
||||
$clean = trim(strip_tags($content));
|
||||
if (mb_strlen($clean, 'UTF-8') > $maxCharsPerPassage) {
|
||||
$clean = rtrim(mb_substr($clean, 0, $maxCharsPerPassage - 1, 'UTF-8')) . '…';
|
||||
}
|
||||
$keptChars = $clean === '' ? 0 : mb_strlen($clean, 'UTF-8');
|
||||
$included = false;
|
||||
if ($clean !== '' && ($total + $keptChars) <= $maxTotalChars) {
|
||||
$parts[] = "=== {$title} ===\n{$clean}";
|
||||
$total += $keptChars;
|
||||
$included = true;
|
||||
}
|
||||
if ($debug !== null) {
|
||||
$debug['chunks'][] = [
|
||||
'title' => $title,
|
||||
'source_name' => $c['source_name'] ?? null,
|
||||
'source_type' => $c['source_type'] ?? null,
|
||||
'source_group' => $c['source_group'] ?? ($c['meta']['source_group'] ?? null),
|
||||
'category' => $c['category'] ?? null,
|
||||
'similarity' => $c['similarity'] ?? null,
|
||||
'reranker_score' => $c['reranker_score'] ?? null,
|
||||
'raw_chars' => $rawChars,
|
||||
'kept_chars' => $included ? $keptChars : 0,
|
||||
'included' => $included,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($debug !== null) {
|
||||
$debug['raw_total'] = array_sum(array_column($debug['chunks'] ?? [], 'raw_chars'));
|
||||
$debug['used_total'] = $total;
|
||||
$debug['chunk_count'] = count($chunks);
|
||||
$debug['search_method'] = $searchMethod;
|
||||
$debug['reranked'] = !empty(array_filter(
|
||||
$debug['chunks'] ?? [],
|
||||
static fn($r) => $r['reranker_score'] !== null
|
||||
));
|
||||
}
|
||||
return implode("\n\n", $parts);
|
||||
} catch (Throwable $e) {
|
||||
|
||||
@@ -19,6 +19,114 @@ final class ToolModels
|
||||
public const TIMELINE_STANDARD_MAX_CHARS = 300000;
|
||||
public const TIMELINE_DEEP_MAX_CHARS = 600000;
|
||||
|
||||
/**
|
||||
* Canonical redact engine registry. Server-side source of truth for which engines
|
||||
* exist, their credit cost, and whether the client UI may offer them. Credits are
|
||||
* ALWAYS resolved from here — never from a client-supplied value.
|
||||
*
|
||||
* gpu_legal is the fine-tuned legal model (Phase 6 roadmap) — wired but not yet
|
||||
* client-selectable until the harness proves a lift.
|
||||
*/
|
||||
public const REDACT_ENGINES = [
|
||||
'azure_mini' => ['label' => 'Azure GPT-4o-mini', 'credits' => 1, 'client_selectable' => true, 'speed' => 'fast'],
|
||||
'azure_full' => ['label' => 'Azure GPT-4o', 'credits' => 2, 'client_selectable' => true, 'speed' => 'medium'],
|
||||
'claude_haiku' => ['label' => 'Claude Haiku 4.5', 'credits' => 1, 'client_selectable' => true, 'speed' => 'fast'],
|
||||
'claude_sonnet' => ['label' => 'Claude Sonnet 4.6', 'credits' => 2, 'client_selectable' => true, 'speed' => 'medium'],
|
||||
'gpu' => ['label' => 'GPU Qwen 2.5 14B', 'credits' => 1, 'client_selectable' => true, 'speed' => 'slow'],
|
||||
'gpu_legal' => ['label' => 'GPU Legal (Qwen FT)','credits' => 1, 'client_selectable' => false, 'speed' => 'slow'],
|
||||
'regex' => ['label' => 'Regex only', 'credits' => 0, 'client_selectable' => false, 'speed' => 'instant'],
|
||||
];
|
||||
|
||||
public static function isValidRedactEngine(string $engine): bool
|
||||
{
|
||||
return isset(self::REDACT_ENGINES[$engine]);
|
||||
}
|
||||
|
||||
/** Normalise an incoming engine to a valid key, defaulting to claude_haiku. */
|
||||
public static function redactEngine(string $engine): string
|
||||
{
|
||||
return self::isValidRedactEngine($engine) ? $engine : 'claude_haiku';
|
||||
}
|
||||
|
||||
/** Server-side credit cost for a redact engine (never trust the client). */
|
||||
public static function redactCredits(string $engine): int
|
||||
{
|
||||
return self::REDACT_ENGINES[self::redactEngine($engine)]['credits'];
|
||||
}
|
||||
|
||||
/** Engine keys the client UI is allowed to offer. */
|
||||
public static function redactSelectableEngines(): array
|
||||
{
|
||||
return array_keys(array_filter(
|
||||
self::REDACT_ENGINES,
|
||||
static fn(array $e) => $e['client_selectable']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified quality-tier registry for the Quick/Pro selector exposed on the analytical
|
||||
* tools (summarize, ask, legal-analysis, barnevernet, korrespond, discrepancy,
|
||||
* deep-research). Each tier maps to an existing engine string that the service layer
|
||||
* already resolves to a gateway (resolveChatGateway/personaGateway) — so no service
|
||||
* change is needed. Credits = the tool's base cost × tier multiplier, resolved
|
||||
* server-side ONLY.
|
||||
*
|
||||
* Anchors picked from the 2026-06-15 tier benchmark (credits-first, Bedrock):
|
||||
* quick → claude_haiku (96.7% quality, ~3.8s); pro → claude_sonnet (100%, ~13s).
|
||||
*/
|
||||
public const QUALITY_TIERS = [
|
||||
'quick' => ['label' => 'Quick', 'engine' => 'claude_haiku', 'credit_mult' => 1],
|
||||
'pro' => ['label' => 'Pro', 'engine' => 'claude_sonnet', 'credit_mult' => 2],
|
||||
];
|
||||
|
||||
/** Normalise an incoming tier to a valid key, defaulting to quick. */
|
||||
public static function qualityTier(string $tier): string
|
||||
{
|
||||
return isset(self::QUALITY_TIERS[$tier]) ? $tier : 'quick';
|
||||
}
|
||||
|
||||
/** The existing engine string backing a quality tier. */
|
||||
public static function tierEngine(string $tier): string
|
||||
{
|
||||
return self::QUALITY_TIERS[self::qualityTier($tier)]['engine'];
|
||||
}
|
||||
|
||||
/** Human label for a quality tier (UI / telemetry). */
|
||||
public static function tierLabel(string $tier): string
|
||||
{
|
||||
return self::QUALITY_TIERS[self::qualityTier($tier)]['label'];
|
||||
}
|
||||
|
||||
/** Server-side credit cost for a tool at a given tier (base cost × tier multiplier). */
|
||||
public static function tierCredits(string $tool, string $tier): int
|
||||
{
|
||||
$mult = self::QUALITY_TIERS[self::qualityTier($tier)]['credit_mult'];
|
||||
return max(1, PricingCatalog::toolCost($tool) * $mult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a requested quality tier for a user + tool. Applies the subscription gate
|
||||
* (free / anonymous → quick only; plus & pro may pick pro) and returns the backing
|
||||
* engine plus the server-side credit cost. Credits are NEVER trusted from the client.
|
||||
*
|
||||
* @return array{tier:string,engine:string,credits:int,label:string}
|
||||
*/
|
||||
public static function resolveTier(int $userId, string $tool, string $requestedTier): array
|
||||
{
|
||||
$tier = self::qualityTier($requestedTier);
|
||||
|
||||
if ($tier === 'pro' && $userId > 0 && FreeTier::tier($userId) === 'free') {
|
||||
$tier = 'quick';
|
||||
}
|
||||
|
||||
return [
|
||||
'tier' => $tier,
|
||||
'engine' => self::tierEngine($tier),
|
||||
'credits' => self::tierCredits($tool, $tier),
|
||||
'label' => self::tierLabel($tier),
|
||||
];
|
||||
}
|
||||
|
||||
public static function engineForUser(int $userId, string $requestedEngine): string
|
||||
{
|
||||
$valid = ['nova_lite', 'azure_mini', 'azure_full', 'gpu', 'regex', 'claude_haiku', 'claude_sonnet'];
|
||||
|
||||
@@ -767,6 +767,50 @@ function dbnToolsIsFreeTier(): bool
|
||||
&& !empty($_SESSION['dbn_tools_sso_uid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current free-tier SSO user id, or 0 for CaveauAI / non-SSO sessions.
|
||||
* Lets an endpoint resolve a quality tier (subscription gate) BEFORE charging,
|
||||
* without reaching into the session directly. Mirrors the uid the check/deduct
|
||||
* helpers use.
|
||||
*/
|
||||
function dbnToolsFreeTierUid(): int
|
||||
{
|
||||
return dbnToolsIsFreeTier() ? (int)$_SESSION['dbn_tools_sso_uid'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the model + credit gate for a tool run. When the request carries a `tier`
|
||||
* param, honour the Quick/Pro quality tier (subscription-gated, server-priced); otherwise
|
||||
* fall back to the legacy engine-selector behaviour. Runs the credit gate (exits 402/429
|
||||
* if over limit) and returns the context an endpoint needs to run + deduct.
|
||||
*
|
||||
* @return array{tier:string,engine:string,credits:?int,ftUid:int,metadata:array}
|
||||
*/
|
||||
function dbnToolsResolveToolRun(string $tool, array $input, string $legacyDefaultEngine = 'azure_mini'): array
|
||||
{
|
||||
if (isset($input['tier'])) {
|
||||
$res = ToolModels::resolveTier(dbnToolsFreeTierUid(), $tool, (string)$input['tier']);
|
||||
$ftUid = dbnToolsFreeTierCheckAmount($tool, $res['credits']);
|
||||
return [
|
||||
'tier' => $res['tier'],
|
||||
'engine' => $res['engine'],
|
||||
'credits' => $res['credits'],
|
||||
'ftUid' => $ftUid,
|
||||
'metadata' => ['tier' => $res['tier'], 'engine' => $res['engine']],
|
||||
];
|
||||
}
|
||||
|
||||
$ftUid = dbnToolsFreeTierCheck($tool);
|
||||
$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? $legacyDefaultEngine));
|
||||
return [
|
||||
'tier' => '',
|
||||
'engine' => $engine,
|
||||
'credits' => null,
|
||||
'ftUid' => $ftUid,
|
||||
'metadata' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce credit + tier gate before a tool call.
|
||||
* Exits with JSON 402/429 if the user is over limit or out of credits.
|
||||
|
||||
+6
-6
@@ -79,13 +79,13 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
</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>
|
||||
<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>
|
||||
<!-- Quality tier -->
|
||||
<div class="control-row" id="korrTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="korrTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 3 credits)</small></label>
|
||||
<label><input type="radio" name="korrTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 6 credits)</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>
|
||||
<p class="upload-hint">Quick uses Claude Haiku 4.5 for drafting — fast and solid for standard correspondence (3 credits). Pro uses Claude Sonnet 4.6 — better at multi-statute cases, complex appeal grounds, and ECHR framing (6 credits).</p>
|
||||
|
||||
<!-- Domain persona -->
|
||||
<div class="control-row corpus-persona is-hidden" id="korrPersonaControl">
|
||||
|
||||
+5
-7
@@ -16,14 +16,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<button type="button" class="lang-btn sum-lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="sumEngineControl">
|
||||
<span class="control-label">Engine</span>
|
||||
<label><input type="radio" name="sumEngine" value="claude_haiku" checked> Standard ★ <small class="control-hint">(Claude Haiku · fast)</small></label>
|
||||
<label><input type="radio" name="sumEngine" value="claude_sonnet"> Deep <small class="control-hint">(Claude Sonnet · best)</small></label>
|
||||
<label><input type="radio" name="sumEngine" value="azure_mini"> Azure mini <small class="control-hint">(gpt-4o-mini)</small></label>
|
||||
<label><input type="radio" name="sumEngine" value="azure_full"> Azure full <small class="control-hint">(gpt-4o)</small></label>
|
||||
<div class="control-row" id="sumTierControl">
|
||||
<span class="control-label">Quality</span>
|
||||
<label><input type="radio" name="sumTier" value="quick" checked> Quick ★ <small class="control-hint">(Claude Haiku · fast · 1 credit)</small></label>
|
||||
<label><input type="radio" name="sumTier" value="pro"> Pro <small class="control-hint">(Claude Sonnet · best · 2 credits)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint">Standard uses Claude Haiku 4.5 — fast and highly accurate. Deep uses Claude Sonnet 4.6 — best for complex multi-statute documents.</p>
|
||||
<p class="upload-hint">Quick uses Claude Haiku 4.5 — fast and highly accurate (1 credit). Pro uses Claude Sonnet 4.6 — best for complex multi-statute documents (2 credits).</p>
|
||||
|
||||
<div class="control-row" id="sumDepthControl">
|
||||
<span class="control-label">Summary depth</span>
|
||||
|
||||
Reference in New Issue
Block a user