feat(tools): persona-driven multi-domain corpus + model routing

Generalize the family-locked legal tools into caveauAI persona profiles
(client 57 chat profiles, resolved in-process via the chat_profiles bridge).
Each tool accepts an optional `profile` slug that scopes the corpus package(s),
search method, system prompt and synthesis model; omitting it falls back to the
family-legal package so existing behaviour is unchanged.

- dbnToolsResolvePersona / dbnToolsListPersonas / dbnToolsBootChatProfiles in
  bootstrap.php; new api/personas.php + dbn.list_personas MCP tool.
- LegalTools search/ask/corpusContextForSummarize and the BvjAnalyzer /
  LegalAnalysis / translate paths take the persona's packages + prompt + model.
- Persona <select> on ask/search/summarize (populated from api/personas.php).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:49:58 +02:00
parent 5a0ef89dca
commit 662fbf7d6d
16 changed files with 404 additions and 58 deletions
+52
View File
@@ -894,6 +894,7 @@ const tools = {
payloadKey: 'question',
placeholder: 'Example: What evidence is needed before asking for changes in custody arrangements?',
usesLanguage: true,
usesPersona: true,
badge: 'family-legal',
},
search: {
@@ -904,6 +905,7 @@ const tools = {
payloadKey: 'query',
placeholder: 'Example: barnets beste samvær foreldreansvar',
usesLanguage: true,
usesPersona: true,
badge: 'family-legal',
},
summarize: {
@@ -914,6 +916,7 @@ const tools = {
payloadKey: 'text',
placeholder: 'Paste a case note, letter, or excerpt.',
usesLanguage: true,
usesPersona: true,
badge: 'process-and-forget',
},
timeline: {
@@ -980,6 +983,8 @@ document.addEventListener('DOMContentLoaded', () => {
uploadClear: document.querySelector('#uploadClear'),
aliasSection: document.querySelector('#aliasSection'),
corpusScopeControl: document.querySelector('#corpusScopeControl'),
personaControl: document.querySelector('#personaControl'),
personaSelect: document.querySelector('#personaSelect'),
addAliasRow: document.querySelector('#addAliasRow'),
aliasRows: document.querySelector('#aliasRows'),
audioZone: document.querySelector('#audioZone'),
@@ -1057,6 +1062,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (state.authenticated) {
checkHealth();
if (els.personaSelect) loadPersonas();
} else {
els.loginEmail?.focus();
}
@@ -1084,6 +1090,7 @@ function setTool(toolName) {
els.input.placeholder = tool.placeholder;
}
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
els.personaControl?.classList.toggle('is-hidden', !tool.usesPersona || !personaState.options.length);
els.corpusScopeControl?.classList.toggle('is-hidden', toolName !== 'search');
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline');
@@ -1154,6 +1161,10 @@ async function runTool(event) {
if (tool.usesLanguage) {
payload.language = currentLanguage();
}
if (tool.usesPersona) {
const profile = currentPersona();
if (profile) payload.profile = profile;
}
if (state.activeTool === 'search') {
payload.limit = 7;
payload.corpus_scope = currentCorpusScope();
@@ -1530,6 +1541,47 @@ function currentCorpusScope() {
return document.querySelector('input[name="corpusScope"]:checked')?.value || 'both';
}
const personaState = { options: [], default: 'family' };
function currentPersona() {
return els.personaSelect?.value || personaState.default || '';
}
async function loadPersonas() {
if (!els.personaSelect) return;
try {
const response = await fetch('api/personas.php', {
method: 'GET',
headers: { Accept: 'application/json' },
credentials: 'same-origin',
});
const data = await response.json().catch(() => ({}));
if (!response.ok || !data || data.ok !== true || !Array.isArray(data.personas) || !data.personas.length) return;
personaState.options = data.personas;
personaState.default = data.default_persona || 'family';
const saved = sessionStorage.getItem('dbnPersona');
els.personaSelect.innerHTML = '';
for (const p of data.personas) {
const opt = document.createElement('option');
opt.value = p.slug;
opt.textContent = p.name || p.slug;
els.personaSelect.appendChild(opt);
}
const initial = (saved && data.personas.some((p) => p.slug === saved))
? saved
: (data.personas.some((p) => p.slug === personaState.default) ? personaState.default : data.personas[0].slug);
els.personaSelect.value = initial;
els.personaSelect.addEventListener('change', () => {
sessionStorage.setItem('dbnPersona', els.personaSelect.value);
});
if (tools[state.activeTool]?.usesPersona) {
els.personaControl?.classList.remove('is-hidden');
}
} catch (_) {
// Personas are optional UI sugar; ignore failures (e.g. pre-seed environments).
}
}
function currentRedactionMode() {
return document.querySelector('input[name="redactionMode"]:checked')?.value || 'standard';
}