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
+21 -3
View File
@@ -12,31 +12,37 @@ final class DbnMcpRuntime
$lang = self::langSchema();
$text = ['type' => 'string', 'description' => 'Text to process.'];
$useCase = ['type' => 'boolean', 'description' => 'Use private My Case context. Defaults to false.'];
$persona = ['type' => 'string', 'description' => 'Legal persona/profile slug that scopes the corpus and answer style: family, child-welfare, immigration, labour, consumer-tenancy, or general. Defaults to family. Call dbn.list_personas for the live list.'];
return [
self::tool('dbn.search_legal', 'Search DBN legal corpus', 'Search the DBN Norwegian family-law corpus.', [
self::tool('dbn.search_legal', 'Search DBN legal corpus', 'Search the DBN Norwegian legal corpus, scoped by the chosen legal persona (family by default).', [
'query' => ['type' => 'string', 'minLength' => 3],
'language' => $lang,
'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 10],
'corpus_scope' => ['type' => 'string', 'enum' => ['shared', 'private', 'both']],
'profile' => $persona,
], ['query']),
self::tool('dbn.corpus_search', 'Advanced corpus search', 'Search the DBN legal corpus with a chosen retrieval mode (hybrid, bm25, vector, azure) and optional category filter.', [
self::tool('dbn.corpus_search', 'Advanced corpus search', 'Search the DBN legal corpus with a chosen retrieval mode (hybrid, bm25, vector, azure) and optional category filter. The persona (profile) scopes the hybrid mode.', [
'query' => ['type' => 'string', 'minLength' => 3],
'language' => $lang,
'mode' => ['type' => 'string', 'enum' => ['hybrid', 'bm25', 'vector', 'azure']],
'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 20],
'category' => ['type' => 'string'],
'profile' => $persona,
], ['query']),
self::tool('dbn.ask', 'Ask a legal question', 'Answer a legal preparation question with source-grounded DBN context.', [
self::tool('dbn.ask', 'Ask a legal question', 'Answer a legal preparation question with source-grounded DBN context, using the chosen legal persona (model + corpus + style).', [
'question' => ['type' => 'string', 'minLength' => 5],
'language' => $lang,
'use_case_context' => $useCase,
'profile' => $persona,
], ['question']),
self::tool('dbn.list_personas', 'List legal personas', 'List the available DBN legal personas (saved caveauAI agent profiles). Pass a returned slug as the `profile` argument to search/ask tools.', []),
self::tool('dbn.summarize', 'Summarize document', 'Summarize pasted case text with optional legal-corpus enrichment.', [
'text' => $text,
'language' => $lang,
'use_legal_corpus' => ['type' => 'boolean'],
'use_case_context' => $useCase,
'profile' => $persona,
], ['text']),
self::tool('dbn.timeline', 'Extract timeline', 'Extract dates, hearings, milestones, and deadlines from case text.', [
'text' => $text,
@@ -162,6 +168,7 @@ final class DbnMcpRuntime
'language' => self::language($args['language'] ?? 'en'),
'limit' => (int)($args['limit'] ?? $args['top_k'] ?? 8),
'corpus_scope' => self::corpusScope($args['corpus_scope'] ?? 'both'),
'profile' => self::persona($args['profile'] ?? null),
]),
'dbn.corpus_search' => self::callJson('api/corpus-search.php', [
'query' => (string)($args['query'] ?? ''),
@@ -169,17 +176,21 @@ final class DbnMcpRuntime
'mode' => in_array($args['mode'] ?? 'hybrid', ['hybrid', 'bm25', 'vector', 'azure'], true) ? (string)$args['mode'] : 'hybrid',
'limit' => (int)($args['limit'] ?? 8),
'category' => (string)($args['category'] ?? ''),
'profile' => self::persona($args['profile'] ?? null),
]),
'dbn.ask' => self::callJson('api/ask.php', [
'question' => (string)($args['question'] ?? ''),
'language' => self::language($args['language'] ?? 'en'),
'use_my_case' => !empty($args['use_case_context']),
'profile' => self::persona($args['profile'] ?? null),
]),
'dbn.list_personas' => self::callGet('api/personas.php', []),
'dbn.summarize' => self::callJson('api/summarize.php', [
'text' => (string)($args['text'] ?? ''),
'language' => self::language($args['language'] ?? 'en'),
'slices' => !empty($args['use_legal_corpus']) ? ['family-legal'] : [],
'use_my_case' => !empty($args['use_case_context']),
'profile' => self::persona($args['profile'] ?? null),
]),
'dbn.timeline' => self::callJson('api/timeline.php', [
'text' => (string)($args['text'] ?? ''),
@@ -682,6 +693,13 @@ final class DbnMcpRuntime
return in_array($value, ['shared', 'private', 'both'], true) ? (string)$value : 'both';
}
/** Normalize a persona/profile slug ('' when absent → endpoint applies the default). */
private static function persona(mixed $value): string
{
$slug = is_string($value) ? trim($value) : '';
return preg_match('/^[a-z0-9-]{1,64}$/', $slug) === 1 ? $slug : '';
}
private static function docType(mixed $value): string
{
$value = (string)$value;