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.
@@ -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)
@@ -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-11
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.93",
3
+ "version": "1.0.95",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",