insight-chat: add get_faces_in_photo agentic tool

The LLM had no path to see face_detections data — get_file_tags
returns user-applied tags, but a face that's been detected and bound
to a person via the embedding-cluster auto-bind path doesn't always
have a matching tag. The new tool joins face_detections with persons
by content_hash and returns bound names + bboxes, plus unidentified
faces (so smaller models can count people in the photo without
inferring from a visual description).

Gated on face_detections being non-empty via the same has_any_*
pattern as daily_summaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-07 17:43:16 -04:00
parent 388eb22cd2
commit b64a5bec28
4 changed files with 143 additions and 0 deletions

View File

@@ -503,6 +503,10 @@ pub trait FaceDao: Send + Sync {
into: i32,
) -> anyhow::Result<Person>;
/// Cheap presence probe — returns true iff at least one face has been
/// detected (excluding marker rows). Used by chat-tool gating.
fn has_any_faces(&mut self, ctx: &opentelemetry::Context) -> anyhow::Result<bool>;
/// Resolve `(library_id, rel_path)` → `content_hash` via image_exif.
/// Returns None when the photo hasn't been EXIF-indexed yet (no row
/// in image_exif) or when the row exists but content_hash is NULL.
@@ -1432,6 +1436,20 @@ impl FaceDao for SqliteFaceDao {
})
}
fn has_any_faces(&mut self, ctx: &opentelemetry::Context) -> anyhow::Result<bool> {
use anyhow::Context;
let mut conn = self.connection.lock().expect("face dao lock");
trace_db_call(ctx, "query", "has_any_faces", |_span| {
face_detections::table
.filter(face_detections::status.eq("detected"))
.select(face_detections::id)
.first::<i32>(conn.deref_mut())
.optional()
.map(|x| x.is_some())
.with_context(|| "has_any_faces query")
})
}
fn resolve_content_hash(
&mut self,
ctx: &opentelemetry::Context,