Spaces:
Running
Running
File size: 13,343 Bytes
fdeeb34 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | # Vera Engagement Framework β Research: Current Merchant Data Access
**Status**: Research notes β companion to `engagement-design.md`.
**Last updated**: 2026-04-26
**Scope**: How the existing system loads merchant + customer data at conversation init and during a turn. Used to inform what the new framework must build vs. adapt.
---
## TL;DR
Two distinct paths exist today β **merchant-facing** (Vera β Dr. Meera) and **customer-facing** (a customer asks Vera *about* Dr. Meera). They share infrastructure (vera-mcp + merchant-support-mcp + Redis) but compose context very differently.
Most of what the proposed `MerchantContext` needs **already exists in scattered form** (`_merchant_snapshot`, `_behavioral_profile`, `_session_scenario`). The genuinely new pieces are:
- `CategoryContext` (no existing equivalent)
- `TriggerContext` (no normalized abstraction today β every cron emits ad-hoc payloads)
- `CustomerContext.relationship` (visit history aggregation doesn't exist)
- `EngagementComposer` (no shared composer β each agent has its own prompt-builder)
The aryan dependency for `category` and `locality` is the soft underbelly β every customer-info-pack call hits aryan synchronously.
---
## Path 1 β Merchant-facing (`VeraMerchantAgent`)
### Init (per session, instance-level)
`agents/vera/merchant_agent.py:402` β `__init__`:
- Spawns its own `vera_mcp_client` (`HTTPMCPClient` β `VERA_MCP_SERVER_URL`, default `vera-mcp:8000`).
- Inherits `BaseAgent._merchant_mcp_client` β class-level shared singleton pointing to `merchant-support-mcp` (`https://search5.magicpin.com/staging/mcp-server/mcp`).
- **No merchant data is loaded at init** β agent doesn't know who it's talking to yet.
### Per-turn (every inbound message)
`agents/vera/merchant_agent.py:2171` β `handle_input(agent_input)`:
1. Extract `merchant_id` from `metadata.context_data.merchant_id`, falling back to regex on the message body (`\d{7,}`).
2. Fire 2 parallel tasks:
- `_detect_state(merchant_id)` β GBP connection + subscription status checks.
- `_prefetch_merchant_context(merchant_id)` β full snapshot + behavioral profile.
### `_prefetch_merchant_context` β `merchant_agent.py:740`
Cache-first design:
1. **Redis check**: `vera:merchant_ctx:{merchant_id}` β TTL **30 min**. Hit β return immediately, skip everything below.
2. Parallel via `asyncio.gather`:
- `vera_merchant_snapshot` (vera-mcp tool)
- `vera_get_merchant_profile` (vera-mcp tool)
3. If snapshot is empty: fall back to `get_aggregated_unassociated_merchant_data` (merchant-support-mcp) β basic GBP health, no commercial data. Wrap it in a snapshot-shaped envelope.
4. Sequential enrichments:
- `_prefetch_product_context` β fans out to `vera_get_subscription_context`, `vera_get_performance_summary`, and (only if DA subscribed) `da_get_campaign_context`. ~2KB total, embedded into snapshot.
- `_enrich_snapshot_with_pricing` β direct HTTP to `https://vera.magicpin.com/api/v1/merchant/pricing/get?mid=...` (vera-mcp's pricing path is unreachable locally). Mounted into snapshot as `pricing_recommendation`.
5. Stuff result into Redis at the same key, TTL 30 min.
Final state: `self._merchant_snapshot` and `self._behavioral_profile` are populated. `_get_system_prompt()` reads from these and serializes the snapshot directly into the LLM system prompt (`merchant_agent.py:996`).
### What `vera_merchant_snapshot` actually fetches
`vera-mcp/src/services/merchant_snapshot.py:51` β `build_merchant_snapshot()`:
1. **Resolve identity** via `gbp_resolve_merchant` β gets `place_id`, `location_name`, `merchant_title`.
2. **One async fan-out** of up to 13 tool calls in parallel:
| Bucket | Tools |
|---|---|
| Merchant-level (no GBP needed) | `vera_get_subscription_status`, `vera_get_pricing_by_merchant`, `vera_get_merchant_pain_points`, `vera_get_merchant_offer`, `vera_get_merchant_config`, `vera_get_onboarding_status`, `vera_get_enhancement_suggestions` |
| Location-level (skipped if no place_id) | `gbp_get_location`, `gbp_get_profile_completeness`, `gbp_get_performance_summary`, `gbp_get_search_keywords`, `gbp_get_review_stats`, `gbp_list_posts` |
3. Composes 7 sections: `identity`, `profile`, `reputation`, `growth`, `commercial`, `conversation_hooks`, `issues`.
> **Note**: `category` and `locality` are not first-class fields on the snapshot β they're buried inside `profile.business_info` (from `gbp_get_location`) and surfaced via `aryan_client.get_merchant_v2()` only when `vera_get_customer_info_pack` is called.
### Mid-conversation tool calls
The LLM gets the full toolset from `MERCHANT_MCP_ALLOWED_TOOLS` (declared on the agent class) merged with `_vera_mcp_tools_cache`. Mid-turn tool calls flow through:
- `self.vera_mcp_client.call_tool(name, args)` β for vera-mcp tools (HTTP)
- `BaseAgent._merchant_mcp_client.call_tool(name, args)` β for merchant-support-mcp tools (HTTP)
No re-prefetch of the snapshot mid-turn. The agent relies on the prefetched snapshot + LLM-initiated lookups when it needs fresh data.
---
## Path 2 β Customer-facing (`CustomerIncomingAgent`)
`agents/vera/customer_incoming_agent.py:91` β different agent, different state model.
### Init + per-turn
The instance carries:
- `_merchant_id` β the merchant the customer is asking about
- `_merchant_data`, `_merchant_name`, `_category`, `_offers`, `_jd_info` β about the **business**
- `_customer_phone`, `_customer_profile` β about the **customer** (the one chatting)
- `_chat_history`, `_session_state` β conversation continuity
### Merchant data load β `_load_merchant_data` (`customer_incoming_agent.py:221`)
Single shape, no Redis cache:
1. **Primary call**: `vera_get_customer_info_pack(merchant_id)` β one MCP call returns `business_info` + `reviews` + `photos` + `offers` + `metadata` in one shot. The aggregated customer-facing endpoint.
2. **Fallback chain** if `info_pack` is empty:
- `get_unassociated_merchant_data` β basic merchant info
- `vera_list_merchant_offers` β separate offer list
3. **Supplementary** (non-blocking): `vera_get_merchant_jd_info` β JustDial crawl data for additional name / address / phone.
Caching is at the request level (in-memory on the agent instance) β re-using the same `merchant_id` in a session avoids re-fetching.
### What `vera_get_customer_info_pack` does
`vera-mcp/src/tools/merchant_info.py:188`:
1. **Resolve place_id**: `_resolve_place_id(merchant_id)` β chain of MongoDB (`gbp_status`) β local JSON β `aryan_client.get_mapping()`.
2. **Parallel fetch**:
- GBP data via `get_or_fetch(place_id)` β 24h cache in MongoDB, falls through to Google Places API
- `_get_active_offers(merchant_id)` β direct MongoDB read of `offers` collection where `status=active`
- `_get_merchant_metadata(merchant_id)` β `aryan_client.get_merchant_v2()` β returns `name`, `category`, `locality`
3. **Last-resort fallback**: `_fetch_embed_data(place_id)` β vera REST `/embed` endpoint if everything else came back empty.
### Customer profile (the *caller*)
`_customer_profile` is loaded separately β populated in `_init_customer_data` from past conversation tickets keyed off `_customer_phone`. There's no rich CRM behind it today; it's mostly conversation continuity (last visit, last topic).
---
## Cross-cutting infrastructure
| Concern | Implementation |
|---|---|
| MCP transport | `HTTPMCPClient` (vera client) β keeps a session, calls `/mcp/tools/{name}` HTTP POST |
| Auth | Not required for challenge bot endpoints |
| Cache key for merchant context | `vera:merchant_ctx:{merchant_id}` β Redis, TTL 30 min, written by `_prefetch_merchant_context` |
| Cache key for GBP data | `gbp_health_report:{place_id}` β MongoDB, TTL 24h, in vera-mcp |
| Snapshot freshness for sends | Whatever's in Redis β not refreshed on send unless agent is in active conversation |
| Source of truth for `category` | aryan `get_merchant_v2` API (via `aryan_client`) β used in `_get_merchant_metadata` |
| Source of truth for `name` | aryan first, GBP `business_info` second |
---
## Observations relevant to the engagement framework
### What already exists
1. **Most of `MerchantContext` already exists** β spread across `_merchant_snapshot`, `_behavioral_profile`, `_session_scenario`, `_jd_info`. A `MerchantContext.from_existing(agent_state)` adapter could load ~80% of the fields without any new fetching.
2. **The customer agent already has half a `CustomerContext`** β `_customer_phone` + `_customer_profile` give us identity + conversation continuity. Missing: visit history, services received, lapse state.
3. **Two MCP servers, one orchestration** β vera-mcp (instance-level) for vera tools, merchant-support-mcp (class-level shared) for fallback merchant tools. The composer can just consume whatever `MerchantContext` already collected β no new MCP wiring required.
4. **Cache TTL of 30 min is fine** for engagement nudges that fire daily/weekly. Redis hits during conversation are plenty fresh for composition.
### What does NOT exist
1. **No `CategoryContext`.** Category is just a string buried in metadata. Voice rules, peer benchmarks, knowledge digests β none of it exists. Biggest greenfield area, but also the most leveraged (one CategoryContext serves all merchants in the vertical).
2. **No `customer_aggregate` field on the merchant snapshot.** No pipeline today aggregates per-merchant customer roster stats (active count, lapsed count, retention rate).
3. **No `TriggerContext` abstraction.** Every nudge type today has its own cron + its own fetch logic + its own send code. The proposed `TriggerContext` is the genuinely new architectural primitive; everything else is reorganization.
4. **No visit-history aggregation per (merchant, customer_phone).** Required for `CustomerContext.relationship`. BOTOPS chat history has the raw data; no derived view exists.
5. **No shared composer.** Each agent has its own prompt builder embedded in `_get_system_prompt()`. The proposed `EngagementComposer` is net new.
### Operational risks to mitigate
1. **Aryan is the synchronous bottleneck.** `aryan_client.get_merchant_v2()` is the only path to category and locality, and it's a remote HTTP call. If aryan is slow, every customer-info-pack call is slow. Worth caching aryan responses per merchant for ~24h before scaling engagement frequency.
2. **The 30-min Redis cache is keyed per-merchant** β fine for in-conversation reuse, but a daily engagement cron will miss this cache 100% of the time and pay the full snapshot-build cost per send. Consider a longer-TTL background-warmed cache for the engagement loop specifically.
3. **No version tracking on prompts today.** Every send loses the prompt-version provenance. The composer should record prompt version + context hash on every send so we can replay and A/B.
---
## Concrete recommendation for Phase 1 of the framework
Phase 1 is mostly an **adapter layer**, not new infrastructure:
| Layer | Effort | What it actually does |
|---|---|---|
| `CategoryContext` | Net new | Build for dentistry first (offer catalog, voice, peer stats, weekly digest, patient-content seed) |
| `MerchantContext` | Adapter | Wrap existing `_merchant_snapshot` + `_behavioral_profile` + a new `_customer_aggregate` derived from BOTOPS chat history |
| `TriggerContext` | Net new abstraction | Normalize the payloads existing crons emit into a single shape; new triggers (research_digest, recall_due) emit it natively |
| `CustomerContext` | Partial adapter | Wrap existing `_customer_profile`, plus a new visit-history aggregator |
| `EngagementComposer` | Net new | Single LLM-prompted module with versioned prompt; consumes the above |
Phase 1 should NOT require modifying any existing agent. The adapter reads from existing state; the composer is a new module that runs in parallel; the new triggers (research digest, recall) are new crons that don't touch the existing matrix-followup or campaign-engagement loops.
---
## Appendix: file pointers for follow-up implementation
- `agents/vera/merchant_agent.py:402` β VeraMerchantAgent init
- `agents/vera/merchant_agent.py:740` β `_prefetch_merchant_context` (the main load)
- `agents/vera/merchant_agent.py:899` β `_prefetch_product_context` (subscription + DA campaign + perf summary)
- `agents/vera/merchant_agent.py:996` β system-prompt builder reads `_merchant_snapshot`
- `agents/vera/merchant_agent.py:2171` β `handle_input` per-turn entry
- `agents/vera/customer_incoming_agent.py:91` β CustomerIncomingAgent class
- `agents/vera/customer_incoming_agent.py:221` β `_load_merchant_data` (single info-pack call + fallbacks)
- `agents/base_agent.py:80-82` β MERCHANT_MCP_SERVER_URL config
- `agents/base_agent.py:237-244` β class-level shared `_merchant_mcp_client`
- `vera-mcp/src/tools/merchant_snapshot.py` β `vera_merchant_snapshot` tool
- `vera-mcp/src/services/merchant_snapshot.py:51` β `build_merchant_snapshot` (the 13-call fan-out)
- `vera-mcp/src/tools/merchant_info.py:188` β `vera_get_customer_info_pack` tool
- `vera-mcp/src/tools/merchant_info.py:30` β `_resolve_place_id` chain (MongoDB β JSON β aryan)
- `vera-mcp/src/services/aryan_client.py` β aryan HTTP client (`get_merchant_v2`, `get_mapping`)
|