feat: add confirm_identity API endpoint
- Add POST /api/v1/agents/identity/confirm endpoint - Calls confirm_identity.py to bind trace to identity - Updates TKG, Qdrant _faces, PG face_detections, _seeds - Optional Round 2 propagation after confirmation - Fix trace_id=0 check in confirm_identity.py (use 'is not None') - Document API endpoint in 08_identity_agent.md
This commit is contained in:
@@ -65,4 +65,63 @@ curl -s -X POST "$API/api/v1/agents/identity/match-from-trace" \
|
||||
```
|
||||
|
||||
---
|
||||
*Updated: 2026-05-19 12:49:24*
|
||||
|
||||
### `POST /api/v1/agents/identity/confirm`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Confirm identity binding for a trace. This marks the trace as confirmed in TKG, updates face_detections, adds to _seeds, and optionally triggers Round 2 propagation.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | Video file UUID |
|
||||
| `trace_id` | integer | Yes | Face trace ID to confirm |
|
||||
| `identity_id` | integer | Yes | Identity internal ID |
|
||||
| `identity_uuid` | string | Yes | Identity UUID |
|
||||
| `name` | string | Yes | Identity name |
|
||||
| `propagate` | boolean | No | Auto-trigger Round 2 matching (default: true) |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/agents/identity/confirm" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "trace_id": 10, "identity_id": 42, "identity_uuid": "'"$IDENTITY_UUID"'", "name": "Cary Grant", "propagate": false}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "384b0ff44aaaa1f1",
|
||||
"trace_id": 10,
|
||||
"identity_uuid": "a9a90105...",
|
||||
"name": "Cary Grant",
|
||||
"steps": {
|
||||
"tkg_updated": true,
|
||||
"qdrant_updated": 150,
|
||||
"pg_updated": 150,
|
||||
"seed_added": true
|
||||
},
|
||||
"propagation": {
|
||||
"matched": 5,
|
||||
"message": "Propagation completed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Side Effects
|
||||
|
||||
1. TKG face_track node status → 'confirmed'
|
||||
2. Qdrant _faces: identity_uuid added to payload
|
||||
3. PG face_detections: identity_id set
|
||||
4. Trace centroid added to _seeds (source='propagation')
|
||||
5. Round 2 matching triggered (if propagate=true)
|
||||
|
||||
---
|
||||
*Updated: 2026-06-26 00:30:00*
|
||||
|
||||
@@ -284,7 +284,7 @@ def main():
|
||||
|
||||
if args.json:
|
||||
result = batch_confirm_from_json(args.file_uuid, args.json, propagate)
|
||||
elif args.trace_id and args.identity_id and args.identity_uuid and args.name:
|
||||
elif args.trace_id is not None and args.identity_id is not None and args.identity_uuid and args.name:
|
||||
result = confirm_single_trace(
|
||||
args.file_uuid,
|
||||
args.trace_id,
|
||||
|
||||
@@ -31,6 +31,10 @@ pub fn identity_agent_routes() -> Router<AppState> {
|
||||
"/api/v1/agents/identity/run",
|
||||
post(run_identity_handler),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/agents/identity/confirm",
|
||||
post(confirm_identity_handler),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -661,12 +665,80 @@ fn average_embeddings<'a>(embeddings: impl Iterator<Item = &'a Vec<f32>>) -> Vec
|
||||
/// Unknown: greedy stranger clustering (TH=0.40)
|
||||
/// Writes identity_ref/stranger_ref to Qdrant payload, TKG nodes, and face_detections.
|
||||
async fn match_faces_iterative(pool: &sqlx::PgPool, file_uuid: &str) -> anyhow::Result<usize> {
|
||||
tracing::warn!(
|
||||
"[FaceMatch] Face matching disabled - FaceEmbeddingDb removed. \
|
||||
TODO: Reimplement with _faces collection for {}",
|
||||
file_uuid
|
||||
use crate::core::processor::executor::PythonExecutor;
|
||||
use std::time::Duration;
|
||||
|
||||
let executor = PythonExecutor::new()?;
|
||||
|
||||
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
||||
.unwrap_or_else(|_| "/Users/accusys/momentry/output".to_string());
|
||||
|
||||
let output_path = std::path::PathBuf::from(&output_dir)
|
||||
.join(file_uuid)
|
||||
.join(format!("{}.identity_match_round1.json", file_uuid));
|
||||
|
||||
std::fs::create_dir_all(output_path.parent().unwrap()).ok();
|
||||
|
||||
let scripts_dir = executor.script_dir();
|
||||
let python_path = executor.python_path();
|
||||
let script_path = scripts_dir.join("identity_matcher.py");
|
||||
|
||||
let qdrant_url = std::env::var("QDRANT_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:6333".to_string());
|
||||
let qdrant_api_key = std::env::var("QDRANT_API_KEY")
|
||||
.unwrap_or_else(|_| "Test3200Test3200Test3200".to_string());
|
||||
let db_url = std::env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgresql://accusys@localhost:5432/momentry".to_string());
|
||||
|
||||
let mut cmd = tokio::process::Command::new(python_path);
|
||||
cmd.env("MOMENTRY_OUTPUT_DIR", &output_dir);
|
||||
cmd.env("DATABASE_SCHEMA", "public");
|
||||
cmd.env("MOMENTRY_DB_SCHEMA", "public");
|
||||
cmd.env("DATABASE_URL", &db_url);
|
||||
cmd.env("QDRANT_URL", &qdrant_url);
|
||||
cmd.env("QDRANT_API_KEY", &qdrant_api_key);
|
||||
cmd.arg(&script_path);
|
||||
cmd.arg("--file-uuid").arg(file_uuid);
|
||||
cmd.arg("--round").arg("1");
|
||||
cmd.arg("--mark-tkg");
|
||||
cmd.arg("--output").arg(&output_path);
|
||||
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
|
||||
tracing::info!("[FaceMatch] Starting identity_matcher for {}", file_uuid);
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if !output.status.success() {
|
||||
tracing::error!("[FaceMatch] identity_matcher failed with exit code: {:?}", output.status.code());
|
||||
tracing::error!("[FaceMatch] stderr: {}", stderr);
|
||||
tracing::error!("[FaceMatch] stdout: {}", stdout);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
tracing::info!("[FaceMatch] stdout: {}", stdout);
|
||||
|
||||
if !output_path.exists() {
|
||||
tracing::info!("[FaceMatch] No matches found for {}", file_uuid);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&output_path)?;
|
||||
let result: serde_json::Value = serde_json::from_str(&content)?;
|
||||
|
||||
let matched = result.get("matched").and_then(|v| v.as_i64()).unwrap_or(0) as usize;
|
||||
let tkg_updated = result.get("tkg_nodes_updated").and_then(|v| v.as_i64()).unwrap_or(0) as usize;
|
||||
|
||||
tracing::info!(
|
||||
"[FaceMatch] Round 1 for {}: {} matches, {} TKG nodes updated",
|
||||
file_uuid, matched, tkg_updated
|
||||
);
|
||||
Ok(0)
|
||||
|
||||
Ok(matched)
|
||||
}
|
||||
|
||||
/// Fallback: PostgreSQL-based matching (disabled - embedding column removed)
|
||||
@@ -1011,6 +1083,142 @@ async fn run_identity_handler(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConfirmIdentityRequest {
|
||||
file_uuid: String,
|
||||
trace_id: i32,
|
||||
identity_id: i32,
|
||||
identity_uuid: String,
|
||||
name: String,
|
||||
propagate: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ConfirmIdentityResponse {
|
||||
success: bool,
|
||||
file_uuid: String,
|
||||
trace_id: i32,
|
||||
identity_uuid: String,
|
||||
name: String,
|
||||
steps: serde_json::Value,
|
||||
propagation: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
async fn confirm_identity_handler(
|
||||
State(_state): State<AppState>,
|
||||
Json(req): Json<ConfirmIdentityRequest>,
|
||||
) -> Result<Json<ConfirmIdentityResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
use crate::core::processor::executor::PythonExecutor;
|
||||
|
||||
let executor = PythonExecutor::new().map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"success": false, "message": format!("PythonExecutor error: {}", e)})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let scripts_dir = executor.script_dir();
|
||||
let python_path = executor.python_path();
|
||||
let script_path = scripts_dir.join("confirm_identity.py");
|
||||
|
||||
let qdrant_url = std::env::var("QDRANT_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:6333".to_string());
|
||||
let qdrant_api_key = std::env::var("QDRANT_API_KEY")
|
||||
.unwrap_or_else(|_| "Test3200Test3200Test3200".to_string());
|
||||
let db_url = std::env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgresql://accusys@localhost:5432/momentry".to_string());
|
||||
let db_schema = std::env::var("DATABASE_SCHEMA")
|
||||
.unwrap_or_else(|_| "dev".to_string());
|
||||
|
||||
let propagate = req.propagate.unwrap_or(true);
|
||||
|
||||
let mut cmd = tokio::process::Command::new(python_path);
|
||||
cmd.env("DATABASE_URL", &db_url);
|
||||
cmd.env("DATABASE_SCHEMA", &db_schema);
|
||||
cmd.env("MOMENTRY_DB_SCHEMA", &db_schema);
|
||||
cmd.env("QDRANT_URL", &qdrant_url);
|
||||
cmd.env("QDRANT_API_KEY", &qdrant_api_key);
|
||||
cmd.arg(&script_path);
|
||||
cmd.arg("--file-uuid").arg(&req.file_uuid);
|
||||
cmd.arg("--trace-id").arg(req.trace_id.to_string());
|
||||
cmd.arg("--identity-id").arg(req.identity_id.to_string());
|
||||
cmd.arg("--identity-uuid").arg(&req.identity_uuid);
|
||||
cmd.arg("--name").arg(&req.name);
|
||||
|
||||
if !propagate {
|
||||
cmd.arg("--no-propagate");
|
||||
}
|
||||
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
|
||||
tracing::info!(
|
||||
"[ConfirmIdentity] Starting for {} trace {} -> {} ({})",
|
||||
req.file_uuid, req.trace_id, req.identity_uuid, req.name
|
||||
);
|
||||
|
||||
let output = cmd.output().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"success": false, "message": format!("Command failed: {}", e)})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if !output.status.success() {
|
||||
tracing::error!("[ConfirmIdentity] Script failed with exit code: {:?}", output.status.code());
|
||||
tracing::error!("[ConfirmIdentity] stderr: {}", stderr);
|
||||
tracing::error!("[ConfirmIdentity] stdout: {}", stdout);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"success": false,
|
||||
"message": format!("Script failed: {}", stderr),
|
||||
"stdout": stdout.to_string(),
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
tracing::info!("[ConfirmIdentity] stdout: {}", stdout);
|
||||
|
||||
let json_start = stdout.find('{');
|
||||
if json_start.is_none() {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"success": false,
|
||||
"message": "No JSON output found",
|
||||
"stdout": stdout.to_string(),
|
||||
})),
|
||||
));
|
||||
}
|
||||
let json_str = &stdout[json_start.unwrap()..];
|
||||
|
||||
let result: serde_json::Value = serde_json::from_str(json_str).map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"success": false,
|
||||
"message": format!("Failed to parse output: {}", e),
|
||||
"stdout": stdout.to_string(),
|
||||
"json_str": json_str.to_string(),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(ConfirmIdentityResponse {
|
||||
success: result.get("status").and_then(|v| v.as_str()) == Some("success"),
|
||||
file_uuid: req.file_uuid,
|
||||
trace_id: req.trace_id,
|
||||
identity_uuid: req.identity_uuid,
|
||||
name: req.name,
|
||||
steps: result.get("steps").cloned().unwrap_or(serde_json::json!({})),
|
||||
propagation: result.get("propagation").cloned(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Read all TMDb identities with profile photos, extract face embeddings, store in Qdrant as seeds.
|
||||
pub async fn generate_seed_embeddings(db: &PostgresDb) -> anyhow::Result<usize> {
|
||||
tracing::warn!(
|
||||
|
||||
Reference in New Issue
Block a user