feat: Phase 2-3 TKG-only architecture

Phase 2.1: build_face_trace_nodes_from_qdrant()
- Read trace_id, frame, bbox directly from Qdrant payload
- No dependency on face_detections table

Phase 2.3: Rule2 queries TKG nodes
- identity resolution from tkg_nodes.properties.identity_id
- TKG-only architecture (Phase 2.3)

Phase 3: Identity Agent updates TKG nodes
- match_faces_iterative() updates tkg_nodes.properties
- bind_identity_trace() syncs identity_id to TKG
- unbind_identity() removes identity_id from TKG

Test results:
- 23 face_trace nodes from Qdrant (Phase 2.1)
- 75 relationship chunks (Rule2)
- TKG rebuild: Phase0 → Phase1 → Phase2
This commit is contained in:
Accusys
2026-06-21 01:30:04 +08:00
parent 2f2ccc94f7
commit 23c440104b
5 changed files with 305 additions and 23 deletions
+53
View File
@@ -225,6 +225,32 @@ pub async fn unbind_identity(
)
})?;
// Phase 2.3: Also update TKG node (find trace_id first)
let trace_id_opt: Option<i32> = sqlx::query_scalar(&format!(
"SELECT trace_id FROM {} WHERE file_uuid = $1 AND face_id = $2",
table
))
.bind(&req.file_uuid)
.bind(&req.face_id)
.fetch_optional(state.db.pool())
.await
.ok()
.flatten();
if let Some(trace_id) = trace_id_opt {
let nodes_table = crate::core::db::schema::table_name("tkg_nodes");
let external_id = format!("trace_{}", trace_id);
let _ = sqlx::query(&format!(
"UPDATE {} SET properties = properties - 'identity_id' - 'identity_name' \
WHERE file_uuid = $1 AND node_type = 'face_trace' AND external_id = $2",
nodes_table
))
.bind(&req.file_uuid)
.bind(&external_id)
.execute(state.db.pool())
.await;
}
// Record history if there was a binding
if let Some(identity_id) = old_identity_id {
// Clear bind redo stack
@@ -794,6 +820,33 @@ pub async fn bind_identity_trace(
)
})?;
// Phase 2.3: Also update TKG node properties
let nodes_table = crate::core::db::schema::table_name("tkg_nodes");
let external_id = format!("trace_{}", req.trace_id);
let identity_name: Option<String> = sqlx::query_scalar(&format!(
"SELECT name FROM {} WHERE id = $1",
id_table
))
.bind(identity_id)
.fetch_optional(state.db.pool())
.await
.ok()
.flatten();
let _ = sqlx::query(&format!(
"UPDATE {} SET properties = jsonb_set(\
jsonb_set(properties, '{{identity_id}}', $1::jsonb, false),\
'{{identity_name}}', $2::jsonb, false)\
WHERE file_uuid = $3 AND node_type = 'face_trace' AND external_id = $4",
nodes_table
))
.bind(identity_id)
.bind(identity_name.as_deref())
.bind(&req.file_uuid)
.bind(&external_id)
.execute(state.db.pool())
.await;
// Clear bind redo stack
let _ = sqlx::query(&format!(
"DELETE FROM {} WHERE identity_id = $1 AND is_undone = true AND operation IN ('bind','unbind','bind_trace')",