toga-ai 1.0.93 → 1.0.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/ai-bdr/INDEX.md +8 -0
- package/knowledge/2.0/apps/ai-bdr/architecture.md +197 -0
- package/knowledge/2.0/apps/ai-bdr/features/call-orchestration.md +250 -0
- package/knowledge/2.0/apps/ai-bdr/features/vapi-integration.md +171 -0
- package/knowledge/2.0/apps/ai-bdr/workflows/new-campaign-onboarding.md +148 -0
- package/knowledge/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/mits-po-to-so-item-linking.md +29 -1
- package/knowledge/registry.json +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ai-bdr (AI-BDR) — 2.0 knowledge
|
|
2
|
+
|
|
3
|
+
| Doc | Summary | Files |
|
|
4
|
+
|-----|---------|-------|
|
|
5
|
+
| [AI-BDR Architecture](architecture.md) | **AI-BDR** is TOGA's outbound AI sales-development representative. | ai-bdr/README.md, ai-bdr/requirements.txt, ai-bdr/.env.example, ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/vapi/templates/assistant.template.json, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py |
|
|
6
|
+
| [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
|
|
7
|
+
| [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
|
|
8
|
+
| [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: AI-BDR Architecture
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- ai-bdr/README.md
|
|
13
|
+
- ai-bdr/requirements.txt
|
|
14
|
+
- ai-bdr/.env.example
|
|
15
|
+
- ai-bdr/docs/client-onboarding-sop.md
|
|
16
|
+
- ai-bdr/docs/vapi-firstmessage-timing-fix.md
|
|
17
|
+
- ai-bdr/docs/vapi-inbound-callback-assistant-request.md
|
|
18
|
+
- ai-bdr/docs/vapi-voicemail-iphone-screening.md
|
|
19
|
+
- ai-bdr/vapi/templates/system-prompt.template.md
|
|
20
|
+
- ai-bdr/vapi/templates/assistant.template.json
|
|
21
|
+
- ai-bdr/prompts/archive/prompt-may-15.txt
|
|
22
|
+
- ai-bdr/prompts/campaigns/healthcare-2026-03-18.md
|
|
23
|
+
- ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md
|
|
24
|
+
- ai-bdr/scripts/update_assistant.py
|
|
25
|
+
- ai-bdr/scripts/update_system_prompt.py
|
|
26
|
+
- ai-bdr/scripts/update_structured_output.py
|
|
27
|
+
- ai-bdr/scripts/update_call_summary_context.py
|
|
28
|
+
- ai-bdr/scripts/fix_booking_guardrails.py
|
|
29
|
+
- ai-bdr/scripts/get_assistant.py
|
|
30
|
+
related:
|
|
31
|
+
- features/vapi-integration.md
|
|
32
|
+
- features/call-orchestration.md
|
|
33
|
+
- workflows/new-campaign-onboarding.md
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Summary
|
|
37
|
+
|
|
38
|
+
**AI-BDR** is TOGA's outbound AI sales-development representative. **Vapi**
|
|
39
|
+
runs the live phone conversation with the prospect; the **PHP worker**
|
|
40
|
+
schedules calls, receives the end-of-call webhook, and acts on the
|
|
41
|
+
structured output (book a Cal.com meeting, flag DNC, schedule a callback,
|
|
42
|
+
queue nurture email, mark wrong-person, etc.). Success = a booked
|
|
43
|
+
discovery call (or a qualified nurture / accurate compliance handling).
|
|
44
|
+
|
|
45
|
+
The repo at `talos-backend/AI-BDR/` is **the Vapi side only** — assistants,
|
|
46
|
+
prompts, structured-output schema, and Python automation scripts. The PHP
|
|
47
|
+
worker side (call initiation + webhook handlers + campaign cadence) lives
|
|
48
|
+
in a TOGA worker repo (`worker` for 1.0 / `worker2` for 2.0) and is **not
|
|
49
|
+
checked into this repo** — its contract is documented here.
|
|
50
|
+
|
|
51
|
+
## Top-level layout
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
AI-BDR/
|
|
55
|
+
├── README.md
|
|
56
|
+
├── requirements.txt # Python deps for the scripts
|
|
57
|
+
├── .env.example
|
|
58
|
+
├── docs/ # SOPs and integration specs
|
|
59
|
+
│ ├── client-onboarding-sop.md # 6-phase new-campaign runbook (canonical)
|
|
60
|
+
│ ├── vapi-firstmessage-timing-fix.md
|
|
61
|
+
│ ├── vapi-inbound-callback-assistant-request.md
|
|
62
|
+
│ └── vapi-voicemail-iphone-screening.md
|
|
63
|
+
├── prompts/
|
|
64
|
+
│ ├── archive/ # historical full prompts (prompt-may-15.txt etc.)
|
|
65
|
+
│ ├── campaigns/ # active per-campaign prompts (healthcare-*.md)
|
|
66
|
+
│ ├── templates/ # campaign-template.md (Liquid skeleton)
|
|
67
|
+
│ └── reference/ # vapi_docs_urls.txt, sales-demo-questions.txt
|
|
68
|
+
├── scripts/ # Python automation against Vapi REST API
|
|
69
|
+
│ ├── get_assistant.py
|
|
70
|
+
│ ├── update_assistant.py # core: firstMessage / prompt / structuredOutput
|
|
71
|
+
│ ├── update_system_prompt.py
|
|
72
|
+
│ ├── update_structured_output.py
|
|
73
|
+
│ ├── update_call_summary_context.py
|
|
74
|
+
│ └── fix_booking_guardrails.py
|
|
75
|
+
└── vapi/ # Vapi assistant + tool templates
|
|
76
|
+
└── templates/
|
|
77
|
+
├── assistant.template.json
|
|
78
|
+
└── system-prompt.template.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Four moving parts
|
|
82
|
+
|
|
83
|
+
1. **Vapi assistant(s)** — three active assistants on three numbers (IDs
|
|
84
|
+
from the SOP):
|
|
85
|
+
- **Primary BDR** — `+1 (216) 616-0067` — assistant id
|
|
86
|
+
`6663a81b-a173-4ffd-b297-1c17aa66dead`.
|
|
87
|
+
- **Healthcare campaign** — `+1 (331) 244-5958` — assistant id
|
|
88
|
+
`0c058f55-1610-413c-ae04-c71b30b33a43`.
|
|
89
|
+
- **Tech demo** — `+1 (331) 281-1679` — assistant id
|
|
90
|
+
`0505c46e-1150-4814-9a5c-60ad44287abf`.
|
|
91
|
+
- All share one **structured-output schema** id
|
|
92
|
+
`daaa4639-4143-4c3f-a71a-2a78ad3c163f`, three tools (`end_call_tool`,
|
|
93
|
+
`GetBookingSlots`, `kb_search`), GPT-4.1 @ temp 0.5, Elliot voice
|
|
94
|
+
@ 0.9 speed.
|
|
95
|
+
2. **PHP worker** (in `worker` / `worker2` — not in this repo) — schedules
|
|
96
|
+
outbound calls (`POST https://api.vapi.ai/call/phone`), implements the
|
|
97
|
+
three webhook endpoints below, runs the campaign cadence loop.
|
|
98
|
+
3. **Webhook surface** at `https://webhook.togahub.com/vapi`:
|
|
99
|
+
- `end-of-call-report` — after every call; carries the structured
|
|
100
|
+
output (`callOutcome`, `phpWorkerAction`, transcript, …).
|
|
101
|
+
- `assistant-request` — inbound callback; backend has 7.5 s to look up
|
|
102
|
+
the contact by phone and return the assistant config + variableValues
|
|
103
|
+
with `callType="callback"`.
|
|
104
|
+
- `status-update` — used for first-message timing (Option B).
|
|
105
|
+
4. **External integrations:**
|
|
106
|
+
- **Cal.com** — `GetBookingSlots` tool hits `/v2/slots` with the
|
|
107
|
+
campaign's event-type slug + team slug + caller-confirmed time range.
|
|
108
|
+
The backend creates the actual meeting on `BOOK_CALCOM_MEETING`.
|
|
109
|
+
- **Gemini 2.0 Flash KB** — `kb_search` tool grounds the assistant in
|
|
110
|
+
~12 TOGA service / compliance PDFs.
|
|
111
|
+
- **NetSuite** — backend creates a lead record on `BOOK_CALCOM_MEETING`
|
|
112
|
+
(sequence in `client-onboarding-sop.md §10.2`).
|
|
113
|
+
|
|
114
|
+
## End-to-end call flow (mental model)
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
PHP worker cadence cron picks next prospect
|
|
118
|
+
→ POST https://api.vapi.ai/call/phone
|
|
119
|
+
{assistantId, phoneNumberId,
|
|
120
|
+
customer.number,
|
|
121
|
+
assistantOverrides.variableValues: {
|
|
122
|
+
firstName, lastName, companyName, jobTitle,
|
|
123
|
+
phoneNumber, knownEmail, callType="immediate",
|
|
124
|
+
attemptNumber, previousCallSummary, …}}
|
|
125
|
+
→ Vapi dials the prospect
|
|
126
|
+
→ assistant runs the call (Liquid-templated system prompt branches on
|
|
127
|
+
attemptNumber + callType; collects email; uses GetBookingSlots and
|
|
128
|
+
kb_search; obeys two-turn closing protocol)
|
|
129
|
+
→ call ends
|
|
130
|
+
→ POST https://webhook.togahub.com/vapi (end-of-call-report)
|
|
131
|
+
{message.endedReason, message.transcript,
|
|
132
|
+
message.analysis.structuredData: {
|
|
133
|
+
callOutcome, phpWorkerAction, dncRequested,
|
|
134
|
+
qualificationScore, callSummary, contactEmail,
|
|
135
|
+
meetingTime, callbackTime, callType,
|
|
136
|
+
nextCallOpeningContext, …}}
|
|
137
|
+
→ PHP worker action handler dispatches on phpWorkerAction:
|
|
138
|
+
BOOK_CALCOM_MEETING → Cal.com POST + NetSuite lead
|
|
139
|
+
SCHEDULE_CALLBACK → update dtAgreedUponNextContact
|
|
140
|
+
FLAG_DNC_REMOVE_FROM_CAMPAIGNS → set isOkayToCall=0 + purge
|
|
141
|
+
SEND_NURTURE_EMAIL → queue email + topic
|
|
142
|
+
MARK_*_RETRY → cadence rules
|
|
143
|
+
MARK_NOT_INTERESTED / MARK_WRONG_PERSON / NO_ACTION
|
|
144
|
+
→ If callback agreed and prospect dials in later:
|
|
145
|
+
Vapi fires assistant-request (≤7.5 s SLA)
|
|
146
|
+
PHP worker looks up contact by phone, returns variableValues with
|
|
147
|
+
callType="callback" + previousCallSummary
|
|
148
|
+
→ same assistant resumes with prior-call context
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
See `features/call-orchestration.md` for the wire-level shape of the three
|
|
152
|
+
webhook payloads and the full `phpWorkerAction` enum.
|
|
153
|
+
|
|
154
|
+
## Why this repo is Python (scripts) + JSON (templates) + Markdown (prompts)
|
|
155
|
+
|
|
156
|
+
All conversation logic lives in **Vapi configuration**, not in TOGA code.
|
|
157
|
+
The Python scripts only update that configuration via the Vapi REST API.
|
|
158
|
+
Markdown prompts are the source of truth for assistant behavior — they are
|
|
159
|
+
authored here, then pushed to Vapi by `scripts/update_system_prompt.py`.
|
|
160
|
+
|
|
161
|
+
## Storage
|
|
162
|
+
|
|
163
|
+
- **No primary DB in this repo.** Prospect / campaign state lives in the
|
|
164
|
+
TOGA database the PHP worker reads (tables include `contacts`,
|
|
165
|
+
`campaigns`, and implied `call_logs` per the SOP; exact schema lives in
|
|
166
|
+
the worker repo).
|
|
167
|
+
- **Vapi platform** stores assistant configs, call recordings, transcripts.
|
|
168
|
+
- **Cal.com** stores meeting bookings.
|
|
169
|
+
- **NetSuite** stores leads from booked meetings.
|
|
170
|
+
|
|
171
|
+
## Observability
|
|
172
|
+
|
|
173
|
+
- **Vapi dashboard** — call logs, recordings, transcripts.
|
|
174
|
+
- **CloudWatch / TOGa Hub backend logs** for the webhook handlers.
|
|
175
|
+
- **No Langfuse / OTEL** — there is no TOGA-side LLM call to trace; the
|
|
176
|
+
LLM call happens inside Vapi.
|
|
177
|
+
|
|
178
|
+
## Key decisions
|
|
179
|
+
|
|
180
|
+
- **Single shared structured-output schema** across all assistants — keeps
|
|
181
|
+
the PHP webhook handler simple (one branch per `phpWorkerAction`,
|
|
182
|
+
campaign-agnostic).
|
|
183
|
+
- **PHP is the orchestrator, Vapi is the actor.** Cadence, retry rules,
|
|
184
|
+
DNC compliance, NetSuite/Cal.com integration all live in PHP — Vapi
|
|
185
|
+
never makes business decisions, only conversation moves.
|
|
186
|
+
- **Liquid-templated system prompts** keyed on `attemptNumber` and
|
|
187
|
+
`callType` — one prompt covers cold / warm-inbound / callback / followup
|
|
188
|
+
without forking assistants.
|
|
189
|
+
- **Two-turn closing protocol** (in the end-call tool description) —
|
|
190
|
+
prevents the assistant from hanging up before the prospect's last word
|
|
191
|
+
has been acknowledged.
|
|
192
|
+
- **DNC is non-negotiable.** `dncRequested` is evaluated independently of
|
|
193
|
+
`phpWorkerAction`; if true, the worker MUST run `FLAG_DNC` regardless of
|
|
194
|
+
the suggested action.
|
|
195
|
+
|
|
196
|
+
## Change history
|
|
197
|
+
- 2026-06-16 — Initial architecture doc for AI-BDR. (akhokhani)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Call Orchestration — PHP Worker ↔ Vapi (the integration seam)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- ai-bdr/docs/client-onboarding-sop.md
|
|
13
|
+
- ai-bdr/docs/vapi-firstmessage-timing-fix.md
|
|
14
|
+
- ai-bdr/docs/vapi-inbound-callback-assistant-request.md
|
|
15
|
+
- ai-bdr/docs/vapi-voicemail-iphone-screening.md
|
|
16
|
+
- ai-bdr/scripts/update_structured_output.py
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- vapi-integration.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
The **PHP worker** is the orchestrator; **Vapi** is the actor. This doc
|
|
25
|
+
defines the exact wire shape of the three integration points so the PHP
|
|
26
|
+
worker handler in `worker` / `worker2` (separate repo — see **Where the
|
|
27
|
+
PHP code lives**) and the Vapi assistants stay in contract.
|
|
28
|
+
|
|
29
|
+
Three integration points:
|
|
30
|
+
|
|
31
|
+
1. **Outbound — `POST https://api.vapi.ai/call/phone`** — worker initiates
|
|
32
|
+
a call with all per-prospect variables.
|
|
33
|
+
2. **Outbound webhook — `POST https://webhook.togahub.com/vapi`
|
|
34
|
+
(`end-of-call-report`)** — every call returns its structured analysis;
|
|
35
|
+
worker dispatches on `phpWorkerAction`.
|
|
36
|
+
3. **Inbound webhook — `POST https://webhook.togahub.com/vapi`
|
|
37
|
+
(`assistant-request`)** — when a prospect dials back, Vapi asks the
|
|
38
|
+
worker which assistant + variableValues to use (7.5 s SLA).
|
|
39
|
+
|
|
40
|
+
## 1) Outbound call initiation
|
|
41
|
+
|
|
42
|
+
```http
|
|
43
|
+
POST https://api.vapi.ai/call/phone
|
|
44
|
+
Authorization: Bearer ${VAPI_API_KEY}
|
|
45
|
+
Content-Type: application/json
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
"assistantId": "6663a81b-a173-4ffd-b297-1c17aa66dead",
|
|
49
|
+
"phoneNumberId": "<vapi-pn-id>",
|
|
50
|
+
"customer": { "number": "+15551234567" },
|
|
51
|
+
"assistantOverrides": {
|
|
52
|
+
"variableValues": {
|
|
53
|
+
"firstName": "Jane",
|
|
54
|
+
"lastName": "Doe",
|
|
55
|
+
"phoneNumber": "+15551234567",
|
|
56
|
+
"knownEmail": "jane@example.com",
|
|
57
|
+
"companyName": "Example Corp",
|
|
58
|
+
"jobTitle": "Director of IT",
|
|
59
|
+
"industry": "Healthcare",
|
|
60
|
+
"campaignSlug": "healthcare-2026-03",
|
|
61
|
+
"offer": "AI Readiness Discovery Call",
|
|
62
|
+
"callType": "immediate",
|
|
63
|
+
"attemptNumber": 1,
|
|
64
|
+
"previousCallSummary": "",
|
|
65
|
+
"nextCallOpeningContext": ""
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `callType` ∈ {`immediate`, `scheduled`, `callback`, `unknown`}.
|
|
72
|
+
- `immediate` — cold outbound (default for the cadence cron).
|
|
73
|
+
- `scheduled` — warm inbound from the website's "schedule a call" flow;
|
|
74
|
+
the prompt uses time-of-day context.
|
|
75
|
+
- `callback` — prospect dialed back after voicemail; **only set by the
|
|
76
|
+
`assistant-request` handler, never the outbound caller.**
|
|
77
|
+
- `attemptNumber` — 1, 2, 3, … The prompt branches at 1 vs 2–3 vs final.
|
|
78
|
+
- `previousCallSummary` — the `callSummary` field returned on the previous
|
|
79
|
+
attempt's `end-of-call-report`; backend stitches across attempts.
|
|
80
|
+
- `nextCallOpeningContext` — one-sentence opener hint produced by the
|
|
81
|
+
prior call's structured output; pass it through so the next opener
|
|
82
|
+
sounds warm, not robotic.
|
|
83
|
+
|
|
84
|
+
Response (excerpt):
|
|
85
|
+
```json
|
|
86
|
+
{ "id": "<call-id>", "status": "queued", "assistantId": "...", ... }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Persist `id` against the call attempt — it's the join key for the webhook.
|
|
90
|
+
|
|
91
|
+
## 2) End-of-call webhook
|
|
92
|
+
|
|
93
|
+
`POST https://webhook.togahub.com/vapi` with `message.type =
|
|
94
|
+
"end-of-call-report"`. **Verify the Vapi signature with `VAPI_SERVER_SECRET`
|
|
95
|
+
before processing — unsigned requests must 401.**
|
|
96
|
+
|
|
97
|
+
Payload (excerpt):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"message": {
|
|
102
|
+
"type": "end-of-call-report",
|
|
103
|
+
"endedReason": "customer-ended-call",
|
|
104
|
+
"call": { "id": "...", "assistantId": "...", "customer": {...} },
|
|
105
|
+
"transcript": "Hi, this is Alex from TOGA... [full string]",
|
|
106
|
+
"messages": [ {"role": "...", "message": "..."}, ... ],
|
|
107
|
+
"analysis": {
|
|
108
|
+
"summary": "Prospect interested; meeting booked for...",
|
|
109
|
+
"structuredData": {
|
|
110
|
+
"callOutcome": "meeting_booked",
|
|
111
|
+
"phpWorkerAction": "BOOK_CALCOM_MEETING",
|
|
112
|
+
"dncRequested": false,
|
|
113
|
+
"prospectSentiment": "positive",
|
|
114
|
+
"qualificationScore": 4,
|
|
115
|
+
"callSummary": "...",
|
|
116
|
+
"contactEmail": "jane@example.com",
|
|
117
|
+
"emailMatchedKnown": true,
|
|
118
|
+
"meetingTime": "2026-06-18T15:00:00-05:00",
|
|
119
|
+
"callbackTime": "",
|
|
120
|
+
"callbackContactPreference": "",
|
|
121
|
+
"callType": "immediate",
|
|
122
|
+
"nextCallOpeningContext": "...",
|
|
123
|
+
"nurtureFollowUpTimeframe": "",
|
|
124
|
+
"nurtureTopic": "",
|
|
125
|
+
"referralName": "",
|
|
126
|
+
"referralTitle": "",
|
|
127
|
+
"dncReason": ""
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `phpWorkerAction` dispatch table
|
|
135
|
+
|
|
136
|
+
| Action | What the worker does |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `BOOK_CALCOM_MEETING` | `POST /v2/bookings` to Cal.com with `meetingTime`; create NetSuite lead; mark contact `meetingBooked`; stop cadence. |
|
|
139
|
+
| `SCHEDULE_CALLBACK` | Update `contacts.dtAgreedUponNextContact = callbackTime`; reschedule via cadence cron; record `callbackContactPreference`. |
|
|
140
|
+
| `SEND_NURTURE_EMAIL` | Queue email with topic = `nurtureTopic`; schedule sentinel for `nurtureFollowUpTimeframe`. |
|
|
141
|
+
| `FLAG_DNC_REMOVE_FROM_CAMPAIGNS` | **NON-NEGOTIABLE.** Set `isOkayToCall=0`; remove contact from every active campaign; log `dncReason`. |
|
|
142
|
+
| `MARK_NOT_INTERESTED` | Set `lastCallOutcome`; apply campaign-level not-interested cadence (typically: no retry for 90 days). |
|
|
143
|
+
| `MARK_WRONG_PERSON` | If `referralName` populated → create new contact + link to original; close the original attempt. |
|
|
144
|
+
| `MARK_VOICEMAIL_RETRY` | Apply voicemail-retry cadence (e.g. retry in 24h, max 3 attempts). |
|
|
145
|
+
| `MARK_NO_ANSWER_RETRY` | Apply no-answer retry cadence (typically tighter than voicemail). |
|
|
146
|
+
| `MARK_GATEKEEPER_BLOCKED_RETRY` | Try a different day-of-week / time-of-day next attempt. |
|
|
147
|
+
| `NO_ACTION` | Log only; cadence cron decides what to do (often used when the call ended ambiguously). |
|
|
148
|
+
|
|
149
|
+
### DNC short-circuit
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
if structuredData.dncRequested == true:
|
|
153
|
+
run FLAG_DNC_REMOVE_FROM_CAMPAIGNS, regardless of phpWorkerAction
|
|
154
|
+
log dncReason
|
|
155
|
+
do NOT proceed to any other action
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This is the only rule that overrides `phpWorkerAction`. Keep it.
|
|
159
|
+
|
|
160
|
+
## 3) Inbound `assistant-request` webhook (callbacks)
|
|
161
|
+
|
|
162
|
+
When a prospect dials the BDR number back (after a voicemail or because
|
|
163
|
+
they were given the number), Vapi posts:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
POST https://webhook.togahub.com/vapi
|
|
167
|
+
{
|
|
168
|
+
"message": {
|
|
169
|
+
"type": "assistant-request",
|
|
170
|
+
"call": { "id": "...", "customer": { "number": "+15551234567" } }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**SLA: respond within 7.5 seconds** — Vapi falls back to a default
|
|
176
|
+
assistant otherwise.
|
|
177
|
+
|
|
178
|
+
Worker must:
|
|
179
|
+
|
|
180
|
+
1. Look up `contacts` by phone (E.164 — store both `+1XXXXXXXXXX` and
|
|
181
|
+
10-digit forms in the index so lookup is one query).
|
|
182
|
+
2. Read the most recent call attempt's `callSummary` and store-side
|
|
183
|
+
campaign config.
|
|
184
|
+
3. Respond:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"assistantId": "6663a81b-a173-4ffd-b297-1c17aa66dead",
|
|
189
|
+
"assistantOverrides": {
|
|
190
|
+
"variableValues": {
|
|
191
|
+
"firstName": "Jane",
|
|
192
|
+
"...": "...",
|
|
193
|
+
"callType": "callback",
|
|
194
|
+
"attemptNumber": 4,
|
|
195
|
+
"previousCallSummary": "<prior call's callSummary>"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If no contact match: still respond (primary BDR assistant id + empty
|
|
202
|
+
variableValues; `attemptNumber=1`, `callType="unknown"`). The prompt
|
|
203
|
+
gracefully handles `"Information not available"`.
|
|
204
|
+
|
|
205
|
+
## Where the PHP code lives
|
|
206
|
+
|
|
207
|
+
**Not in this repo.** The handler lives in the TOGA worker repo (`worker`
|
|
208
|
+
for 1.0 or `worker2` for 2.0). Likely class shape:
|
|
209
|
+
|
|
210
|
+
- 1.0: an `App_Controller_*` for the webhook (under the framework's HTTP
|
|
211
|
+
surface), with an `App_Worker_*` or scheduled cron for cadence.
|
|
212
|
+
- 2.0: a `_Controller_*` for the webhook + `_Worker_*` invoked from the
|
|
213
|
+
`_Queue` dispatcher for cadence.
|
|
214
|
+
|
|
215
|
+
When the actual PHP code is committed to `worker` / `worker2`, add a
|
|
216
|
+
feature doc in that repo's `features/` and link it from this file's
|
|
217
|
+
`related:`.
|
|
218
|
+
|
|
219
|
+
## Security
|
|
220
|
+
|
|
221
|
+
- **Verify `Vapi-Signature` on every webhook** using
|
|
222
|
+
`VAPI_SERVER_SECRET`. Reject unsigned requests with 401.
|
|
223
|
+
- **Idempotency:** dedupe webhooks by `message.call.id` — Vapi may retry
|
|
224
|
+
on transient failures.
|
|
225
|
+
- **PII in the transcript** — treat at rest as PII (names, phone numbers,
|
|
226
|
+
emails, sometimes payment hints). Don't log full transcripts to
|
|
227
|
+
unredacted backends.
|
|
228
|
+
- **DNC is auditable.** Persist `dncReason` + raw structured-output JSON
|
|
229
|
+
alongside the action so a future compliance review can reconstruct
|
|
230
|
+
intent.
|
|
231
|
+
|
|
232
|
+
## Gotchas / known issues
|
|
233
|
+
|
|
234
|
+
- **`assistant-request` SLA is 7.5 s.** Anything synchronous in the path
|
|
235
|
+
(NetSuite, slow joins) will trip Vapi's fallback. Cache contact lookups
|
|
236
|
+
by phone in Redis with a 60 s TTL if the DB hop is slow.
|
|
237
|
+
- **`callType="callback"` must include `previousCallSummary`** — leaving
|
|
238
|
+
it empty makes the assistant open like a cold call. Empty is acceptable
|
|
239
|
+
only when the contact is genuinely unknown.
|
|
240
|
+
- **Cal.com booking happens server-side**, not by the assistant. The
|
|
241
|
+
`GetBookingSlots` tool only *suggests* a slot to the prospect; the worker
|
|
242
|
+
is responsible for actually reserving it on `BOOK_CALCOM_MEETING`.
|
|
243
|
+
- **Schema field additions need both sides.** Adding a field to the shared
|
|
244
|
+
structured-output schema without a corresponding handler branch in the
|
|
245
|
+
worker silently drops data. Coordinate via `update_structured_output.py`
|
|
246
|
+
dry-run + worker PR.
|
|
247
|
+
- **`attemptNumber` is 1-indexed**, not 0-indexed. First call = 1.
|
|
248
|
+
|
|
249
|
+
## Change history
|
|
250
|
+
- 2026-06-16 — Initial call-orchestration feature doc (PHP ↔ Vapi seam). (akhokhani)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vapi Integration — Assistants, Tools, Structured Output
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- ai-bdr/vapi/templates/assistant.template.json
|
|
13
|
+
- ai-bdr/vapi/templates/system-prompt.template.md
|
|
14
|
+
- ai-bdr/prompts/archive/prompt-may-15.txt
|
|
15
|
+
- ai-bdr/prompts/campaigns/healthcare-2026-03-18.md
|
|
16
|
+
- ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md
|
|
17
|
+
- ai-bdr/prompts/templates/campaign-template.md
|
|
18
|
+
- ai-bdr/scripts/update_assistant.py
|
|
19
|
+
- ai-bdr/scripts/update_system_prompt.py
|
|
20
|
+
- ai-bdr/scripts/update_structured_output.py
|
|
21
|
+
- ai-bdr/scripts/update_call_summary_context.py
|
|
22
|
+
- ai-bdr/scripts/fix_booking_guardrails.py
|
|
23
|
+
- ai-bdr/scripts/get_assistant.py
|
|
24
|
+
- ai-bdr/docs/vapi-firstmessage-timing-fix.md
|
|
25
|
+
- ai-bdr/docs/vapi-voicemail-iphone-screening.md
|
|
26
|
+
related:
|
|
27
|
+
- ../architecture.md
|
|
28
|
+
- call-orchestration.md
|
|
29
|
+
- ../workflows/new-campaign-onboarding.md
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Summary
|
|
33
|
+
|
|
34
|
+
Everything inside Vapi: the three assistants, the three shared tools, the
|
|
35
|
+
shared structured-output schema, the Liquid-templated system prompt, and
|
|
36
|
+
the Python scripts that author them. PHP never touches Vapi config — all
|
|
37
|
+
changes go through the scripts in this repo.
|
|
38
|
+
|
|
39
|
+
## Key files / entry points
|
|
40
|
+
|
|
41
|
+
| File | Purpose |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `vapi/templates/assistant.template.json` | Reference shape of a Vapi assistant (model, voice, transcriber, tools, server-message subscriptions) |
|
|
44
|
+
| `vapi/templates/system-prompt.template.md` | Liquid skeleton for the system prompt |
|
|
45
|
+
| `prompts/archive/prompt-may-15.txt` | Canonical reference prompt — Phases 1–6, persistence rules, email verification, objection handling, DNC compliance |
|
|
46
|
+
| `prompts/campaigns/healthcare-*.md` | Active per-campaign prompts |
|
|
47
|
+
| `scripts/get_assistant.py` | Fetch a live assistant JSON (read-only) |
|
|
48
|
+
| `scripts/update_assistant.py` | **Core** — patch firstMessage / system prompt / structured-output fields. Dry-run by default; `--apply` writes |
|
|
49
|
+
| `scripts/update_system_prompt.py` | Push a prompt file to a live assistant |
|
|
50
|
+
| `scripts/update_structured_output.py` | Modify the shared schema (add a field, change an enum) |
|
|
51
|
+
| `scripts/update_call_summary_context.py` | Inject `previousCallSummary` into the prompt for follow-up attempts |
|
|
52
|
+
| `scripts/fix_booking_guardrails.py` | Guardrails for `GetBookingSlots` (e.g. don't span Friday→Monday) |
|
|
53
|
+
|
|
54
|
+
## Assistants
|
|
55
|
+
|
|
56
|
+
All three assistants share the same skeleton; they differ only in
|
|
57
|
+
firstMessage, system prompt, and the phone number wired to them.
|
|
58
|
+
|
|
59
|
+
| Field | Value |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Model | OpenAI `gpt-4.1`, `temperature=0.5`, `maxTokens=250` |
|
|
62
|
+
| Voice | Vapi "Elliot" @ speed 0.9; fallback "Neil" |
|
|
63
|
+
| Transcriber | Deepgram Nova-3 (confidence threshold 0.4) |
|
|
64
|
+
| VAD / endpointing | Vapi defaults; voicemail detection 6 retries, 5 s initial + 5 s frequency backoff |
|
|
65
|
+
| Server messages | `end-of-call-report` (all); `status-update` (when first-message-timing live); `assistant-request` (on inbound callbacks) |
|
|
66
|
+
| Server URL | `https://webhook.togahub.com/vapi` |
|
|
67
|
+
|
|
68
|
+
### Phone numbers + assistant IDs
|
|
69
|
+
|
|
70
|
+
- Primary BDR — `+1 (216) 616-0067` — `6663a81b-a173-4ffd-b297-1c17aa66dead`
|
|
71
|
+
- Healthcare — `+1 (331) 244-5958` — `0c058f55-1610-413c-ae04-c71b30b33a43`
|
|
72
|
+
- Tech demo — `+1 (331) 281-1679` — `0505c46e-1150-4814-9a5c-60ad44287abf`
|
|
73
|
+
|
|
74
|
+
## Tools (3, shared across assistants)
|
|
75
|
+
|
|
76
|
+
| Tool | Id | Purpose |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `end_call_tool` | `2c7e5ce6-d3ef-4239-85e1-e8da489a9229` | Closes the call with a two-turn closing protocol; never invoked before the prospect's last word is acknowledged |
|
|
79
|
+
| `GetBookingSlots` (api_request_tool) | `a51209dc-bd08-441c-ba20-dc87b0fc163b` | `GET https://cal.com/api/v2/slots?eventTypeSlug=<campaign>&teamSlug=toga-technology&start={{start}}&end={{end}}&timeZone=America/Chicago` |
|
|
80
|
+
| `kb_search` | `0b2b7a85-6111-4fea-86cd-3b1696ec5ae8` | Gemini 2.0 Flash retrieval over ~12 TOGA service / compliance PDFs |
|
|
81
|
+
|
|
82
|
+
The assistant is **not** allowed to invent meeting times — it must call
|
|
83
|
+
`GetBookingSlots`, name the slot to the prospect, and let the prospect
|
|
84
|
+
confirm. The actual booking happens in the PHP worker after the call.
|
|
85
|
+
|
|
86
|
+
## Liquid-templated system prompt
|
|
87
|
+
|
|
88
|
+
The prompt branches on:
|
|
89
|
+
|
|
90
|
+
- `{{attemptNumber}}` — `1` (cold opener) vs `2-3` (follow-up with prior
|
|
91
|
+
context) vs `final` (last-attempt heuristics).
|
|
92
|
+
- `{{callType}}` — `immediate` (cold outbound) / `scheduled` (warm
|
|
93
|
+
inbound, time-of-day matters) / `callback` (prospect dialed back; use
|
|
94
|
+
`previousCallSummary` to resume) / `unknown` (fall-back path).
|
|
95
|
+
- Missing fields render as `"Information not available"` — never as an
|
|
96
|
+
empty string (which would silently break openers).
|
|
97
|
+
|
|
98
|
+
`scripts/update_assistant.py` reads the prompt markdown, fills the Liquid
|
|
99
|
+
template, and PATCHes the live assistant. `--apply` is required to write —
|
|
100
|
+
default is dry-run with a diff.
|
|
101
|
+
|
|
102
|
+
## Structured output schema (shared, id `daaa4639-4143-4c3f-a71a-2a78ad3c163f`)
|
|
103
|
+
|
|
104
|
+
Returned in `message.analysis.structuredData` on every `end-of-call-report`:
|
|
105
|
+
|
|
106
|
+
| Field | Type | Notes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `callOutcome` | string enum | `meeting_booked`, `callback_scheduled`, `nurture_requested`, `not_interested`, `dnc_requested`, `voicemail`, `wrong_person`, `gatekeeper_blocked`, `no_answer` |
|
|
109
|
+
| `phpWorkerAction` | string enum | The action the PHP worker should perform — see `call-orchestration.md` for the full enum |
|
|
110
|
+
| `dncRequested` | boolean | Independent of `phpWorkerAction`. **If true, the worker MUST run `FLAG_DNC` regardless of the suggested action.** |
|
|
111
|
+
| `prospectSentiment` | string enum | `positive` / `neutral` / `negative` / `hostile` |
|
|
112
|
+
| `qualificationScore` | int | 1 (cold) – 5 (hot); cold outreach mostly lands at 2–3 |
|
|
113
|
+
| `callSummary` | string | One paragraph; reused as `previousCallSummary` on next attempt |
|
|
114
|
+
| `contactEmail` | string | Email the prospect confirmed; empty if not collected |
|
|
115
|
+
| `emailMatchedKnown` | boolean | True if confirmed email matches `knownEmail` from variableValues |
|
|
116
|
+
| `meetingTime` | ISO 8601 | Present if `callOutcome=meeting_booked` |
|
|
117
|
+
| `callbackTime` | ISO 8601 | Present if `callOutcome=callback_scheduled`; backend converts a spoken time to ISO |
|
|
118
|
+
| `callbackContactPreference` | string | `phone` / `email` / `sms` |
|
|
119
|
+
| `callType` | string | Mirrors the `callType` variable injected on this call |
|
|
120
|
+
| `nextCallOpeningContext` | string | One-sentence coaching note for the next attempt's opener |
|
|
121
|
+
| `nurtureFollowUpTimeframe` | string | e.g. `"2 days"` |
|
|
122
|
+
| `nurtureTopic` | string | e.g. `"AI Readiness Whitepaper"` |
|
|
123
|
+
| `referralName`, `referralTitle` | strings | If `callOutcome=wrong_person`, the contact the prospect referred you to |
|
|
124
|
+
| `dncReason` | string | e.g. `"prospect_request"` / `"regulatory"` |
|
|
125
|
+
|
|
126
|
+
To change the schema: `python scripts/update_structured_output.py` (dry
|
|
127
|
+
run by default; pass `--apply` to write). Adding a field needs a paired
|
|
128
|
+
PHP worker handler change — coordinate before deploying.
|
|
129
|
+
|
|
130
|
+
## Environment variables (`.env.example`)
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
VAPI_API_KEY= # Vapi private API key
|
|
134
|
+
VAPI_SERVER_SECRET= # Vapi webhook secret (verify signature)
|
|
135
|
+
VAPI_ASSISTANT_ID=6663a81b-... # default target for scripts
|
|
136
|
+
VAPI_STRUCTURED_OUTPUT_ID=daaa... # shared schema id
|
|
137
|
+
VAPI_END_CALL_TOOL_ID=2c7e5ce6-...
|
|
138
|
+
VAPI_BOOKING_TOOL_ID=a51209dc-...
|
|
139
|
+
VAPI_BASE_URL=https://api.vapi.ai # optional override
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Secret values live only in `.env` (gitignored) and in the deployment
|
|
143
|
+
runtime — never in the repo.
|
|
144
|
+
|
|
145
|
+
## Why scripts use raw HTTP (`requests` + curl-style headers) instead of the Vapi SDK
|
|
146
|
+
|
|
147
|
+
Cloudflare in front of `api.vapi.ai` blocks default `python-requests`
|
|
148
|
+
user-agents. The scripts spoof a curl-like UA. If you swap to an SDK,
|
|
149
|
+
ensure UA spoofing is preserved or you'll see 403s with no body.
|
|
150
|
+
|
|
151
|
+
## Gotchas / known issues
|
|
152
|
+
|
|
153
|
+
- **Voicemail detection isn't perfect on iPhone "Silence Unknown Callers".**
|
|
154
|
+
See `docs/vapi-voicemail-iphone-screening.md` for the screening
|
|
155
|
+
workaround.
|
|
156
|
+
- **First-message timing (Option B)** — the assistant waits for the human's
|
|
157
|
+
first phoneme before speaking, via the `status-update` server message.
|
|
158
|
+
Don't unsubscribe from `status-update` without re-checking the prompt's
|
|
159
|
+
opener logic.
|
|
160
|
+
- **`GetBookingSlots` can span a weekend.** `fix_booking_guardrails.py`
|
|
161
|
+
enforces a Mon–Fri 9–5 default; relax it cautiously.
|
|
162
|
+
- **`previousCallSummary` is regenerated per call** — don't try to persist
|
|
163
|
+
one paragraph as the long-running CRM note; that's `callSummary` plus
|
|
164
|
+
PHP-side aggregation.
|
|
165
|
+
- **One structured-output schema, all assistants.** Adding a campaign-
|
|
166
|
+
specific field forces every other assistant to declare it (Vapi resolves
|
|
167
|
+
by id). Prefer a generic field with campaign-level meaning encoded in
|
|
168
|
+
`callSummary` over a schema fork.
|
|
169
|
+
|
|
170
|
+
## Change history
|
|
171
|
+
- 2026-06-16 — Initial Vapi integration feature doc. (akhokhani)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: New-Campaign Onboarding (6-phase runbook)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- ai-bdr/docs/client-onboarding-sop.md
|
|
13
|
+
- ai-bdr/docs/calcom-new-campaign-event-guide.md
|
|
14
|
+
- ai-bdr/prompts/templates/campaign-template.md
|
|
15
|
+
- ai-bdr/scripts/update_assistant.py
|
|
16
|
+
- ai-bdr/scripts/update_system_prompt.py
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../features/vapi-integration.md
|
|
20
|
+
- ../features/call-orchestration.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
The end-to-end procedure for launching a new outbound BDR campaign
|
|
26
|
+
(industry, offer, target persona, geography). Six phases, spans marketing
|
|
27
|
+
+ backend + Vapi config + Cal.com + QA + go-live. Source of truth is
|
|
28
|
+
`docs/client-onboarding-sop.md`; this doc captures the operational shape
|
|
29
|
+
so a future Claude session can drive it.
|
|
30
|
+
|
|
31
|
+
## Phase 1 — Marketing brief
|
|
32
|
+
|
|
33
|
+
Inputs needed before any code or config changes:
|
|
34
|
+
|
|
35
|
+
- Campaign slug — kebab-case, used everywhere (`campaignSlug` variable,
|
|
36
|
+
Cal.com event-type slug, prompt filename).
|
|
37
|
+
- Industry + persona — target title/seniority.
|
|
38
|
+
- Offer — the one-line value prop the assistant pitches.
|
|
39
|
+
- Geography + time zone for cadence.
|
|
40
|
+
- Meeting contact info — who the booked meeting is *with* (name, email,
|
|
41
|
+
Cal.com handle).
|
|
42
|
+
- Compliance posture — DNC handling defaults are non-negotiable; capture
|
|
43
|
+
any campaign-specific addenda (e.g. healthcare HIPAA mentions).
|
|
44
|
+
|
|
45
|
+
Owner: marketing / sales. Hand off written + signed.
|
|
46
|
+
|
|
47
|
+
## Phase 2 — Backend setup (PHP worker side)
|
|
48
|
+
|
|
49
|
+
In `worker` / `worker2`:
|
|
50
|
+
|
|
51
|
+
1. Insert a `campaigns` row with the slug, offer, persona, cadence rules,
|
|
52
|
+
and the meeting contact's Cal.com handle.
|
|
53
|
+
2. Add a contact import job for the prospect list (CSV → `contacts`),
|
|
54
|
+
tagged with the campaign slug.
|
|
55
|
+
3. Confirm the cadence cron picks up the new campaign (typical interval:
|
|
56
|
+
every N minutes scan for `dtAgreedUponNextContact <= now AND
|
|
57
|
+
isOkayToCall=1`).
|
|
58
|
+
|
|
59
|
+
## Phase 3 — Vapi config
|
|
60
|
+
|
|
61
|
+
Two paths:
|
|
62
|
+
|
|
63
|
+
**A. Reuse the primary BDR assistant** (preferred for early campaigns):
|
|
64
|
+
|
|
65
|
+
1. Copy `prompts/templates/campaign-template.md` → `prompts/campaigns/<slug>.md`.
|
|
66
|
+
2. Fill the Liquid placeholders (offer, persona, KB topics).
|
|
67
|
+
3. `python scripts/update_system_prompt.py --assistant-id <primary> --prompt prompts/campaigns/<slug>.md`
|
|
68
|
+
— dry-run first, review diff, then `--apply`.
|
|
69
|
+
4. The shared structured-output schema needs no change unless the
|
|
70
|
+
campaign requires a new field (very rare — see `vapi-integration.md`
|
|
71
|
+
§gotchas).
|
|
72
|
+
|
|
73
|
+
**B. Provision a new dedicated assistant** (only when the campaign needs a
|
|
74
|
+
different voice / phone number / model):
|
|
75
|
+
|
|
76
|
+
1. Provision a Vapi phone number; capture `phoneNumberId`.
|
|
77
|
+
2. Create a new assistant from `vapi/templates/assistant.template.json`
|
|
78
|
+
with the new firstMessage + the per-campaign prompt.
|
|
79
|
+
3. Wire it to the **same** structured-output schema id and the same three
|
|
80
|
+
tool ids — never fork the schema.
|
|
81
|
+
4. Add the assistant id + phone number id to the `campaigns` row in PHP.
|
|
82
|
+
|
|
83
|
+
## Phase 4 — Cal.com event
|
|
84
|
+
|
|
85
|
+
Per `docs/calcom-new-campaign-event-guide.md` (≈5 minutes):
|
|
86
|
+
|
|
87
|
+
1. Create an event type with slug = campaign slug, team =
|
|
88
|
+
`toga-technology`, default duration 30 min, time zone matching the
|
|
89
|
+
campaign target.
|
|
90
|
+
2. Set the meeting contact as the host.
|
|
91
|
+
3. Verify the slug returns slots at
|
|
92
|
+
`https://cal.com/api/v2/slots?eventTypeSlug=<slug>&teamSlug=toga-technology&start=YYYY-MM-DD&end=YYYY-MM-DD&timeZone=America/Chicago`.
|
|
93
|
+
|
|
94
|
+
## Phase 5 — Integration testing
|
|
95
|
+
|
|
96
|
+
Drive end-to-end through a personal phone:
|
|
97
|
+
|
|
98
|
+
1. From PHP, `POST /call/phone` to your own number with full
|
|
99
|
+
variableValues + `attemptNumber=1`, `callType=immediate`.
|
|
100
|
+
2. Verify the assistant answers, runs the flow, books a real Cal.com slot
|
|
101
|
+
via `GetBookingSlots`, ends gracefully (two-turn closing protocol).
|
|
102
|
+
3. Verify the `end-of-call-report` webhook arrives at
|
|
103
|
+
`webhook.togahub.com/vapi` with the expected `phpWorkerAction`.
|
|
104
|
+
4. Verify the worker handler dispatches correctly (DNC, booking, callback,
|
|
105
|
+
nurture, retry).
|
|
106
|
+
5. Verify the `assistant-request` flow by calling the BDR number back from
|
|
107
|
+
your test phone within the same hour — confirm `callType=callback` and
|
|
108
|
+
`previousCallSummary` populates the next call.
|
|
109
|
+
|
|
110
|
+
## Phase 6 — Go-live
|
|
111
|
+
|
|
112
|
+
- Move campaign status to `active` in `campaigns`.
|
|
113
|
+
- Confirm cadence cron is consuming.
|
|
114
|
+
- Spot-check the first 10 call recordings in the Vapi dashboard.
|
|
115
|
+
- Set up CloudWatch / TOGa Hub log alerts on:
|
|
116
|
+
- `phpWorkerAction` distribution shift (sudden DNC spike, sudden
|
|
117
|
+
NO_ACTION surge).
|
|
118
|
+
- Webhook signature failures (auth misconfig).
|
|
119
|
+
- `assistant-request` SLA breaches (P95 > 5 s).
|
|
120
|
+
|
|
121
|
+
## Rollback
|
|
122
|
+
|
|
123
|
+
Per-phase reversibility:
|
|
124
|
+
|
|
125
|
+
- Phase 1–2 — pause cadence in `campaigns` (`status=paused`).
|
|
126
|
+
- Phase 3 — re-run `update_system_prompt.py` with the prior prompt; the
|
|
127
|
+
diff log is your audit trail.
|
|
128
|
+
- Phase 4 — disable the Cal.com event type (slots return empty; the
|
|
129
|
+
assistant gracefully tells the prospect "no times available, I'll email
|
|
130
|
+
options").
|
|
131
|
+
- Phase 5–6 — flip `isOkayToCall` for the campaign's contact set;
|
|
132
|
+
cadence stops within one cron tick.
|
|
133
|
+
|
|
134
|
+
## Common failure modes
|
|
135
|
+
|
|
136
|
+
- **First call sounds robotic.** Cause: `attemptNumber=1` + empty
|
|
137
|
+
`previousCallSummary` is fine, but **empty `companyName` / `jobTitle`
|
|
138
|
+
also renders as "Information not available"** which spooks the opener.
|
|
139
|
+
Fix at the import job, not the prompt.
|
|
140
|
+
- **DNC requests not honored.** Cause: handler reads `phpWorkerAction`
|
|
141
|
+
before `dncRequested`. Always evaluate `dncRequested` first.
|
|
142
|
+
- **Cal.com returns weekend slots.** Cause: `fix_booking_guardrails.py`
|
|
143
|
+
not applied on the new event type. Re-run after creating each event.
|
|
144
|
+
- **`assistant-request` 7.5 s SLA breach.** Cause: NetSuite synchronous
|
|
145
|
+
lookup in the handler. Move NetSuite out of the hot path.
|
|
146
|
+
|
|
147
|
+
## Change history
|
|
148
|
+
- 2026-06-16 — Initial new-campaign onboarding workflow doc. (akhokhani)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -24,6 +24,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
24
24
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
25
25
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
26
26
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
27
|
+
- **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
27
28
|
|
|
28
29
|
## standalone framework
|
|
29
30
|
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql |
|
|
6
6
|
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql |
|
|
7
|
-
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php |
|
|
7
|
+
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
8
8
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
9
9
|
| [Compass Order Lifecycle & Data-Integrity Invariants](workflows/order-lifecycle-and-data-integrity.md) | 2.0 | End-to-end map of how a Compass order flows through the `Client_Compass` (2.0) database and the **expected raw-data shape** at each link/ASN/IF level. | |
|
|
@@ -6,10 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-16
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
|
+
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
13
14
|
related:
|
|
14
15
|
- asn-to-item-fulfillment.md
|
|
15
16
|
---
|
|
@@ -90,7 +91,34 @@ USA and Canada share the parent handler unchanged.
|
|
|
90
91
|
other orders (e.g. SA114091 25→125) do not fit the split-PO 2× pattern and are a separate,
|
|
91
92
|
uninvestigated cause.
|
|
92
93
|
|
|
94
|
+
- **SECOND, INDEPENDENT source of the same cross-part links — the Office Depot worker cron
|
|
95
|
+
(fixed 2026-06-16).** The 2026-06-11 fix above only covered the `_underscore`
|
|
96
|
+
`_Model_Compass_PurchaseOrder` path. Office-Depot-originated POs are imported by the **1.0
|
|
97
|
+
worker** cron `worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`,
|
|
98
|
+
which had its **own** off-by-one. It creates the bridge twice: (A) correctly, inline in the
|
|
99
|
+
`/purchase-orders` POST payload via `$lookupSalesOrderItemUuidFromPurchaseOrderLine` (keyed by
|
|
100
|
+
the PO item's lineNumber); then (B) a redundant second loop that paired the **API-returned**
|
|
101
|
+
PO items to queued SO links **by array index** (`$queuedSoiPoiLinks[$indPurchaseOrderItem]`),
|
|
102
|
+
assuming the response lists PO items in submission order. When the SO carries a config-parent
|
|
103
|
+
line (line 1), the returned order is offset, so path B inserted an extra bridge row pointing
|
|
104
|
+
each PO item at the SO line **one above** its real line — the same wrong-part scramble, and
|
|
105
|
+
the same downstream tracking misattribution. **Confirmed on SA132763:** each part (9X3V1UT,
|
|
106
|
+
920-012059) received its own tracking number plus a neighbor's, surfacing as duplicate IF
|
|
107
|
+
lines in NetSuite. **Fix:** path B now resolves each returned PO item's SOI by its own
|
|
108
|
+
`lineNumber` via the same `$lookupSalesOrderItemUuidFromPurchaseOrderLine`, and the dead
|
|
109
|
+
`$queuedSoiPoiLinks` is removed. The cleanup predicate is unchanged (`soi.itemId <> vi.itemId`
|
|
110
|
+
via `VendorItems`); for tracking, an item-level `ItemFulfillmentItems_TrackingNumbers` row is
|
|
111
|
+
spurious unless some ASN item carrying that tracking number maps (by actual item) to the IFI's
|
|
112
|
+
SOI — see `dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql`.
|
|
113
|
+
- **Scope still open:** ~116 PO items across other Compass orders still carry the consecutive
|
|
114
|
+
off-by-one bridge fingerprint (a `SalesOrderItems_PurchaseOrderItems` PO item linked to two
|
|
115
|
+
adjacent SO items). Only SA132763 was remediated; a broader reviewed backfill is pending.
|
|
116
|
+
|
|
93
117
|
## Change history
|
|
118
|
+
- 2026-06-16 — Found and fixed a SECOND source of cross-part bridge links: the Office Depot
|
|
119
|
+
worker cron `3a_import_office_depot_purchase_orders.php` paired API-returned PO items to SO
|
|
120
|
+
items by array index; now pairs by lineNumber. Remediated SA132763's spurious bridge +
|
|
121
|
+
cross-line tracking (dated dbchanges2 file). ~116 other orders still pending. (jcardinal)
|
|
94
122
|
- 2026-06-11 — Documented the over-fulfillment symptom and the phantom-IFI cleanup predicate;
|
|
95
123
|
remediated SA132657 and SA132641 (one dated dbchanges2 file each). (jcardinal)
|
|
96
124
|
- 2026-06-11 — Guarded `postPost` SO↔PO item linking to MR orders only; SA links come solely
|
package/knowledge/registry.json
CHANGED
|
@@ -16,5 +16,6 @@
|
|
|
16
16
|
{ "repo": "walmarttechservices", "project": "Walmart Tech Services", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
|
|
17
17
|
{ "repo": "talos", "project": "TOGa IQ", "framework": "2.0", "role": "app", "dependsOn": [] },
|
|
18
18
|
{ "repo": "test", "project": "Test", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
|
|
19
|
-
{ "repo": "voice-to-voice", "project": "TOGa Voice", "framework": "2.0", "role": "app", "dependsOn": [] }
|
|
19
|
+
{ "repo": "voice-to-voice", "project": "TOGa Voice", "framework": "2.0", "role": "app", "dependsOn": [] },
|
|
20
|
+
{ "repo": "ai-bdr", "project": "AI-BDR", "framework": "2.0", "role": "app", "dependsOn": [] }
|
|
20
21
|
]
|
package/package.json
CHANGED