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:
+144
-2
@@ -448,6 +448,144 @@ function dbnToolsBootCaveau(): void
|
||||
$booted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load caveauAI's chat-profile resolver (the persona bridge). DBN reads client 57's
|
||||
* saved chat profiles in-process to drive tools by persona instead of hardcoding
|
||||
* family law. Returns false when the platform file or resolver functions are absent
|
||||
* (callers then fall back to the family-legal package).
|
||||
*/
|
||||
function dbnToolsBootChatProfiles(): bool
|
||||
{
|
||||
static $loaded = null;
|
||||
if ($loaded !== null) {
|
||||
return $loaded;
|
||||
}
|
||||
dbnToolsBootCaveau(); // ensures db.php + platform autoload context
|
||||
$root = dbnToolsAiPortalRoot();
|
||||
$cpFile = $root . DIRECTORY_SEPARATOR . 'platform' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'chat_profiles.php';
|
||||
if (!is_file($cpFile)) {
|
||||
$loaded = false;
|
||||
return false;
|
||||
}
|
||||
require_once $cpFile;
|
||||
$loaded = function_exists('cpFetchProfileForUser') && function_exists('cpResolveRuntime');
|
||||
return $loaded;
|
||||
}
|
||||
|
||||
/** Default persona slug (back-compat: family). Override via .env DBN_DEFAULT_PERSONA. */
|
||||
function dbnToolsDefaultPersonaSlug(): string
|
||||
{
|
||||
$v = trim((string)(dbnToolsEnv('DBN_DEFAULT_PERSONA', 'family') ?? 'family'));
|
||||
return $v !== '' ? $v : 'family';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a DBN persona (a caveauAI chat profile for client 57) into a normalized
|
||||
* runtime bundle the tools can act on:
|
||||
* ['slug','name','package_ids','package','system_prompt','model','search_method',
|
||||
* 'chunk_limit','rag_opts','source']
|
||||
*
|
||||
* - source='chat_profile' when a seeded persona was found and resolved.
|
||||
* - source='fallback' when no persona exists yet (pre-seed): the legacy family-legal
|
||||
* package is used so behaviour is unchanged.
|
||||
* model is null when the persona doesn't pin one (caller keeps its default routing).
|
||||
*/
|
||||
function dbnToolsResolvePersona(int $clientId, ?string $slug = null): array
|
||||
{
|
||||
$slug = trim((string)($slug ?? '')) ?: dbnToolsDefaultPersonaSlug();
|
||||
$resolved = null;
|
||||
|
||||
if (dbnToolsBootChatProfiles()) {
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
$user = ['client_id' => $clientId];
|
||||
$row = cpFetchProfileForUser($db, $user, $slug);
|
||||
if ($row) {
|
||||
$runtime = cpResolveRuntime($db, $row, []);
|
||||
$packageIds = array_values(array_filter(
|
||||
array_map('intval', $runtime['package_ids'] ?? []),
|
||||
static fn(int $id): bool => $id > 0
|
||||
));
|
||||
|
||||
$package = null;
|
||||
if ($packageIds) {
|
||||
$pkgStmt = $db->prepare('SELECT * FROM corpus_packages WHERE id = ? LIMIT 1');
|
||||
$pkgStmt->execute([$packageIds[0]]);
|
||||
$package = $pkgStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
$chunkLimit = (int)($runtime['chunk_limit'] ?? 8);
|
||||
$ragOpts = (is_array($runtime['retrieval_tuning'] ?? null) && function_exists('cpRetrievalTuningToRagOptions'))
|
||||
? cpRetrievalTuningToRagOptions($runtime['retrieval_tuning'], $chunkLimit)
|
||||
: [];
|
||||
|
||||
$resolved = [
|
||||
'slug' => $slug,
|
||||
'name' => (string)$row['name'],
|
||||
'package_ids' => $packageIds,
|
||||
'package' => $package,
|
||||
'system_prompt' => ($runtime['system_prompt'] ?? '') !== '' ? (string)$runtime['system_prompt'] : null,
|
||||
'model' => trim((string)($runtime['model'] ?? '')) ?: null,
|
||||
'search_method' => (string)($runtime['search_method'] ?? 'keyword'),
|
||||
'chunk_limit' => $chunkLimit,
|
||||
'rag_opts' => $ragOpts,
|
||||
'source' => 'chat_profile',
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[dbn-persona] resolve failed for slug ' . $slug . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolved === null) {
|
||||
// Pre-seed / missing persona → legacy family-legal package (unchanged behaviour).
|
||||
$package = dbnToolsFetchPackage('family-legal');
|
||||
if (!$package || empty($package['is_active'])) {
|
||||
dbnToolsAbort('No persona profile and the family-legal corpus package is not active.', 503, 'package_unavailable');
|
||||
}
|
||||
if (!dbnToolsHasActiveSubscription($clientId, (int)$package['id'])) {
|
||||
dbnToolsAbort('Do Better Norge does not have an active corpus subscription.', 503, 'subscription_missing');
|
||||
}
|
||||
$resolved = [
|
||||
'slug' => 'family',
|
||||
'name' => 'Family Law',
|
||||
'package_ids' => [(int)$package['id']],
|
||||
'package' => $package,
|
||||
'system_prompt' => null,
|
||||
'model' => null,
|
||||
'search_method' => 'keyword',
|
||||
'chunk_limit' => 8,
|
||||
'rag_opts' => [],
|
||||
'source' => 'fallback',
|
||||
];
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/** List enabled DBN personas (client 57 chat profiles) for the persona picker / dbn.list_personas. */
|
||||
function dbnToolsListPersonas(int $clientId): array
|
||||
{
|
||||
if (!dbnToolsBootChatProfiles()) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
$rows = cpFetchProfilesForUser($db, ['client_id' => $clientId], false);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[dbn-persona] list failed: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
return array_map(static function (array $r): array {
|
||||
return [
|
||||
'slug' => (string)$r['slug'],
|
||||
'name' => (string)$r['name'],
|
||||
'description' => (string)($r['description'] ?? ''),
|
||||
'model' => (string)($r['model'] ?? ''),
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve (or lazily provision) the dashboard tenant for the current session.
|
||||
*
|
||||
@@ -1166,7 +1304,7 @@ function dbnToolsLiteLLMEmbedBatch(array $texts, string $model = 'nomic-embed-te
|
||||
// Strategy: ask ONE targeted question per document type, cap tokens at 350 to cut
|
||||
// off before the tool-planning loop kicks in, parse the answer for legal findings.
|
||||
|
||||
function dbnToolsRunLegalCheck(string $brief, string $docType): array
|
||||
function dbnToolsRunLegalCheck(string $brief, string $docType, ?string $personaPrompt = null): array
|
||||
{
|
||||
$question = dbnToolsSelectCheckQuestion($docType, $brief);
|
||||
if ($question === null) {
|
||||
@@ -1181,7 +1319,11 @@ function dbnToolsRunLegalCheck(string $brief, string $docType): array
|
||||
// No 'json' key — plain narrative, no response_format flag
|
||||
];
|
||||
|
||||
$sysMsg = 'Du er en ekspert på norsk barnevernsloven og EMD-praksis. Svar alltid på norsk med korrekt juridisk terminologi. Bruk terskler fra barnevernsloven 2021: § 4-25 krever «klar nødvendighet». Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. Aldri oppfinn paragrafnumre, saksnumre eller dommernavn.';
|
||||
// Base prompt from the resolved persona (default = Child-welfare/barnevern).
|
||||
$personaPrompt = is_string($personaPrompt) ? trim($personaPrompt) : '';
|
||||
$sysMsg = $personaPrompt !== ''
|
||||
? $personaPrompt
|
||||
: 'Du er en ekspert på norsk barnevernsloven og EMD-praksis. Svar alltid på norsk med korrekt juridisk terminologi. Bruk terskler fra barnevernsloven 2021: § 4-25 krever «klar nødvendighet». Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. Aldri oppfinn paragrafnumre, saksnumre eller dommernavn.';
|
||||
|
||||
// Prepend a short snippet of the actual synthesis text so v3 answers in context,
|
||||
// not just as a general law quiz. Strip HTML tags and cap at 350 chars.
|
||||
|
||||
Reference in New Issue
Block a user