{"agent_id":"outlet-dedupe","agent_version":"0.1.0","display_name":"Dedupe — outlets and assets","description":"Photo-based deduplication for CPG field data. Outlets: duplicate outlet records via storefront-photo similarity and geometry, multilingual names, geo, phone, GSTIN and vision adjudication. Assets: one physical cooler or display unit logged under multiple outlet records, found image-led from audit / image-recognition photos with no name or GPS required. Both ship reviewable photo evidence.","owner_squad":"fieldassist-product","tier":"M","capacity":{"sustained_rps":300,"peak_rps":500,"max_concurrent_runs":100,"p95_latency_ms":310,"rate_limit_per_tenant_rps":20,"load_tested_at":"2026-07-01T21:36:24Z","load_tested_to_rps":300,"verified":true,"notes":"Measured 2026-07-01 on single-process uvicorn (local dev Mac) via closed-loop httpx: trigger/action path (POST /v1/runs, dry_run) sustained ~305 rps @ p95 306 ms at 32 concurrent, 0 errors; authed DB read (/v1/runs) ~331 rps; public read ~530 rps peak; throughput saturates beyond ~32 concurrent on one process. Figures cover the API front door only — a full dedupe RUN is minutes (VLM-bound: a 175-outlet FA run took ~25 min; metadata/geo-only runs of ~140 outlets finish in <2 s). max_concurrent_runs and rate_limit_per_tenant_rps are production worker-pool / gateway targets and are not yet enforced in-process. Production (multi-worker Azure) will scale higher."},"slo":{"availability":0.995},"actions":[{"name":"dedupe_outlets","description":"Run multi-signal outlet deduplication on an uploaded file, a staged file, or outlets fetched live from the FieldAssist outlet master (source_type=fa_outlet_master).","input_schema":{"type":"object","required":["name","source_type"],"properties":{"name":{"type":"string"},"input_path":{"type":"string"},"source_type":{"type":"string","enum":["file_upload","staged_file","fa_outlet_master"],"description":"fa_outlet_master pulls company-scoped live FieldAssist outlets; staged_file/file_upload use normalized files."},"fa_scope":{"type":"object","description":"For source_type=fa_outlet_master. Optional geography filter within the company; empty = whole company.","properties":{"region_ids":{"type":"array","items":{"type":"string"}},"territory_ids":{"type":"array","items":{"type":"string"}},"beat_ids":{"type":"array","items":{"type":"string"}},"include_inactive":{"type":"boolean"}}},"company_context":{"type":"object","description":"fa_outlet_master is company-scoped. The company is taken from the trusted bearer token; in dev/demo it may be selected via the X-Company-ID header when allow_dev_company_override is enabled.","properties":{"company_id":{"type":"integer"},"source":{"type":"string","enum":["bearer_token","dev_header"]}}},"params":{"type":"object","description":"Dedupe parameters such as use_vlm, vlm_swap_check, require_vlm_confirm, max_workers, distance_threshold_m, and blocking."}}},"output_schema":{"type":"object","required":["run_id","status"],"properties":{"run_id":{"type":"string"},"status":{"type":"string"}}},"side_effects":["dedupe.run_created","dedupe.evidence_generated"],"reversible":true,"async":true},{"name":"dedupe_assets","description":"Find one physical asset (cooler / display unit) logged under multiple outlet records, from the field photos an audit or image-recognition programme already collects. Image-led: no outlet name or GPS required. Answers 'how many of these audits are the same asset photographed again?'","input_schema":{"type":"object","required":["name","source_type"],"properties":{"name":{"type":"string"},"input_path":{"type":"string"},"source_type":{"type":"string","enum":["file_upload","staged_file"],"description":"An audit/IR export with one row per capture."},"params":{"type":"object","description":"column_mapping accepts the asset roles image_url and block_key (required), plus record_id, outlet_id, asset_name, asset_code, capture_date. use_vlm defaults ON — asset-tag evidence is read during adjudication, so turning it off loses an entire evidence tier."}}},"output_schema":{"type":"object","required":["run_id","status"],"properties":{"run_id":{"type":"string"},"status":{"type":"string"}}},"side_effects":["dedupe.run_created","dedupe.evidence_generated"],"reversible":true,"async":true}],"surfaces":{"api":"https://dedupe.fieldassist.io","mcp":"https://dedupe.fieldassist.io/mcp","ui":"https://dedupe.fieldassist.io","cli":"dedupe"},"discovery":{"public":true,"auth_required":false,"manifest":"/.well-known/agent.json","a2a_card":"/.well-known/agent-card.json","human_contract":"/docs/orchestrator_integration.md"},"auth":{"type":"bearer","required_for_actions":true,"production_requirement":"Set DEDUPE_REQUIRE_AUTH=1 before exposing this agent to an orchestrator.","token_claims":{"tenant_id":"Required tenant boundary for all protected APIs.","company_id":"Required for FieldAssist live fetches unless a dev override is explicitly enabled.","source_system":"Expected to be fa for FieldAssist-native runs.","permissions":["dedupe:run","dedupe:review","dedupe:retire:preview","dedupe:retire:apply"]},"tenant_source":"bearer_token","tenant_id_in_body":"Optional echo only; a mismatch with the token is rejected with 403 tenant_mismatch.","permissions":{"mode":"enforced_when_scoped","empty_permissions_claim":"treated as full-access dev token"},"company_scope":{"source":"bearer_token.company_id","dev_override_header":"X-Company-ID","dev_override_rule":"accepted only when enabled and the bearer token does not already carry company_id"}},"endpoints":{"preflight":{"companies":"GET /v1/integrations/fa/companies?q=<search>","hierarchy":"GET /v1/integrations/fa/hierarchy?company_id=<id>","fa_preview":"POST /v1/integrations/fa/outlets/preview"},"trigger":{"rest":{"method":"POST","path":"/v1/runs","required_headers":["Authorization: Bearer <token>","Idempotency-Key: <stable-key>"]},"a2a":{"method":"POST","path":"/a2a","methods":["message/send","tasks/send"],"params_shape":"params.action_request","idempotency":"params.idempotency_key or Idempotency-Key header"}},"await":{"poll":"GET /v1/runs/{run_id}","events":"GET /v1/runs/{run_id}/events?access_token=<token>","callback":"Provide callback_url in the action request when the caller wants a completion webhook."},"results":{"clusters":"GET /v1/runs/{run_id}/clusters","review_pairs":"GET /v1/runs/{run_id}/pairs?review_queue=true","retirements":"GET /v1/runs/{run_id}/retirements","export":"GET /v1/runs/{run_id}/export.xlsx?access_token=<token>"}},"runtime":{"async":true,"statuses":["queued","running","succeeded","failed","cancelled"],"a2a_task_states":["submitted","working","completed","failed","canceled","input_required"],"idempotency_required":true,"idempotency_header":"Idempotency-Key","streaming":false,"push_notifications":false,"local_auto_worker":"DEDUPE_LOCAL_AUTO_WORKER=1 is for local development only; production should run the worker deployable.","state":"Durable SQLite/Parquet/filesystem locally; production replacements are SQL/blob/queue/observability."},"fa_native":{"source_type":"fa_outlet_master","active_outlets_only":true,"default_use_vlm_when_omitted":true,"explicit_use_vlm_false_honored":true,"max_fetch_outlets_per_run":50000,"scope":{"whole_company":"omit fa_scope ids","region_ids":"uses FA BeatId hierarchy under selected regions","territory_ids":"uses FA BeatId hierarchy under selected territories","beat_ids":"uses FA BeatId directly when supplied"},"images":{"source":"FA ImageId resolved to static outlet-image URL when available","degradation":"Rows without usable photos still run metadata and geo dedupe; VLM is skipped for those pairs."}},"vision_adjudication":{"default_model":"gemini-3.1-flash-lite","same_outlet_max_distance_m":50,"default_vlm_concurrency":12,"swap_check_default":true,"require_vlm_confirm_default":true,"degradation":"If Gemini is not configured or all candidate photos are unusable, the run completes with metadata/geo signals and emits a vlm_warning timeline event.","decision_role":"VLM adjudicates candidate pairs after blocking; it is not called on every possible outlet pair."},"known_limits":[{"area":"capacity","detail":"load_tested_to_rps=300 is a verified LOCAL single-process measurement (2026-07-01), not a production figure. It reflects the API front door; a full dedupe run is minutes (VLM-bound). max_concurrent_runs and rate_limit_per_tenant_rps remain unenforced design targets. Re-test on the production worker deployment."},{"area":"fa_fetch","detail":"Live FA fetches are capped at 50,000 outlets per run to protect local execution."},{"area":"hierarchy","detail":"Current live hierarchy is geography-based. Employee/rep hierarchy is not yet exposed as a first-class scope."},{"area":"stdio_mcp","detail":"HTTP/A2A are the supported orchestrator surfaces. Stdio MCP is local-dev only until the FastAPI/Starlette dependency stack is pinned compatibly."}],"tradeoffs":[{"choice":"FA-native runs default to VLM on","reason":"Prevents classical image/name/geo matches from auto-merging visually different outlets.","cost":"Higher run time and Gemini cost on candidate pairs with usable photos."},{"choice":"50m same-outlet distance cap","reason":"A visually-same verdict beyond this radius is held back from confident auto-merge.","cost":"Some legitimate GPS-drift duplicates may move to review instead of auto-merge."},{"choice":"No phone-only corroboration","reason":"Shared or reused phone numbers can chain unrelated outlets into one cluster.","cost":"Some phone-supported duplicates need stronger name/GSTIN/VLM evidence."}]}