Spaces:
Running
Download engagement-design.md from IDKHowToCodeFr/vera-engine: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/spaces/IDKHowToCodeFr/vera-engine/resolve/main/engagement-design.md
- Command line
-
hf download hf://spaces/IDKHowToCodeFr/vera-engine/engagement-design.md
-
curl -L -o engagement-design.md https://huggingface.co/spaces/IDKHowToCodeFr/vera-engine/resolve/main/engagement-design.md
Vera Engagement Framework β Design
Status: Draft proposal β not implemented. Last updated: 2026-04-26 Scope: How Vera composes every outbound conversation, both merchant-facing and (on-behalf-of-merchant) customer-facing.
Why this exists
The current engagement loop (agents/vera/followup/) is a collection of ad-hoc nudges β campaign templates, performance reminders, conversation requeues. Each was built point-to-point, with hardcoded assumptions baked in. Adding a new nudge type means re-writing prompts, finding new data, and re-implementing send/dedup logic.
Two problems this creates:
Functional nudges are inherently low-frequency. A merchant only has a handful of "broken things" or "events" per month. To engage them 3-5Γ/week, we need a framework that supports curiosity-driven, knowledge-driven, and customer-driven conversations β not just "fix-this" reminders.
Same engine should drive merchant-facing AND customer-facing messages. When Dr. Meera's clinic sends a recall reminder to her patient Priya, Vera does the work β but the framework for composing that message should be the same one that produces the research digest Vera sends to Dr. Meera herself.
This doc proposes a 4-context composition framework that separates the slow-changing (category knowledge) from the fast-changing (per-merchant, per-customer state) and the event-driven (triggers).
The 4 contexts
Every Vera message = compose(category, merchant, trigger, customer?).
| Context | Question it answers | Refresh cadence | Owner | Shared across |
|---|---|---|---|---|
| Category | How do we talk to this type of business? What do they know, care about, offer, get regulated on? | Weekly (digest), monthly (voice/catalog) | Domain curator | All merchants in the vertical |
| Merchant | Who is this specific business, how are they doing, what's in their history with us? | Daily (perf), real-time (conversation) | Snapshot + conversation sync | Just this merchant |
| Trigger | Why am I messaging right now? What specific event prompts this? | Per-event | Event detectors | This one send |
| Customer | Who is the merchant's customer, and what's their state with this merchant? | Per-visit / per-interaction | Merchant CRM sync | Just this customer |
The composer takes these 4 (3 if no customer scope) and produces the message body, template parameters, CTA, and suppression key. Nothing is hardcoded into the composer β all variation comes from the contexts.
βββββββββββββββββββ
Category ββββΊβ β
Merchant ββββΊβ Composer (LLM) βββββΊ message {body, params, cta, send_as}
Trigger ββββΊβ β
Customer? ββββΊβ β
βββββββββββββββββββ
Layer details
CategoryContext
Slow-changing knowledge pack per vertical. One per category (dentists, salons, gyms, restaurants, car_service, ...). Bootstrap is labor-intensive (needs domain expertise); ongoing maintenance is mostly the weekly digest.
Fields:
slugβ"dentists"offer_catalogβ canonical service+price patterns from vera-mcp + category-specific extensions. Prefer"Dental Cleaning @ βΉ299"over"Flat 20% OFF". Service+price is more compelling than discount.voiceβ tone, vocabulary, taboos. For dentists: technical terms welcome ("fluoride varnish at 3-month recall"), legal taboos ("cure","guaranteed"), peer tone not hype.peer_statsβ city-scoped benchmarks: avg rating, avg reviews, typical CTR, typical patient volume. Anchors comparative messages.digestβ this week's curated research / compliance / CDE / tech / peer-practice items, with source citations. Sourced from a per-category source list (PubMed dental RSS, JIDA, IDA Delhi calendar, DCI circulars, Dental Tribune India, Google Trends for dental queries, vendor press releases).patient_content_libraryβ items written at patient-reading level that the merchant can reshare with their own customers (powers thePRO_PATIENT_CONTENTfamily).seasonal_beatsβ cycles like "exam-stress bruxism spikes Nov-Feb" that cue category-specific timing.trend_signalsβ Google Trends + Practo-style query data showing what patients in this vertical are searching for.
MerchantContext
Per-merchant state. Refreshed daily for performance; real-time for conversation history.
Fields:
merchant_ididentityβ name, place_id, locality, city, verified, languagessubscriptionβ status, days remaining, planperformanceβ views/calls/CTR/leads/directions, 30d + 7d deltasoffersβ active + paused, sourced from vera-mcp's offers collection (and eventually the as-yet-undefined "real" offer source-of-truth)conversation_historyβ last N turns w/ Vera, with engagement tags (replied, ignored, unsubscribed-from-topic)customer_aggregateβ derived stats over the merchant's customer roster (active count, lapsed count, retention rate). Not individual customers β aggregates only.signalsβ derived flags:stale_posts,ctr_below_peer_median,customer_lapse_rate_high,dormant_with_vera, ...
TriggerContext
The event that prompts this specific message. Two families:
- External β happens outside the merchant's account. News, weather, festival, regulation change, category-trend movement, competitor opens nearby, weekly research digest release.
- Internal β happens within the merchant's account or customer roster. Performance dip/spike, milestone hit, dormancy threshold crossed, customer lapse threshold crossed, appointment due, review pattern emerged, scheduled-recurring nudge.
Fields:
idβ uniquescopeβmerchant|customerkindβresearch_digest,recall_due,perf_spike,competitor_opened,festival, ...sourceβexternal|internalpayloadβ kind-specific data (e.g., forrecall_due:{patient_id, last_visit, due_date})urgencyβ 1-5; ranks against other queued triggerssuppression_keyβ used by Redis dedup to prevent re-sendsexpires_atβ after which the trigger is stale
CustomerContext
Only populated when scope=customer. Per-customer state with this specific merchant.
Fields:
customer_idmerchant_ididentityβ name, phone, language preferencerelationshipβ first_visit, last_visit, visits_total, services received, lifetime valuestateβnew|active|lapsed_soft(3-6mo) |lapsed_hard(6mo+) |churned(12mo+)preferencesβ preferred slot times (derived from booking history), preferred channel, opt-in statusconsentβ when did they opt in, via what mechanism, scope of consent
Composer
Single LLM-prompted module. Takes the 4 contexts as input. Produces:
bodyβ the WhatsApp message bodytemplate_paramsβ params to fill an approved Kaleyra template (used only for the first touch in a session window)ctaβ the binary or open-ended asksuppression_keyβ for the trigger-level dedupsend_asβ"vera"for merchant-facing,"merchant_on_behalf"for customer-facing
The composer prompt is the single point of failure. It must be:
- Versioned (
composer_v1,composer_v2, ...) - A/B-testable
- Auditable (we can replay any past message and see all 4 input contexts)
Different kind values may use different prompt variants β e.g., research_digest needs source-citation framing, recall_due needs slot-offering framing, competitor_opened needs voyeur-curiosity framing. The composer dispatches by kind.
Worked example 1: merchant-facing
Merchant: Dr. Meera, Lajpat Nagar, Delhi Trigger: weekly dentistry research digest just landed
Inputs:
| Context | Key values used |
|---|---|
| Category (dentists) | voice=peer/technical; digest_top_item="JIDA Oct trial: 3-mo fluoride recall cuts caries 38% better"; peer_stat="South-Delhi solo CTR median 3.0%" |
| Merchant (Dr. Meera) | CTR 2.1% (below peer); ran "Deep Cleaning βΉ499" 2mo ago; 78 lapsed patients; last Vera touch 2d ago (engaged) |
| Trigger | kind=research_digest_release, scope=merchant, urgency=2, source=external, suppression_key=research:dentists:2026-W17 |
| Customer | (not populated) |
Composed message:
Dr. Meera, JIDA's Oct issue landed. One item relevant to your high-risk adult patients β 2,100-patient trial showed 3-month fluoride recall cuts caries recurrence 38% better than 6-month. Worth a look (2-min abstract). Want me to pull it + draft a patient-ed WhatsApp you can share? β JIDA Oct 2026 p.14
Why it works:
- Category drives voice (technical, source-cited, peer tone)
- Merchant drives specificity ("your high-risk adult patients" β derived from her customer aggregate)
- Trigger drives the hook (this week's digest, not a promo ask)
- No customer context needed; this is merchant-to-Vera
Worked example 2: customer-facing (same framework)
Merchant: Dr. Meera (same) Customer: Priya β patient since 2025-11, last visit 2026-05 (cleaning + whitening), prefers weekday evenings, opted-in to reminders Trigger: 6-month recall window opens
Inputs:
| Context | Key values used |
|---|---|
| Category (dentists, customer-facing) | voice=warm-clinical; taboos=no medical claims, no "guaranteed"; recall framing pattern |
| Merchant (Dr. Meera) | active offer Dental Cleaning @ βΉ299; available slots Wed 6pm + Thu 5pm (next 7d); WhatsApp Business number |
| Trigger | kind=recall_due, scope=customer, urgency=3, source=internal, payload={patient_id: priya, last_visit: 2026-05, due_date: 2026-11} |
| Customer (Priya) | name + phone; lapsed_soft state; preferred=weekday evening; consent active; language=Hindi-English mix |
Composed message (sent from Dr. Meera's WhatsApp number, drafted by Vera):
Hi Priya, Dr. Meera's clinic here π¦· It's been 5 months since your last visit β your 6-month cleaning recall is due. Apke liye 2 slots ready hain: Wed 6 Nov, 6pm ya Thu 7 Nov, 5pm. βΉ299 cleaning + complimentary fluoride. Reply 1 for Wed, 2 for Thu, or tell us a time that works.
Why it works:
- Category sets the legal/clinical voice constraints
- Merchant provides the actual catalog price + actual open slots from the schedule
- Trigger provides the recall payload (last_visit, due_date)
- Customer drives personalization (name, language mix, evening preference)
Same composer. Different context inputs. Two completely different conversations.
Engagement loops this enables
Once the framework exists, every loop is just a small cron that emits TriggerContext instances. The composer handles the rest.
| Loop | Emits trigger kinds | Scope |
|---|---|---|
News/weather scanner (already built β see agents/vera/followup/event_sources.py) |
external (festival, heatwave, fuel, IPL, monsoon, news) |
merchant |
| Weekly research digest per category | research_digest_release |
merchant |
| Performance monitor | perf_spike, perf_dip, milestone_reached |
merchant |
| Review-pattern detector | review_theme_emerged |
merchant |
| Conversation curiosity-ask scheduler | curious_ask_due |
merchant |
| Recall scheduler (from merchant CRM) | recall_due |
customer |
| Lapse detector | customer_lapsed_soft, customer_lapsed_hard |
customer |
| Appointment reminder | appointment_tomorrow |
customer |
| Capacity optimizer | unplanned_slot_open (offered to likely-to-book lapsed customers) |
customer |
Adding a loop = define one new kind, implement the detector, add a composer prompt variant. No change to merchant/category/customer code.
Implementation shape
# agents/vera/engagement/contexts.py
@dataclass
class CategoryContext:
slug: str # "dentists"
offer_catalog: list[OfferTemplate]
voice: VoiceProfile
peer_stats: PeerStats
digest: list[DigestItem]
patient_content_library: list[ContentItem]
seasonal_beats: list[SeasonalBeat]
trend_signals: list[TrendSignal]
@dataclass
class MerchantContext:
merchant_id: str
identity: Identity
subscription: Subscription
performance: PerformanceSnapshot
offers: list[MerchantOffer]
conversation_history: ConversationHistory
customer_aggregate: CustomerAggregate
signals: list[DerivedSignal]
@dataclass
class TriggerContext:
id: str
scope: Literal["merchant", "customer"]
kind: str
source: Literal["external", "internal"]
payload: dict
urgency: int # 1-5
suppression_key: str
expires_at: datetime
@dataclass
class CustomerContext:
customer_id: str
merchant_id: str
identity: CustomerIdentity
relationship: Relationship
state: Literal["new", "active", "lapsed_soft", "lapsed_hard", "churned"]
preferences: Preferences
consent: Consent
# agents/vera/engagement/composer.py
class EngagementComposer:
def compose(self,
category: CategoryContext,
merchant: MerchantContext,
trigger: TriggerContext,
customer: CustomerContext | None = None) -> ComposedMessage:
"""Returns ComposedMessage(body, template_params, cta,
suppression_key, send_as)."""
Both engagement surfaces (merchant-facing, customer-on-behalf-of-merchant) call the same composer. The only thing that changes is whether customer is populated.
Phased rollout
Phase 1 β framework skeleton + dentistry vertical (β 2 weeks)
- Define the 4 dataclasses in
agents/vera/engagement/contexts.py. - Build the
CategoryContextfor dentistry β offer catalog, voice profile, peer stats, one weekly research digest pipeline, patient-content seed. - Build
MerchantContextloader from the existingmerchant_snapshot_datacollection. - Build the first
EngagementComposerwith a prompt that handles 2 trigger kinds (research_digest_releaseand one merchant-facing perf trigger). - Render (no send) the Dr. Meera research-digest message end-to-end from the 4 contexts. Inspect the output before any send happens.
Phase 2 β customer-on-behalf sends (β 2 weeks)
- Resolve the customer-data source-of-truth. This is the biggest unknown. Options: clinic SaaS integration (Practo, Dentcubate), merchant CSV upload, BOTOPS chat-derived patient list. Without this, customer engagement is theoretical.
- Define the consent model: customer opted in via merchant, not via Vera directly. Capture timestamp + scope.
- Stand up a send-as-merchant channel: WhatsApp Business API under the merchant's number, or Vera's shared number with attribution
"Dr. Meera's clinic via Vera". - Ship the first customer-facing trigger in production:
recall_due. Lowest abuse risk, highest merchant intent.
Phase 3 β multiply verticals (β 1 week per vertical)
- Replicate
CategoryContextfor 4-5 more verticals (salons, gyms, pharmacies, restaurants, car service). Mostly data filling, not code. - Add 3 more triggers per scope. Merchant:
perf_dip,milestone_reached,review_theme_emerged. Customer:customer_lapsed_soft,appointment_tomorrow,unplanned_slot_open.
Open questions
These need answers before Phase 2 can ship:
- Where does the merchant's customer list live? No clean answer yet. Most likely: per-merchant clinic software with no standard integration. May need a self-serve CSV upload or a per-vertical SaaS adapter.
- Consent architecture. Can Vera message a patient directly, or must every outbound require merchant approval before send? Recommendation: templated auto-sends with merchant override available, switching to fully-auto after the merchant has approved N consecutive sends.
- Attribution. Does the patient see "Dr. Meera's clinic" or "Vera on behalf of Dr. Meera's clinic"? Trust + legal implications either way. Probably category-dependent (regulated verticals need clearer attribution).
- Composer prompt versioning. Single point of failure. Versioned + A/B tested from day 1 β every send records the prompt version that produced it.
- Offer source-of-truth. Per the parallel discussion, the canonical merchant offer catalog likely lives outside vera-mcp (aryan
catalogoffer, merchant-portal-api, or magicpin_jobs output). MerchantContext needs to read from that source β pending identification. - Composer model choice. Azure OpenAI primary, Deepseek fallback (matching
template_generator._call_llm)? Or is there a case for a smaller faster model for high-volume per-customer sends?
Why this is worth building
- Engagement frequency goes from "few times a month" to "few times a week" β by adding curiosity-driven, knowledge-driven, and customer-driven loops on top of the existing functional ones.
- One framework, two products β the same composition engine drives Vera-to-merchant and merchant-to-customer messaging. Build once, ship twice.
- Vertical scaling is data work, not code work β adding a new category becomes "fill in a CategoryContext", not "write a new agent".
- Auditable + versioned β every message has explicit inputs and a versioned composer; we can replay, A/B test, and answer "why did Vera send this?" for any past send.
Appendix: relationship to existing code
agents/vera/followup/event_sources.pyandagents/vera/followup/event_extractor.py(branchfeature/vera-campaign-engagement) already produce externalTriggerContext-shaped objects for the news/weather scanner. They become the first concrete trigger source feeding the new composer.agents/vera/followup/template_registry.pywill continue to host the Kaleyra-approved template names (used for the first-touch send before the 24h session window opens). The composer fills the template parameters.agents/vera/followup/snapshot_data.pyalready provides most of theMerchantContextfields. Customer aggregate fields would be added as new sections onMerchantSnapshotData.services/vera-mcp/src/services/offer_suggester.pyis the leading candidate forCategoryContext.offer_catalog(pending the open offer source-of-truth question).