Tracing: DAO layer + handler spans on tracing, drop all Context params (#141)
trace_db_call no longer takes an OpenTelemetry Context: it opens a db.<query_type>.<operation> span (same name via otel.name, same db.query_type / db.operation attrs) on the caller's ambient tracing span and runs the DAO body inside it. The cx / ctx / context parameter is gone from every DAO trait method, impl and call site, and from the helpers and background drains that only threaded it. The remaining request-handler spans (files, tags, faces, image, video, favorites, duplicates, memories) move to #[instrument]; the Context::current_with_span / with_remote_span_context plumbing is gone, as is extract_context_from_request (the tracing-actix-web root span covers it). otel::set_span_attributes replaces BoxedSpan::set_attributes. Background-job raw spans (thumbnails, video actors) are left for #142. tests/otel_db_spans.rs pins name, attributes, status and nesting (across an instrumented spawn); otel_traceparent now uses an #[instrument] handler. CLAUDE.md documents the tracing conventions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c508cdabd
commit
b6deca2a52
55 files changed
+1942
-3852
No files matched your search
+65
-131
@@ -239,7 +239,7 @@ impl InsightGenerator {
|
||||
.user_ai_prefs_dao
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut dao| dao.get_prefs(&opentelemetry::Context::new()).ok().flatten())
|
||||
.and_then(|mut dao| dao.get_prefs().ok().flatten())
|
||||
.and_then(|p| p.display_tz);
|
||||
crate::camera_tz::display_zone_for(stored.as_deref())
|
||||
};
|
||||
@@ -345,36 +345,35 @@ impl InsightGenerator {
|
||||
has_vision: bool,
|
||||
persona: Option<(i32, &str)>,
|
||||
) -> ToolGateOpts {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let calendar_present = {
|
||||
let mut dao = self
|
||||
.calendar_dao
|
||||
.lock()
|
||||
.expect("Unable to lock CalendarEventDao");
|
||||
dao.get_event_count(&cx).map(|n| n > 0).unwrap_or(false)
|
||||
dao.get_event_count().map(|n| n > 0).unwrap_or(false)
|
||||
};
|
||||
let location_history_present = {
|
||||
let mut dao = self
|
||||
.location_dao
|
||||
.lock()
|
||||
.expect("Unable to lock LocationHistoryDao");
|
||||
dao.get_location_count(&cx).map(|n| n > 0).unwrap_or(false)
|
||||
dao.get_location_count().map(|n| n > 0).unwrap_or(false)
|
||||
};
|
||||
let daily_summaries_present = {
|
||||
let mut dao = self
|
||||
.daily_summary_dao
|
||||
.lock()
|
||||
.expect("Unable to lock DailySummaryDao");
|
||||
dao.has_any_summaries(&cx).unwrap_or(false)
|
||||
dao.has_any_summaries().unwrap_or(false)
|
||||
};
|
||||
let faces_present = {
|
||||
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
|
||||
dao.has_any_faces(&cx).unwrap_or(false)
|
||||
dao.has_any_faces().unwrap_or(false)
|
||||
};
|
||||
let allow_agent_corrections = persona
|
||||
.and_then(|(uid, pid)| {
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(&cx, uid, pid).ok().flatten()
|
||||
pdao.get_persona(uid, pid).ok().flatten()
|
||||
})
|
||||
.map(|p| p.allow_agent_corrections)
|
||||
.unwrap_or(false);
|
||||
@@ -396,9 +395,8 @@ impl InsightGenerator {
|
||||
/// carries `persona_id` but no explicit `system_prompt` should speak
|
||||
/// in the persona's stored voice, not the neutral default.
|
||||
pub(crate) fn persona_system_prompt(&self, user_id: i32, persona_id: &str) -> Option<String> {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(&cx, user_id, persona_id)
|
||||
pdao.get_persona(user_id, persona_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|p| p.system_prompt)
|
||||
@@ -446,9 +444,8 @@ impl InsightGenerator {
|
||||
/// that need a few specific fields (date_taken, GPS) without
|
||||
/// duplicating DAO plumbing.
|
||||
pub(crate) fn fetch_exif(&self, file_path: &str) -> Option<crate::database::models::ImageExif> {
|
||||
let cx = opentelemetry::Context::current();
|
||||
let mut dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
|
||||
dao.get_exif(&cx, file_path).ok().flatten()
|
||||
dao.get_exif(file_path).ok().flatten()
|
||||
}
|
||||
|
||||
/// Load image file, resize it, and encode as base64 for vision models
|
||||
@@ -619,7 +616,6 @@ impl InsightGenerator {
|
||||
span.set_attribute("query", query.clone());
|
||||
|
||||
// Create context with this span for child operations
|
||||
let search_cx = span.context();
|
||||
|
||||
log::info!("========================================");
|
||||
log::info!("RAG QUERY: {}", query);
|
||||
@@ -640,7 +636,7 @@ impl InsightGenerator {
|
||||
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
let similar_summaries = summary_dao
|
||||
.find_similar_summaries_with_time_weight(&search_cx, &query_embedding, &date_str, limit)
|
||||
.find_similar_summaries_with_time_weight(&query_embedding, &date_str, limit)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to find similar summaries: {:?}", e))?;
|
||||
|
||||
log::info!(
|
||||
@@ -684,7 +680,6 @@ impl InsightGenerator {
|
||||
if let Some(d) = date {
|
||||
span.set_attribute("date", d.to_string());
|
||||
}
|
||||
let search_cx = span.context();
|
||||
|
||||
log::info!("RAG QUERY: {} (anchor date: {:?})", query, date);
|
||||
|
||||
@@ -700,12 +695,11 @@ impl InsightGenerator {
|
||||
|
||||
let similar_summaries = match date {
|
||||
Some(d) => summary_dao.find_similar_summaries_with_time_weight(
|
||||
&search_cx,
|
||||
&query_embedding,
|
||||
&d.format("%Y-%m-%d").to_string(),
|
||||
limit,
|
||||
),
|
||||
None => summary_dao.find_similar_summaries(&search_cx, &query_embedding, limit),
|
||||
None => summary_dao.find_similar_summaries(&query_embedding, limit),
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("Failed to find similar summaries: {:?}", e))?;
|
||||
|
||||
@@ -770,7 +764,6 @@ impl InsightGenerator {
|
||||
location: Option<&str>,
|
||||
) -> Result<Option<String>> {
|
||||
let span = tracing::Span::current();
|
||||
let calendar_cx = span.context();
|
||||
|
||||
let query_embedding = if let Some(loc) = location {
|
||||
match crate::ai::embed_query(&self.ollama, self.llamacpp.as_deref(), loc).await {
|
||||
@@ -790,7 +783,6 @@ impl InsightGenerator {
|
||||
.lock()
|
||||
.expect("Unable to lock CalendarEventDao");
|
||||
dao.find_relevant_events_hybrid(
|
||||
&calendar_cx,
|
||||
timestamp,
|
||||
7, // ±7 days window
|
||||
query_embedding.as_deref(),
|
||||
@@ -844,7 +836,6 @@ impl InsightGenerator {
|
||||
exif_gps: Option<(f64, f64)>,
|
||||
) -> Result<Option<String>> {
|
||||
let span = tracing::Span::current();
|
||||
let location_cx = span.context();
|
||||
|
||||
let nearest = {
|
||||
let mut dao = self
|
||||
@@ -852,9 +843,7 @@ impl InsightGenerator {
|
||||
.lock()
|
||||
.expect("Unable to lock LocationHistoryDao");
|
||||
dao.find_nearest_location(
|
||||
&location_cx,
|
||||
timestamp,
|
||||
10800, // ±3 hours (more realistic for photo timing)
|
||||
timestamp, 10800, // ±3 hours (more realistic for photo timing)
|
||||
)
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -922,7 +911,6 @@ impl InsightGenerator {
|
||||
enrichment: Option<&str>,
|
||||
) -> Result<Option<String>> {
|
||||
let span = tracing::Span::current();
|
||||
let search_cx = span.context();
|
||||
|
||||
// Use enrichment (topics + photo description + tags) if available;
|
||||
// fall back to generic temporal query.
|
||||
@@ -961,7 +949,6 @@ impl InsightGenerator {
|
||||
.lock()
|
||||
.expect("Unable to lock SearchHistoryDao");
|
||||
dao.find_relevant_searches_hybrid(
|
||||
&search_cx,
|
||||
timestamp,
|
||||
30, // ±30 days (wider window than calendar)
|
||||
Some(&query_embedding),
|
||||
@@ -1099,13 +1086,12 @@ impl InsightGenerator {
|
||||
}
|
||||
|
||||
// Create context with this span for child operations
|
||||
let insight_cx = span.context();
|
||||
|
||||
// 1. Get EXIF data for the photo
|
||||
let exif = {
|
||||
let mut exif_dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
|
||||
exif_dao
|
||||
.get_exif(&insight_cx, &file_path)
|
||||
.get_exif(&file_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get EXIF: {:?}", e))?
|
||||
};
|
||||
|
||||
@@ -1124,7 +1110,7 @@ impl InsightGenerator {
|
||||
// Fetch file tags (used to enrich RAG and final context)
|
||||
let tag_names: Vec<String> = {
|
||||
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
|
||||
dao.get_tags_for_path(&insight_cx, &file_path)
|
||||
dao.get_tags_for_path(&file_path)
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("Failed to fetch tags for insight {}: {}", file_path, e);
|
||||
Vec::new()
|
||||
@@ -1562,9 +1548,8 @@ impl InsightGenerator {
|
||||
|
||||
let dao = self.insight_dao.clone();
|
||||
let result = retry_with_backoff("store_insight", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.store_insight(&cx, insight.clone())
|
||||
d.store_insight(insight.clone())
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store insight: {:?}", e));
|
||||
|
||||
@@ -1770,34 +1755,33 @@ Return ONLY the summary, nothing else."#,
|
||||
file_path: &str,
|
||||
user_id: i32,
|
||||
persona_id: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
let model = backend.model();
|
||||
let backend_label = backend.kind.as_str();
|
||||
let result = match tool_name {
|
||||
"search_rag" => self.tool_search_rag(arguments, backend.local()).await,
|
||||
"search_messages" => self.tool_search_messages(arguments, cx).await,
|
||||
"get_sms_messages" => self.tool_get_sms_messages(arguments, cx).await,
|
||||
"get_calendar_events" => self.tool_get_calendar_events(arguments, cx).await,
|
||||
"get_location_history" => self.tool_get_location_history(arguments, cx).await,
|
||||
"get_file_tags" => self.tool_get_file_tags(arguments, cx).await,
|
||||
"get_faces_in_photo" => self.tool_get_faces_in_photo(arguments, cx).await,
|
||||
"search_messages" => self.tool_search_messages(arguments).await,
|
||||
"get_sms_messages" => self.tool_get_sms_messages(arguments).await,
|
||||
"get_calendar_events" => self.tool_get_calendar_events(arguments).await,
|
||||
"get_location_history" => self.tool_get_location_history(arguments).await,
|
||||
"get_file_tags" => self.tool_get_file_tags(arguments).await,
|
||||
"get_faces_in_photo" => self.tool_get_faces_in_photo(arguments).await,
|
||||
"describe_photo" => {
|
||||
self.tool_describe_photo(backend.local(), image_base64)
|
||||
.await
|
||||
}
|
||||
"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_entities" => self.tool_recall_entities(arguments).await,
|
||||
"recall_facts_for_photo" => {
|
||||
self.tool_recall_facts_for_photo(arguments, user_id, persona_id, cx)
|
||||
self.tool_recall_facts_for_photo(arguments, user_id, persona_id)
|
||||
.await
|
||||
}
|
||||
"recall_facts_for_entity" => {
|
||||
self.tool_recall_facts_for_entity(arguments, user_id, persona_id, cx)
|
||||
self.tool_recall_facts_for_entity(arguments, user_id, persona_id)
|
||||
.await
|
||||
}
|
||||
"store_entity" => self.tool_store_entity(arguments, cx).await,
|
||||
"store_entity" => self.tool_store_entity(arguments).await,
|
||||
"store_fact" => {
|
||||
self.tool_store_fact(
|
||||
arguments,
|
||||
@@ -1806,16 +1790,15 @@ Return ONLY the summary, nothing else."#,
|
||||
persona_id,
|
||||
model,
|
||||
backend_label,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"update_fact" => {
|
||||
self.tool_update_fact(arguments, user_id, persona_id, model, backend_label, cx)
|
||||
self.tool_update_fact(arguments, user_id, persona_id, model, backend_label)
|
||||
.await
|
||||
}
|
||||
"supersede_fact" => {
|
||||
self.tool_supersede_fact(arguments, user_id, persona_id, model, backend_label, cx)
|
||||
self.tool_supersede_fact(arguments, user_id, persona_id, model, backend_label)
|
||||
.await
|
||||
}
|
||||
"get_current_datetime" => Self::tool_get_current_datetime(),
|
||||
@@ -2039,11 +2022,7 @@ Return ONLY the summary, nothing else."#,
|
||||
/// Tool: search_messages — keyword / semantic / hybrid search over all
|
||||
/// SMS message bodies via the Django FTS5 + embeddings pipeline. Now
|
||||
/// supports optional `contact_id`, `start_ts`, `end_ts` filters.
|
||||
async fn tool_search_messages(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_search_messages(&self, args: &serde_json::Value) -> String {
|
||||
let query = match args.get("query").and_then(|v| v.as_str()) {
|
||||
Some(q) if !q.trim().is_empty() => q.trim(),
|
||||
_ => {
|
||||
@@ -2068,7 +2047,7 @@ Return ONLY the summary, nothing else."#,
|
||||
log::info!(
|
||||
"search_messages with `date` and no `query` — routing to get_sms_messages"
|
||||
);
|
||||
let routed = self.tool_get_sms_messages(args, cx).await;
|
||||
let routed = self.tool_get_sms_messages(args).await;
|
||||
return format!(
|
||||
"(Note: routed to get_sms_messages — search_messages requires a \
|
||||
`query`; date-only browsing belongs on get_sms_messages. Prefer \
|
||||
@@ -2207,11 +2186,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: get_sms_messages — fetch SMS messages near a date for a contact
|
||||
async fn tool_get_sms_messages(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
_cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_get_sms_messages(&self, args: &serde_json::Value) -> String {
|
||||
let date_str = match args.get("date").and_then(|v| v.as_str()) {
|
||||
Some(d) => d,
|
||||
None => return "Error: missing required parameter 'date'".to_string(),
|
||||
@@ -2289,11 +2264,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: get_calendar_events — fetch calendar events near a date
|
||||
async fn tool_get_calendar_events(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_get_calendar_events(&self, args: &serde_json::Value) -> String {
|
||||
let date_str = match args.get("date").and_then(|v| v.as_str()) {
|
||||
Some(d) => d,
|
||||
None => return "Error: missing required parameter 'date'".to_string(),
|
||||
@@ -2327,7 +2298,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.calendar_dao
|
||||
.lock()
|
||||
.expect("Unable to lock CalendarEventDao");
|
||||
dao.find_relevant_events_hybrid(cx, timestamp, days_radius, None, limit)
|
||||
dao.find_relevant_events_hybrid(timestamp, days_radius, None, limit)
|
||||
.ok()
|
||||
};
|
||||
|
||||
@@ -2369,11 +2340,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: get_location_history — fetch location records near a date
|
||||
async fn tool_get_location_history(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_get_location_history(&self, args: &serde_json::Value) -> String {
|
||||
let date_str = match args.get("date").and_then(|v| v.as_str()) {
|
||||
Some(d) => d,
|
||||
None => return "Error: missing required parameter 'date'".to_string(),
|
||||
@@ -2404,7 +2371,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.location_dao
|
||||
.lock()
|
||||
.expect("Unable to lock LocationHistoryDao");
|
||||
dao.find_locations_in_range(cx, start_ts, end_ts).ok()
|
||||
dao.find_locations_in_range(start_ts, end_ts).ok()
|
||||
};
|
||||
|
||||
match locations {
|
||||
@@ -2462,11 +2429,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: get_file_tags — fetch tags for a file path
|
||||
async fn tool_get_file_tags(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_get_file_tags(&self, args: &serde_json::Value) -> 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(),
|
||||
@@ -2476,7 +2439,7 @@ Return ONLY the summary, nothing else."#,
|
||||
|
||||
let tags = {
|
||||
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
|
||||
dao.get_tags_for_path(cx, &file_path).ok()
|
||||
dao.get_tags_for_path(&file_path).ok()
|
||||
};
|
||||
|
||||
match tags {
|
||||
@@ -2493,11 +2456,7 @@ Return ONLY the summary, nothing else."#,
|
||||
/// the given file path. Resolves rel_path → content_hash via FaceDao,
|
||||
/// then queries face_detections joined with persons (status='detected'
|
||||
/// only). Returns a compact bullet list keyed for human-LLM readability.
|
||||
async fn tool_get_faces_in_photo(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_get_faces_in_photo(&self, args: &serde_json::Value) -> String {
|
||||
let file_path = match args.get("file_path").and_then(|v| v.as_str()) {
|
||||
Some(p) if !p.trim().is_empty() => p.trim().to_string(),
|
||||
_ => return "Error: missing required parameter 'file_path'".to_string(),
|
||||
@@ -2509,11 +2468,9 @@ Return ONLY the summary, nothing else."#,
|
||||
// is synchronous and there's no await in the loop body.
|
||||
let content_hash: Option<String> = {
|
||||
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
|
||||
self.libraries.iter().find_map(|lib| {
|
||||
dao.resolve_content_hash(cx, lib.id, &file_path)
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
self.libraries
|
||||
.iter()
|
||||
.find_map(|lib| dao.resolve_content_hash(lib.id, &file_path).ok().flatten())
|
||||
};
|
||||
let Some(content_hash) = content_hash else {
|
||||
return "No content_hash found for that file path (the photo may not be indexed yet, \
|
||||
@@ -2523,7 +2480,7 @@ Return ONLY the summary, nothing else."#,
|
||||
|
||||
let faces = {
|
||||
let mut dao = self.face_dao.lock().expect("Unable to lock FaceDao");
|
||||
match dao.list_for_content_hash(cx, &content_hash) {
|
||||
match dao.list_for_content_hash(&content_hash) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => return format!("Error querying faces: {}", e),
|
||||
}
|
||||
@@ -2642,11 +2599,7 @@ Return ONLY the summary, nothing else."#,
|
||||
}
|
||||
|
||||
/// Tool: recall_entities — search the knowledge memory for known entities
|
||||
async fn tool_recall_entities(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_recall_entities(&self, args: &serde_json::Value) -> String {
|
||||
use crate::database::EntityFilter;
|
||||
|
||||
let name_search = args
|
||||
@@ -2688,7 +2641,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
match kdao.list_entities(cx, filter) {
|
||||
match kdao.list_entities(filter) {
|
||||
Ok((entities, _total)) if entities.iter().all(|e| e.status == "rejected") => {
|
||||
"No known entities found matching the query.".to_string()
|
||||
}
|
||||
@@ -2719,7 +2672,6 @@ Return ONLY the summary, nothing else."#,
|
||||
args: &serde_json::Value,
|
||||
user_id: i32,
|
||||
persona_id: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
use crate::database::PersonaFilter;
|
||||
let persona_filter = PersonaFilter::Single {
|
||||
@@ -2739,7 +2691,7 @@ Return ONLY the summary, nothing else."#,
|
||||
// default of active+reviewed.
|
||||
let reviewed_only = {
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(cx, user_id, persona_id)
|
||||
pdao.get_persona(user_id, persona_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|p| p.reviewed_only_facts)
|
||||
@@ -2752,7 +2704,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
|
||||
// Fetch photo links to find which entities appear in this photo
|
||||
let links = match kdao.get_links_for_photo(cx, &file_path) {
|
||||
let links = match kdao.get_links_for_photo(&file_path) {
|
||||
Ok(l) => l,
|
||||
Err(e) => return format!("Error fetching photo links: {:?}", e),
|
||||
};
|
||||
@@ -2766,7 +2718,7 @@ Return ONLY the summary, nothing else."#,
|
||||
|
||||
// For each linked entity, fetch its facts
|
||||
for entity_id in entity_ids {
|
||||
if let Ok(entity) = kdao.get_entity_by_id(cx, entity_id)
|
||||
if let Ok(entity) = kdao.get_entity_by_id(entity_id)
|
||||
&& let Some(e) = entity
|
||||
{
|
||||
let role = links
|
||||
@@ -2778,7 +2730,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, &persona_filter) {
|
||||
if let Ok(facts) = kdao.get_facts_for_entity(entity_id, &persona_filter) {
|
||||
// Default scope: active + reviewed (everything not
|
||||
// rejected / superseded). Strict mode trims to
|
||||
// reviewed only — the persona has opted into seeing
|
||||
@@ -2794,7 +2746,7 @@ Return ONLY the summary, nothing else."#,
|
||||
let obj = if let Some(ref v) = f.object_value {
|
||||
v.clone()
|
||||
} else if let Some(oid) = f.object_entity_id {
|
||||
kdao.get_entity_by_id(cx, oid)
|
||||
kdao.get_entity_by_id(oid)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|e| format!("{} (entity ID: {})", e.name, e.id))
|
||||
@@ -2825,7 +2777,6 @@ Return ONLY the summary, nothing else."#,
|
||||
args: &serde_json::Value,
|
||||
user_id: i32,
|
||||
persona_id: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
use crate::database::PersonaFilter;
|
||||
let persona_filter = PersonaFilter::Single {
|
||||
@@ -2853,7 +2804,7 @@ Return ONLY the summary, nothing else."#,
|
||||
// row → permissive active+reviewed default).
|
||||
let reviewed_only = {
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(cx, user_id, persona_id)
|
||||
pdao.get_persona(user_id, persona_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|p| p.reviewed_only_facts)
|
||||
@@ -2865,14 +2816,14 @@ Return ONLY the summary, nothing else."#,
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
|
||||
let entity = match kdao.get_entity_by_id(cx, entity_id) {
|
||||
let entity = match kdao.get_entity_by_id(entity_id) {
|
||||
Ok(Some(e)) => e,
|
||||
Ok(None) => return format!("No entity found with ID {}.", entity_id),
|
||||
Err(e) => return format!("Error fetching entity: {:?}", e),
|
||||
};
|
||||
|
||||
let mut output_lines = vec![format!("Entity: {} ({})", entity.name, entity.entity_type)];
|
||||
match kdao.get_facts_for_entity(cx, entity_id, &persona_filter) {
|
||||
match kdao.get_facts_for_entity(entity_id, &persona_filter) {
|
||||
Ok(facts) => {
|
||||
// Default scope: active + reviewed. Strict mode trims to
|
||||
// reviewed only — same allow rule as recall_facts_for_photo.
|
||||
@@ -2887,7 +2838,7 @@ Return ONLY the summary, nothing else."#,
|
||||
let obj = if let Some(ref v) = f.object_value {
|
||||
v.clone()
|
||||
} else if let Some(oid) = f.object_entity_id {
|
||||
kdao.get_entity_by_id(cx, oid)
|
||||
kdao.get_entity_by_id(oid)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|e| format!("{} (entity ID: {})", e.name, e.id))
|
||||
@@ -2914,11 +2865,7 @@ Return ONLY the summary, nothing else."#,
|
||||
/// Tool: store_entity — upsert an entity into the knowledge memory.
|
||||
/// Embeddings go through the configured local backend (`LLM_BACKEND`),
|
||||
/// independent of the per-request chat backend in the caller.
|
||||
async fn tool_store_entity(
|
||||
&self,
|
||||
args: &serde_json::Value,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
async fn tool_store_entity(&self, args: &serde_json::Value) -> String {
|
||||
use crate::database::models::InsertEntity;
|
||||
|
||||
let name = match args.get("name").and_then(|v| v.as_str()) {
|
||||
@@ -2964,7 +2911,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.list_entities(cx, filter)
|
||||
kdao.list_entities(filter)
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
.into_iter()
|
||||
@@ -3025,7 +2972,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.upsert_entity(cx, insert.clone())
|
||||
kdao.upsert_entity(insert.clone())
|
||||
}) {
|
||||
Ok(entity) => {
|
||||
let mut response = format!(
|
||||
@@ -3056,7 +3003,6 @@ Return ONLY the summary, nothing else."#,
|
||||
persona_id: &str,
|
||||
model: &str,
|
||||
backend: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
use crate::database::models::{InsertEntityFact, InsertEntityPhotoLink};
|
||||
|
||||
@@ -3140,7 +3086,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.upsert_fact(cx, fact.clone())
|
||||
kdao.upsert_fact(fact.clone())
|
||||
}) {
|
||||
Ok((f, is_new)) => (f, is_new),
|
||||
Err(e) => return format!("Error storing fact: {:?}", e),
|
||||
@@ -3164,7 +3110,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.upsert_photo_link(cx, link.clone())
|
||||
kdao.upsert_photo_link(link.clone())
|
||||
},
|
||||
) {
|
||||
log::warn!(
|
||||
@@ -3197,14 +3143,13 @@ Return ONLY the summary, nothing else."#,
|
||||
persona_id: &str,
|
||||
model: &str,
|
||||
backend: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
use crate::database::FactPatch;
|
||||
|
||||
// Defense-in-depth gate check.
|
||||
let allowed = {
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(cx, user_id, persona_id)
|
||||
pdao.get_persona(user_id, persona_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|p| p.allow_agent_corrections)
|
||||
@@ -3257,7 +3202,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.update_fact(cx, fact_id, patch.clone(), Some((model, backend)))
|
||||
kdao.update_fact(fact_id, patch.clone(), Some((model, backend)))
|
||||
}) {
|
||||
Ok(Some(f)) => format!(
|
||||
"Updated fact ID:{} (status={}, confidence={:.2})",
|
||||
@@ -3277,11 +3222,10 @@ Return ONLY the summary, nothing else."#,
|
||||
persona_id: &str,
|
||||
model: &str,
|
||||
backend: &str,
|
||||
cx: &opentelemetry::Context,
|
||||
) -> String {
|
||||
let allowed = {
|
||||
let mut pdao = self.persona_dao.lock().expect("Unable to lock PersonaDao");
|
||||
pdao.get_persona(cx, user_id, persona_id)
|
||||
pdao.get_persona(user_id, persona_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|p| p.allow_agent_corrections)
|
||||
@@ -3318,7 +3262,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
kdao.supersede_fact(cx, old_fact_id, new_fact_id, Some((model, backend)))
|
||||
kdao.supersede_fact(old_fact_id, new_fact_id, Some((model, backend)))
|
||||
},
|
||||
) {
|
||||
Ok(Some(f)) => format!(
|
||||
@@ -4140,13 +4084,11 @@ Return ONLY the summary, nothing else."#,
|
||||
span.set_attribute("model", backend.model().to_string());
|
||||
span.set_attribute("images_inline", backend.images_inline);
|
||||
|
||||
let insight_cx = span.context();
|
||||
|
||||
// 3. Fetch EXIF
|
||||
let exif = {
|
||||
let mut exif_dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
|
||||
exif_dao
|
||||
.get_exif(&insight_cx, &file_path)
|
||||
.get_exif(&file_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get EXIF: {:?}", e))?
|
||||
};
|
||||
|
||||
@@ -4160,7 +4102,7 @@ Return ONLY the summary, nothing else."#,
|
||||
// 5. Fetch tags
|
||||
let tag_names: Vec<String> = {
|
||||
let mut dao = self.tag_dao.lock().expect("Unable to lock TagDao");
|
||||
dao.get_tags_for_path(&insight_cx, &file_path)
|
||||
dao.get_tags_for_path(&file_path)
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("Failed to fetch tags for agentic {}: {}", file_path, e);
|
||||
Vec::new()
|
||||
@@ -4196,7 +4138,7 @@ Return ONLY the summary, nothing else."#,
|
||||
created_at: Utc::now().timestamp(),
|
||||
updated_at: Utc::now().timestamp(),
|
||||
};
|
||||
match kdao.upsert_entity(&insight_cx, owner) {
|
||||
match kdao.upsert_entity(owner) {
|
||||
Ok(e) => {
|
||||
log::info!("Owner entity '{}' ID: {}", owner_name, e.id);
|
||||
Some(e.id)
|
||||
@@ -4334,7 +4276,6 @@ Return ONLY the summary, nothing else."#,
|
||||
// LLM / tool awaits below run inside `loop_span` so their spans
|
||||
// nest under it.
|
||||
let loop_span = tracing::info_span!("ai.agentic.loop");
|
||||
let loop_cx = loop_span.context();
|
||||
|
||||
let mut final_content = String::new();
|
||||
let mut iterations_used = 0usize;
|
||||
@@ -4392,7 +4333,6 @@ Return ONLY the summary, nothing else."#,
|
||||
&file_path,
|
||||
user_id,
|
||||
&persona_id,
|
||||
&loop_cx,
|
||||
)
|
||||
.instrument(loop_span.clone())
|
||||
.await;
|
||||
@@ -4445,7 +4385,6 @@ Return ONLY the summary, nothing else."#,
|
||||
&file_path,
|
||||
user_id,
|
||||
&persona_id,
|
||||
&loop_cx,
|
||||
)
|
||||
.instrument(loop_span.clone())
|
||||
.await;
|
||||
@@ -4526,9 +4465,8 @@ Return ONLY the summary, nothing else."#,
|
||||
|
||||
let dao = self.insight_dao.clone();
|
||||
let stored = retry_with_backoff("store_insight", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.store_insight(&cx, insight.clone())
|
||||
d.store_insight(insight.clone())
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store agentic insight: {:?}", e));
|
||||
|
||||
@@ -4551,8 +4489,7 @@ Return ONLY the summary, nothing else."#,
|
||||
.knowledge_dao
|
||||
.lock()
|
||||
.expect("Unable to lock KnowledgeDao");
|
||||
if let Err(e) = kdao.update_facts_insight_id(&insight_cx, &file_path, stored_insight.id)
|
||||
{
|
||||
if let Err(e) = kdao.update_facts_insight_id(&file_path, stored_insight.id) {
|
||||
log::warn!(
|
||||
"Failed to backfill source_insight_id for {}: {:?}",
|
||||
file_path,
|
||||
@@ -4591,7 +4528,6 @@ Return ONLY the summary, nothing else."#,
|
||||
tools: Vec<Tool>,
|
||||
max_iter: usize,
|
||||
) -> Result<String> {
|
||||
let loop_cx = opentelemetry::Context::current();
|
||||
let mut final_content = String::new();
|
||||
|
||||
for iteration in 0..max_iter {
|
||||
@@ -4641,7 +4577,6 @@ Return ONLY the summary, nothing else."#,
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
&loop_cx,
|
||||
)
|
||||
.await;
|
||||
messages.push(ChatMessage::tool_result(result));
|
||||
@@ -4683,7 +4618,6 @@ Return ONLY the summary, nothing else."#,
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
&loop_cx,
|
||||
)
|
||||
.await;
|
||||
messages.push(ChatMessage::tool_result(result));
|
||||
|
||||
Reference in new issue
Block a user