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
@@ -232,12 +232,8 @@ pub async fn generate_daily_summaries(
|
||||
// Check if summary already exists
|
||||
{
|
||||
let mut dao = summary_dao.lock().expect("Unable to lock DailySummaryDao");
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
|
||||
if dao
|
||||
.summary_exists(&otel_context, &date_str, contact)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if dao.summary_exists(&date_str, contact).unwrap_or(false) {
|
||||
skipped += 1;
|
||||
if idx % 10 == 0 {
|
||||
log::info!(
|
||||
@@ -366,11 +362,10 @@ async fn generate_and_store_daily_summary(
|
||||
};
|
||||
|
||||
// DAO calls still take an explicit OTel context (#141).
|
||||
let child_cx = span.context();
|
||||
|
||||
let mut dao = summary_dao.lock().expect("Unable to lock DailySummaryDao");
|
||||
let result = dao
|
||||
.store_summary(&child_cx, insert)
|
||||
.store_summary(insert)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store summary: {:?}", e));
|
||||
|
||||
match &result {
|
||||
|
||||
+34
-69
@@ -93,14 +93,12 @@ pub async fn generation_status_handler(
|
||||
query: web::Query<GenerationStatusQuery>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let ctx = opentelemetry::Context::new();
|
||||
|
||||
if let Some(jid) = query.job_id {
|
||||
let mut dao = app_state
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
match dao.get_job_by_id(&ctx, jid) {
|
||||
match dao.get_job_by_id(jid) {
|
||||
Ok(Some(job)) => {
|
||||
return HttpResponse::Ok().json(GenerationStatusResponse {
|
||||
job_id: job.id,
|
||||
@@ -136,7 +134,7 @@ pub async fn generation_status_handler(
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
match dao.get_active_job(&ctx, library.id, &normalized) {
|
||||
match dao.get_active_job(library.id, &normalized) {
|
||||
Ok(Some(job)) => {
|
||||
return HttpResponse::Ok().json(GenerationStatusResponse {
|
||||
job_id: job.id,
|
||||
@@ -187,14 +185,12 @@ pub async fn cancel_generation_handler(
|
||||
request: web::Json<CancelGenerationRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let ctx = opentelemetry::Context::new();
|
||||
|
||||
if let Some(jid) = request.job_id {
|
||||
let mut dao = app_state
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
match dao.cancel_job(&ctx, jid) {
|
||||
match dao.cancel_job(jid) {
|
||||
Ok(true) => {
|
||||
let mut handles = app_state
|
||||
.insight_job_handles
|
||||
@@ -238,12 +234,12 @@ pub async fn cancel_generation_handler(
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
let ids = dao
|
||||
.get_active_job(&ctx, library.id, &normalized)
|
||||
.get_active_job(library.id, &normalized)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|j| vec![j.id])
|
||||
.unwrap_or_default();
|
||||
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized);
|
||||
let _ = dao.cancel_active_jobs(library.id, &normalized);
|
||||
ids
|
||||
};
|
||||
|
||||
@@ -400,7 +396,7 @@ fn apply_owner_defaults(
|
||||
let Some(mut dao) = app_state.user_ai_prefs_dao.lock().ok() else {
|
||||
return;
|
||||
};
|
||||
let Ok(Some(prefs)) = dao.get_prefs(&opentelemetry::Context::new()) else {
|
||||
let Ok(Some(prefs)) = dao.get_prefs() else {
|
||||
return;
|
||||
};
|
||||
if model.is_none() {
|
||||
@@ -455,14 +451,13 @@ pub async fn generate_insight_handler(
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let ids = dao
|
||||
.get_active_job(&ctx, library.id, &normalized_path)
|
||||
.get_active_job(library.id, &normalized_path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|j| vec![j.id])
|
||||
.unwrap_or_default();
|
||||
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
|
||||
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
|
||||
ids
|
||||
};
|
||||
for jid in &old_job_ids {
|
||||
@@ -481,12 +476,7 @@ pub async fn generate_insight_handler(
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
match dao.create_job(
|
||||
&opentelemetry::Context::new(),
|
||||
library.id,
|
||||
&normalized_path,
|
||||
gen_type,
|
||||
) {
|
||||
match dao.create_job(library.id, &normalized_path, gen_type) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
log::error!("Failed to create generation job: {:?}", e);
|
||||
@@ -538,7 +528,6 @@ pub async fn generate_insight_handler(
|
||||
})
|
||||
.await;
|
||||
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
|
||||
|
||||
match result {
|
||||
@@ -547,18 +536,12 @@ pub async fn generate_insight_handler(
|
||||
.insight_dao()
|
||||
.lock()
|
||||
.expect("Unable to lock InsightDao");
|
||||
let insight_id = insight_dao
|
||||
.get_insight(&ctx, &path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|i| i.id);
|
||||
let insight_id = insight_dao.get_insight(&path).ok().flatten().map(|i| i.id);
|
||||
if let Some(id) = insight_id {
|
||||
if let Err(e) = dao.complete_job(&ctx, job_id, id) {
|
||||
if let Err(e) = dao.complete_job(job_id, id) {
|
||||
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
|
||||
}
|
||||
} else if let Err(e) =
|
||||
dao.fail_job(&ctx, job_id, "generation returned no insight")
|
||||
{
|
||||
} else if let Err(e) = dao.fail_job(job_id, "generation returned no insight") {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
|
||||
}
|
||||
}
|
||||
@@ -566,7 +549,7 @@ pub async fn generate_insight_handler(
|
||||
log::error!("Insight generation failed for {}: {:?}", path, e);
|
||||
// `{:#}` = one-line context chain; the job's error_message is
|
||||
// returned to the client verbatim, so no Debug/backtrace here.
|
||||
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
|
||||
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
@@ -577,14 +560,14 @@ pub async fn generate_insight_handler(
|
||||
timeout_secs
|
||||
);
|
||||
if let Err(err) =
|
||||
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
|
||||
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
|
||||
{
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Insight generation task panicked for {}", path);
|
||||
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
|
||||
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
@@ -627,8 +610,6 @@ pub async fn get_insight_handler(
|
||||
let normalized_path = normalize_path(&query.path);
|
||||
log::debug!("Fetching insight for {}", normalized_path);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
|
||||
// Expand to rel_paths sharing content so an insight generated under
|
||||
// library 1 still shows when the same photo is viewed from library 2.
|
||||
let library = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
|
||||
@@ -637,13 +618,13 @@ pub async fn get_insight_handler(
|
||||
.unwrap_or_else(|| app_state.primary_library());
|
||||
let sibling_paths = {
|
||||
let mut exif = exif_dao.lock().expect("Unable to lock ExifDao");
|
||||
exif.get_rel_paths_sharing_content(&otel_context, library.id, &normalized_path)
|
||||
exif.get_rel_paths_sharing_content(library.id, &normalized_path)
|
||||
.unwrap_or_else(|_| vec![normalized_path.clone()])
|
||||
};
|
||||
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
match dao.get_insight_for_paths(&otel_context, &sibling_paths) {
|
||||
match dao.get_insight_for_paths(&sibling_paths) {
|
||||
Ok(Some(insight)) => HttpResponse::Ok().json(PhotoInsightResponse::from(insight)),
|
||||
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
|
||||
"error": "Insight not found"
|
||||
@@ -667,10 +648,9 @@ pub async fn delete_insight_handler(
|
||||
let normalized_path = normalize_path(&query.path);
|
||||
log::info!("Deleting insight for {}", normalized_path);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
match dao.delete_insight(&otel_context, &normalized_path) {
|
||||
match dao.delete_insight(&normalized_path) {
|
||||
Ok(()) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"success": true,
|
||||
"message": "Insight deleted successfully"
|
||||
@@ -692,10 +672,9 @@ pub async fn get_all_insights_handler(
|
||||
) -> impl Responder {
|
||||
log::debug!("Fetching all insights");
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
match dao.get_all_insights(&otel_context) {
|
||||
match dao.get_all_insights() {
|
||||
Ok(insights) => {
|
||||
let responses: Vec<PhotoInsightResponse> = insights
|
||||
.into_iter()
|
||||
@@ -725,10 +704,9 @@ pub async fn get_insight_history_handler(
|
||||
let normalized_path = normalize_path(&query.path);
|
||||
log::debug!("Fetching insight history for {}", normalized_path);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
match dao.get_insight_history(&otel_context, &normalized_path) {
|
||||
match dao.get_insight_history(&normalized_path) {
|
||||
Ok(insights) => {
|
||||
let responses: Vec<PhotoInsightResponse> = insights
|
||||
.into_iter()
|
||||
@@ -790,14 +768,13 @@ pub async fn generate_agentic_insight_handler(
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let ids = dao
|
||||
.get_active_job(&ctx, library.id, &normalized_path)
|
||||
.get_active_job(library.id, &normalized_path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|j| vec![j.id])
|
||||
.unwrap_or_default();
|
||||
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
|
||||
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
|
||||
ids
|
||||
};
|
||||
for jid in &old_job_ids {
|
||||
@@ -816,12 +793,7 @@ pub async fn generate_agentic_insight_handler(
|
||||
.insight_job_dao
|
||||
.lock()
|
||||
.expect("Unable to lock InsightJobDao");
|
||||
match dao.create_job(
|
||||
&opentelemetry::Context::new(),
|
||||
library.id,
|
||||
&normalized_path,
|
||||
gen_type,
|
||||
) {
|
||||
match dao.create_job(library.id, &normalized_path, gen_type) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
log::error!("Failed to create agentic generation job: {:?}", e);
|
||||
@@ -844,7 +816,6 @@ pub async fn generate_agentic_insight_handler(
|
||||
};
|
||||
|
||||
let fewshot_examples: Vec<Vec<ChatMessage>> = {
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = app_state
|
||||
.insight_chat
|
||||
.insight_dao()
|
||||
@@ -853,7 +824,7 @@ pub async fn generate_agentic_insight_handler(
|
||||
fewshot_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let insight = dao.get_insight_by_id(&otel_context, *id).ok().flatten()?;
|
||||
let insight = dao.get_insight_by_id(*id).ok().flatten()?;
|
||||
let json = insight.training_messages?;
|
||||
match serde_json::from_str::<Vec<ChatMessage>>(&json) {
|
||||
Ok(msgs) => Some(msgs),
|
||||
@@ -940,19 +911,16 @@ pub async fn generate_agentic_insight_handler(
|
||||
})
|
||||
.await;
|
||||
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
|
||||
|
||||
match result {
|
||||
Ok(Ok(Ok((Some(insight_id), _, _)))) => {
|
||||
if let Err(e) = dao.complete_job(&ctx, job_id, insight_id) {
|
||||
if let Err(e) = dao.complete_job(job_id, insight_id) {
|
||||
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
|
||||
}
|
||||
}
|
||||
Ok(Ok(Ok((None, _, _)))) => {
|
||||
if let Err(e) =
|
||||
dao.fail_job(&ctx, job_id, "agentic generation returned no insight")
|
||||
{
|
||||
if let Err(e) = dao.fail_job(job_id, "agentic generation returned no insight") {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
|
||||
}
|
||||
}
|
||||
@@ -960,7 +928,7 @@ pub async fn generate_agentic_insight_handler(
|
||||
log::error!("Agentic insight generation failed for {}: {:?}", path, e);
|
||||
// `{:#}` = one-line context chain; the job's error_message is
|
||||
// returned to the client verbatim, so no Debug/backtrace here.
|
||||
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
|
||||
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
@@ -971,14 +939,14 @@ pub async fn generate_agentic_insight_handler(
|
||||
timeout_secs
|
||||
);
|
||||
if let Err(err) =
|
||||
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
|
||||
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
|
||||
{
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Agentic insight generation task panicked for {}", path);
|
||||
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
|
||||
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
|
||||
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
|
||||
}
|
||||
}
|
||||
@@ -1135,14 +1103,13 @@ pub async fn rate_insight_handler(
|
||||
request.approved
|
||||
);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
// Rate a specific version by id when provided (history view), otherwise
|
||||
// rate the current insight for the path.
|
||||
let result = match request.insight_id {
|
||||
Some(id) => dao.rate_insight_by_id(&otel_context, id, request.approved),
|
||||
None => dao.rate_insight(&otel_context, &normalized_path, request.approved),
|
||||
Some(id) => dao.rate_insight_by_id(id, request.approved),
|
||||
None => dao.rate_insight(&normalized_path, request.approved),
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -1169,13 +1136,12 @@ pub async fn export_training_data_handler(
|
||||
let approved_only = query.approved_only.unwrap_or(true);
|
||||
log::info!("Exporting training data (approved_only={})", approved_only);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
let insights = if approved_only {
|
||||
dao.get_approved_insights(&otel_context)
|
||||
dao.get_approved_insights()
|
||||
} else {
|
||||
dao.get_all_insights(&otel_context)
|
||||
dao.get_all_insights()
|
||||
};
|
||||
|
||||
match insights {
|
||||
@@ -2369,8 +2335,7 @@ mod owner_defaults_tests {
|
||||
)));
|
||||
if let Some(p) = prefs {
|
||||
let mut dao = SqliteUserAiPrefsDao::from_connection(conn);
|
||||
dao.upsert_prefs(&opentelemetry::Context::new(), &p)
|
||||
.expect("seed prefs");
|
||||
dao.upsert_prefs(&p).expect("seed prefs");
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
+18
-41
@@ -154,15 +154,14 @@ impl InsightChatService {
|
||||
branch_id: Option<u64>,
|
||||
) -> Result<HistoryView> {
|
||||
let normalized = normalize_path(file_path);
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
let insight = match dao
|
||||
.get_current_insight_for_library(&cx, library_id, &normalized)
|
||||
.get_current_insight_for_library(library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
{
|
||||
Some(i) => i,
|
||||
None => dao
|
||||
.get_insight(&cx, &normalized)
|
||||
.get_insight(&normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
.ok_or_else(|| anyhow!("no insight found for path"))?,
|
||||
};
|
||||
@@ -238,9 +237,8 @@ impl InsightChatService {
|
||||
|
||||
// 2. Load the current insight + history.
|
||||
let insight = {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
dao.get_current_insight_for_library(&cx, req.library_id, &normalized)
|
||||
dao.get_current_insight_for_library(req.library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
.ok_or_else(|| anyhow!("no insight found for path"))?
|
||||
};
|
||||
@@ -363,7 +361,6 @@ impl InsightChatService {
|
||||
// Each LLM / tool await runs inside `loop_span` so their spans
|
||||
// nest under it.
|
||||
let loop_span = tracing::info_span!("ai.chat.loop");
|
||||
let loop_cx = loop_span.context();
|
||||
let mut tool_calls_made = 0usize;
|
||||
let mut iterations_used = 0usize;
|
||||
let mut last_prompt_eval_count: Option<i32> = None;
|
||||
@@ -414,7 +411,6 @@ impl InsightChatService {
|
||||
&normalized,
|
||||
req.user_id,
|
||||
&active_persona,
|
||||
&loop_cx,
|
||||
)
|
||||
.instrument(loop_span.clone())
|
||||
.await;
|
||||
@@ -533,18 +529,16 @@ impl InsightChatService {
|
||||
};
|
||||
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, new_row.clone())
|
||||
d.store_insight(new_row.clone())
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
|
||||
amended_insight_id = Some(stored.id);
|
||||
} else {
|
||||
let dao = self.insight_dao.clone();
|
||||
let rows = retry_with_backoff("update_training_messages", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.update_training_messages(&cx, req.library_id, &normalized, &json)
|
||||
d.update_training_messages(req.library_id, &normalized, &json)
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
|
||||
if rows == 0 {
|
||||
@@ -602,9 +596,8 @@ impl InsightChatService {
|
||||
let _guard = entry_lock.lock().await;
|
||||
|
||||
let insight = {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
dao.get_current_insight_for_library(&cx, library_id, &normalized)
|
||||
dao.get_current_insight_for_library(library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
.ok_or_else(|| anyhow!("no insight found for path"))?
|
||||
};
|
||||
@@ -639,9 +632,8 @@ impl InsightChatService {
|
||||
|
||||
let dao = self.insight_dao.clone();
|
||||
let rows = retry_with_backoff("update_training_messages", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.update_training_messages(&cx, library_id, &normalized, &json)
|
||||
d.update_training_messages(library_id, &normalized, &json)
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to persist rewound history: {:?}", e))?;
|
||||
if rows == 0 {
|
||||
@@ -677,9 +669,8 @@ impl InsightChatService {
|
||||
let _guard = entry_lock.lock().await;
|
||||
|
||||
let insight = {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
dao.get_current_insight_for_library(&cx, library_id, &normalized)
|
||||
dao.get_current_insight_for_library(library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
.ok_or_else(|| anyhow!("no insight found for path"))?
|
||||
};
|
||||
@@ -710,9 +701,8 @@ impl InsightChatService {
|
||||
|
||||
let dao = self.insight_dao.clone();
|
||||
let rows = retry_with_backoff("update_training_messages", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.update_training_messages(&cx, library_id, &normalized, &json)
|
||||
d.update_training_messages(library_id, &normalized, &json)
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to persist branch switch: {:?}", e))?;
|
||||
if rows == 0 {
|
||||
@@ -742,10 +732,9 @@ impl InsightChatService {
|
||||
viewing_leaf: Option<u64>,
|
||||
) -> Result<(Vec<BranchLeafInfo>, u64)> {
|
||||
let normalized = normalize_path(file_path);
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
let insight = dao
|
||||
.get_current_insight_for_library(&cx, library_id, &normalized)
|
||||
.get_current_insight_for_library(library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
.ok_or_else(|| anyhow!("no insight found for path"))?;
|
||||
|
||||
@@ -930,9 +919,8 @@ impl InsightChatService {
|
||||
|
||||
// Look up existing insight scoped to this turn's library_id.
|
||||
let existing_insight = {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
dao.get_current_insight_for_library(&cx, req.library_id, &normalized)
|
||||
dao.get_current_insight_for_library(req.library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
};
|
||||
|
||||
@@ -1129,18 +1117,16 @@ impl InsightChatService {
|
||||
};
|
||||
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, new_row.clone())
|
||||
d.store_insight(new_row.clone())
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
|
||||
amended_insight_id = Some(stored.id);
|
||||
} else {
|
||||
let dao = self.insight_dao.clone();
|
||||
let rows = retry_with_backoff("update_training_messages", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.update_training_messages(&cx, req.library_id, &normalized, &json)
|
||||
d.update_training_messages(req.library_id, &normalized, &json)
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
|
||||
if rows == 0 {
|
||||
@@ -1327,9 +1313,8 @@ impl InsightChatService {
|
||||
};
|
||||
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, new_row.clone())
|
||||
d.store_insight(new_row.clone())
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to store bootstrap insight: {:?}", e))?;
|
||||
|
||||
@@ -1417,9 +1402,8 @@ impl InsightChatService {
|
||||
// library bootstraps cleanly, even when another library has
|
||||
// an insight for the same rel_path.
|
||||
let existing_insight = {
|
||||
let db_cx = opentelemetry::Context::new();
|
||||
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
dao.get_current_insight_for_library(&db_cx, req.library_id, &normalized)
|
||||
dao.get_current_insight_for_library(req.library_id, &normalized)
|
||||
.map_err(|e| anyhow!("failed to load insight: {:?}", e))?
|
||||
};
|
||||
|
||||
@@ -1595,18 +1579,16 @@ impl InsightChatService {
|
||||
};
|
||||
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, new_row.clone())
|
||||
d.store_insight(new_row.clone())
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to store amended insight: {:?}", e))?;
|
||||
amended_insight_id = Some(stored.id);
|
||||
} else {
|
||||
let dao = self.insight_dao.clone();
|
||||
let rows = retry_with_backoff("update_training_messages", 3, || {
|
||||
let cx = opentelemetry::Context::new();
|
||||
let mut d = dao.lock().expect("Unable to lock InsightDao");
|
||||
d.update_training_messages(&cx, req.library_id, &normalized, &json)
|
||||
d.update_training_messages(req.library_id, &normalized, &json)
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to persist chat history: {:?}", e))?;
|
||||
if rows == 0 {
|
||||
@@ -1810,9 +1792,8 @@ impl InsightChatService {
|
||||
};
|
||||
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, new_row.clone())
|
||||
d.store_insight(new_row.clone())
|
||||
})
|
||||
.map_err(|e| anyhow!("failed to store bootstrap insight: {:?}", e))?;
|
||||
|
||||
@@ -1920,7 +1901,6 @@ impl InsightChatService {
|
||||
arguments: tool_call.function.arguments.clone(),
|
||||
})
|
||||
.await;
|
||||
let cx = opentelemetry::Context::new();
|
||||
let result = self
|
||||
.generator
|
||||
.execute_tool(
|
||||
@@ -1931,7 +1911,6 @@ impl InsightChatService {
|
||||
normalized,
|
||||
user_id,
|
||||
active_persona,
|
||||
&cx,
|
||||
)
|
||||
.await;
|
||||
let (result_preview, result_truncated) = truncate_tool_result(&result);
|
||||
@@ -2421,7 +2400,6 @@ pub async fn run_streaming_agentic_loop_with_entry(
|
||||
arguments: tool_call.function.arguments.clone(),
|
||||
})
|
||||
.await;
|
||||
let cx = opentelemetry::Context::new();
|
||||
let result = generator
|
||||
.execute_tool(
|
||||
&tool_call.function.name,
|
||||
@@ -2431,7 +2409,6 @@ pub async fn run_streaming_agentic_loop_with_entry(
|
||||
normalized,
|
||||
user_id,
|
||||
active_persona,
|
||||
&cx,
|
||||
)
|
||||
.await;
|
||||
let (result_preview, result_truncated) = truncate_tool_result(&result);
|
||||
|
||||
+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));
|
||||
|
||||
+12
-22
@@ -529,26 +529,24 @@ impl PersonaChatSession {
|
||||
if persona_id.is_empty() {
|
||||
return Err(PersonaChatError::UnknownPersona(String::new()));
|
||||
}
|
||||
let cx = opentelemetry::Context::current();
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
// Validate up front rather than letting the first turn 404: a
|
||||
// conversation row pointing at a persona that doesn't exist would be
|
||||
// a permanently dead entry in the list.
|
||||
let persona = dao
|
||||
.get_persona(&cx, user_id, persona_id)
|
||||
.get_persona(user_id, persona_id)
|
||||
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?;
|
||||
if persona.is_none() {
|
||||
return Err(PersonaChatError::UnknownPersona(persona_id.to_string()));
|
||||
}
|
||||
dao.create_persona_chat(&cx, user_id, persona_id, Utc::now().timestamp_millis())
|
||||
dao.create_persona_chat(user_id, persona_id, Utc::now().timestamp_millis())
|
||||
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))
|
||||
}
|
||||
|
||||
/// Delete a conversation and its transcript outright.
|
||||
pub fn delete_conversation(&self, user_id: i32, conversation_id: &str) -> Result<()> {
|
||||
let cx = opentelemetry::Context::current();
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
dao.delete_persona_chat(&cx, user_id, conversation_id)
|
||||
dao.delete_persona_chat(user_id, conversation_id)
|
||||
.map_err(|e| anyhow!("failed to delete persona chat: {e:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -565,10 +563,9 @@ impl PersonaChatSession {
|
||||
conversation_id: &str,
|
||||
branch_id: Option<u64>,
|
||||
) -> Result<PersonaChatHistoryView> {
|
||||
let cx = opentelemetry::Context::current();
|
||||
let row = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
dao.get_persona_chat(&cx, user_id, conversation_id)
|
||||
dao.get_persona_chat(user_id, conversation_id)
|
||||
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
|
||||
};
|
||||
let Some(row) = row else {
|
||||
@@ -628,18 +625,17 @@ impl PersonaChatSession {
|
||||
return Err(PersonaChatError::UnknownConversation(conversation_id));
|
||||
}
|
||||
|
||||
let cx = opentelemetry::Context::current();
|
||||
// The persona comes from the stored conversation, not the request:
|
||||
// a transcript's voice is fixed when it is created, so a stale
|
||||
// client can't quietly swap it half-way through a thread.
|
||||
let (persona, persona_id) = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
let row = dao
|
||||
.get_persona_chat(&cx, user_id, &conversation_id)
|
||||
.get_persona_chat(user_id, &conversation_id)
|
||||
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
|
||||
.ok_or_else(|| PersonaChatError::UnknownConversation(conversation_id.clone()))?;
|
||||
let persona = dao
|
||||
.get_persona(&cx, user_id, &row.persona_id)
|
||||
.get_persona(user_id, &row.persona_id)
|
||||
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
|
||||
.ok_or_else(|| PersonaChatError::UnknownPersona(row.persona_id.clone()))?;
|
||||
(persona, row.persona_id)
|
||||
@@ -742,10 +738,9 @@ impl PersonaChatSession {
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
// Replay the active branch of the conversation tree.
|
||||
let cx = opentelemetry::Context::current();
|
||||
let row = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
dao.get_persona_chat(&cx, user_id, &conversation_id)
|
||||
dao.get_persona_chat(user_id, &conversation_id)
|
||||
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
|
||||
};
|
||||
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
|
||||
@@ -935,7 +930,6 @@ impl PersonaChatSession {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
let rows = dao
|
||||
.update_persona_chat(
|
||||
&cx,
|
||||
user_id,
|
||||
&conversation_id,
|
||||
&json,
|
||||
@@ -958,7 +952,7 @@ impl PersonaChatSession {
|
||||
.unwrap_or_else(|| fallback_title(&messages));
|
||||
if !title.is_empty() {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
if let Err(e) = dao.set_persona_chat_title(&cx, user_id, &conversation_id, &title) {
|
||||
if let Err(e) = dao.set_persona_chat_title(user_id, &conversation_id, &title) {
|
||||
log::warn!("failed to store persona chat title: {e:?}");
|
||||
}
|
||||
}
|
||||
@@ -989,14 +983,13 @@ impl PersonaChatSession {
|
||||
/// Conversations that hold nothing but the seed are omitted too — the
|
||||
/// user has never actually said anything in them.
|
||||
pub fn list_conversations(&self, user_id: i32) -> Result<Vec<PersonaConversationSummary>> {
|
||||
let cx = opentelemetry::Context::current();
|
||||
let (rows, personas) = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
let rows = dao
|
||||
.list_persona_chats(&cx, user_id)
|
||||
.list_persona_chats(user_id)
|
||||
.map_err(|e| anyhow!("failed to list persona chats: {e:?}"))?;
|
||||
let personas = dao
|
||||
.list_personas(&cx, user_id)
|
||||
.list_personas(user_id)
|
||||
.map_err(|e| anyhow!("failed to list personas: {e:?}"))?;
|
||||
(rows, personas)
|
||||
};
|
||||
@@ -1099,10 +1092,9 @@ impl PersonaChatSession {
|
||||
};
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
let cx = opentelemetry::Context::current();
|
||||
let row = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
dao.get_persona_chat(&cx, user_id, conversation_id)
|
||||
dao.get_persona_chat(user_id, conversation_id)
|
||||
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
|
||||
};
|
||||
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
|
||||
@@ -1121,7 +1113,6 @@ impl PersonaChatSession {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
let rows = dao
|
||||
.update_persona_chat(
|
||||
&cx,
|
||||
user_id,
|
||||
conversation_id,
|
||||
&json,
|
||||
@@ -1195,10 +1186,9 @@ impl PersonaChatSession {
|
||||
node_id: Option<u64>,
|
||||
viewing_leaf: Option<u64>,
|
||||
) -> Result<(Vec<crate::ai::llm_client::BranchLeafInfo>, u64)> {
|
||||
let cx = opentelemetry::Context::current();
|
||||
let row = {
|
||||
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
|
||||
dao.get_persona_chat(&cx, user_id, conversation_id)
|
||||
dao.get_persona_chat(user_id, conversation_id)
|
||||
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
|
||||
};
|
||||
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
|
||||
|
||||
Reference in new issue
Block a user