personas: elevate to server with per-persona fact scoping
Move personas off the mobile client into ImageApi as first-class records, and scope entity_facts by persona so each one builds its own voice over a shared entity graph. The new include_all_memories flag lets a persona opt back into the full hive-mind pool for human browsing of /knowledge/*; agentic generation always stays in-voice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,10 @@ pub struct ChatTurnRequest {
|
||||
/// In amend mode, persisted into the new insight row's system message.
|
||||
/// None / empty = no change.
|
||||
pub system_prompt: Option<String>,
|
||||
/// Active persona id for this turn. Tools that write to
|
||||
/// `entity_facts` tag the new rows with it; `recall_facts_for_photo`
|
||||
/// scopes its read to it. None defaults to `"default"`.
|
||||
pub persona_id: Option<String>,
|
||||
/// When true, write a new insight row (regenerating title) instead of
|
||||
/// updating training_messages on the existing row.
|
||||
pub amend: bool,
|
||||
@@ -231,6 +235,13 @@ impl InsightChatService {
|
||||
bail!("user_message exceeds 8192 chars");
|
||||
}
|
||||
|
||||
let active_persona = req
|
||||
.persona_id
|
||||
.clone()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
span.set_attribute(KeyValue::new("persona_id", active_persona.clone()));
|
||||
|
||||
let normalized = normalize_path(&req.file_path);
|
||||
|
||||
// 1. Acquire the per-(library, file) async mutex. Two concurrent
|
||||
@@ -464,6 +475,7 @@ impl InsightChatService {
|
||||
&ollama_client,
|
||||
&image_base64,
|
||||
&normalized,
|
||||
&active_persona,
|
||||
&loop_cx,
|
||||
)
|
||||
.await;
|
||||
@@ -737,6 +749,11 @@ impl InsightChatService {
|
||||
insight: crate::database::models::PhotoInsight,
|
||||
tx: tokio::sync::mpsc::Sender<ChatStreamEvent>,
|
||||
) -> Result<()> {
|
||||
let active_persona = req
|
||||
.persona_id
|
||||
.clone()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
let raw_history = insight.training_messages.as_ref().ok_or_else(|| {
|
||||
anyhow!("insight has no chat history; regenerate this insight in agentic mode")
|
||||
})?;
|
||||
@@ -826,6 +843,7 @@ impl InsightChatService {
|
||||
tools,
|
||||
&image_base64,
|
||||
&normalized,
|
||||
&active_persona,
|
||||
max_iterations,
|
||||
&tx,
|
||||
)
|
||||
@@ -915,6 +933,11 @@ impl InsightChatService {
|
||||
normalized: String,
|
||||
tx: tokio::sync::mpsc::Sender<ChatStreamEvent>,
|
||||
) -> Result<()> {
|
||||
let active_persona = req
|
||||
.persona_id
|
||||
.clone()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
let effective_backend = resolve_bootstrap_backend(req.backend.as_deref())?;
|
||||
let is_hybrid = effective_backend == "hybrid";
|
||||
|
||||
@@ -1008,6 +1031,7 @@ impl InsightChatService {
|
||||
tools,
|
||||
&image_base64,
|
||||
&normalized,
|
||||
&active_persona,
|
||||
max_iterations,
|
||||
&tx,
|
||||
)
|
||||
@@ -1157,6 +1181,7 @@ impl InsightChatService {
|
||||
tools: Vec<Tool>,
|
||||
image_base64: &Option<String>,
|
||||
normalized: &str,
|
||||
active_persona: &str,
|
||||
max_iterations: usize,
|
||||
tx: &tokio::sync::mpsc::Sender<ChatStreamEvent>,
|
||||
) -> Result<AgenticLoopOutcome> {
|
||||
@@ -1235,6 +1260,7 @@ impl InsightChatService {
|
||||
ollama_client,
|
||||
image_base64,
|
||||
normalized,
|
||||
active_persona,
|
||||
&cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user