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:
Cameron Cordes
2026-05-09 17:59:20 -04:00
parent 55a986c249
commit 3e2f36a748
15 changed files with 1024 additions and 20 deletions

View File

@@ -1534,7 +1534,15 @@ Return ONLY the summary, nothing else."#,
// ── Tool executors for agentic loop ────────────────────────────────
/// Dispatch a tool call to the appropriate executor
/// Dispatch a tool call to the appropriate executor.
///
/// `persona_id` identifies the persona this loop is generating for —
/// `store_fact` tags new facts with it, `recall_facts_for_photo`
/// filters reads to it (always Single in the agentic loop, even when
/// the persona has `include_all_memories=true`; the hive-mind toggle
/// is for human browsing of `/knowledge/*`, where mixing voices is
/// the explicit goal — during generation the persona's own voice
/// must stay clean).
pub(crate) async fn execute_tool(
&self,
tool_name: &str,
@@ -1542,6 +1550,7 @@ Return ONLY the summary, nothing else."#,
ollama: &OllamaClient,
image_base64: &Option<String>,
file_path: &str,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
let result = match tool_name {
@@ -1556,9 +1565,15 @@ Return ONLY the summary, nothing else."#,
"reverse_geocode" => self.tool_reverse_geocode(arguments).await,
"get_personal_place_at" => self.tool_get_personal_place_at(arguments).await,
"recall_entities" => self.tool_recall_entities(arguments, cx).await,
"recall_facts_for_photo" => self.tool_recall_facts_for_photo(arguments, cx).await,
"recall_facts_for_photo" => {
self.tool_recall_facts_for_photo(arguments, persona_id, cx)
.await
}
"store_entity" => self.tool_store_entity(arguments, ollama, cx).await,
"store_fact" => self.tool_store_fact(arguments, file_path, cx).await,
"store_fact" => {
self.tool_store_fact(arguments, file_path, persona_id, cx)
.await
}
"get_current_datetime" => Self::tool_get_current_datetime(),
unknown => format!("Unknown tool: {}", unknown),
};
@@ -2391,8 +2406,11 @@ Return ONLY the summary, nothing else."#,
async fn tool_recall_facts_for_photo(
&self,
args: &serde_json::Value,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::PersonaFilter;
let persona_filter = PersonaFilter::Single(persona_id.to_string());
let file_path = match args.get("file_path").and_then(|v| v.as_str()) {
Some(p) => p.to_string(),
None => return "Error: missing required parameter 'file_path'".to_string(),
@@ -2432,7 +2450,7 @@ Return ONLY the summary, nothing else."#,
"Entity: {} ({}, role: {})",
e.name, e.entity_type, role
));
if let Ok(facts) = kdao.get_facts_for_entity(cx, entity_id) {
if let Ok(facts) = kdao.get_facts_for_entity(cx, entity_id, &persona_filter) {
for f in facts.iter().filter(|f| f.status == "active") {
let obj = if let Some(ref v) = f.object_value {
v.clone()
@@ -2577,6 +2595,7 @@ Return ONLY the summary, nothing else."#,
&self,
args: &serde_json::Value,
file_path: &str,
persona_id: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::models::{InsertEntityFact, InsertEntityPhotoLink};
@@ -2627,6 +2646,7 @@ Return ONLY the summary, nothing else."#,
confidence: 0.6,
status: "active".to_string(),
created_at: chrono::Utc::now().timestamp(),
persona_id: persona_id.to_string(),
};
let mut kdao = self
@@ -3176,6 +3196,7 @@ Return ONLY the summary, nothing else."#,
backend: Option<String>,
fewshot_examples: Vec<Vec<ChatMessage>>,
fewshot_source_ids: Vec<i32>,
persona_id: String,
) -> Result<(Option<i32>, Option<i32>)> {
let tracer = global_tracer();
let current_cx = opentelemetry::Context::current();
@@ -3652,6 +3673,7 @@ Return ONLY the summary, nothing else."#,
&ollama_client,
&image_base64,
&file_path,
&persona_id,
&loop_cx,
)
.await;