@leadbay/mcp 0.9.1 → 0.10.0
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/CHANGELOG.md +62 -0
- package/README.md +43 -3
- package/dist/bin.js +385 -53
- package/dist/{chunk-ZRTAIXEJ.js → chunk-F3EWCHME.js} +1599 -244
- package/dist/{dist-X5AOPZ2V.js → dist-BHLIJAIH.js} +17 -5
- package/package.json +4 -2
|
@@ -414,7 +414,21 @@ var LeadbayClient = class {
|
|
|
414
414
|
return this.makeError("AUTH_EXPIRED", "Authentication token expired or invalid", "Your LEADBAY_TOKEN is no longer valid. Regenerate it: npx -y @leadbay/mcp login --email <you> --region <us|fr>, then restart your MCP client.", endpoint);
|
|
415
415
|
}
|
|
416
416
|
if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
|
|
417
|
-
|
|
417
|
+
const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
|
|
418
|
+
return this.makeError(
|
|
419
|
+
"QUOTA_EXCEEDED",
|
|
420
|
+
retryAfter ? `Quota exceeded \u2014 retry in ${retryAfter}s` : "Quota exceeded",
|
|
421
|
+
// The Leadbay user can either wait for the window to reset OR top up
|
|
422
|
+
// AI credits (which clears the throttle immediately). Tell the agent
|
|
423
|
+
// both options exist so it offers the top-up path to the user instead
|
|
424
|
+
// of forcing them to wait. Surface leadbay_create_topup_link so the
|
|
425
|
+
// agent can generate the URL itself instead of asking the user to
|
|
426
|
+
// navigate to a website. Once the user has topped up, the previous
|
|
427
|
+
// 429 is stale — retry the failed call.
|
|
428
|
+
`${hintBase}, OR top up AI credits \u2014 top-ups clear the throttle immediately. Offer the user to generate a Stripe checkout URL via leadbay_create_topup_link, OR direct them to app.leadbay.ai \u2192 Billing. Check leadbay_account_status / leadbay_get_quota to see which resource window (daily/weekly/monthly) was hit. Once the user has topped up, the previous QUOTA_EXCEEDED is stale \u2014 re-call leadbay_account_status to refresh, then RETRY the original operation.`,
|
|
429
|
+
endpoint,
|
|
430
|
+
retryAfter
|
|
431
|
+
);
|
|
418
432
|
}
|
|
419
433
|
if (status === 403) {
|
|
420
434
|
const msg = parsed?.message || parsed?.error || parsed?.error?.message || "";
|
|
@@ -533,11 +547,44 @@ function parseRetryAfter(value) {
|
|
|
533
547
|
}
|
|
534
548
|
|
|
535
549
|
// ../core/dist/tool-descriptions.generated.js
|
|
536
|
-
var leadbay_account_status =
|
|
550
|
+
var leadbay_account_status = `## WHEN TO USE
|
|
537
551
|
|
|
538
|
-
|
|
552
|
+
Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "am I an admin", "is intelligence still regenerating", "I topped up / I bought credits / I added credits".
|
|
539
553
|
|
|
540
|
-
|
|
554
|
+
Do NOT use for: "show me leads / today's prospects" \u2192 \`leadbay_pull_leads\`.
|
|
555
|
+
|
|
556
|
+
Prefer when: user opens with a meta question about themselves / their workspace \u2014 typically as a daily check-in opener
|
|
557
|
+
|
|
558
|
+
Examples that SHOULD invoke this tool:
|
|
559
|
+
- "What's my account status?"
|
|
560
|
+
- "How much quota do I have left this week?"
|
|
561
|
+
- "Is my intelligence still regenerating?"
|
|
562
|
+
|
|
563
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
564
|
+
- "Show me today's leads."
|
|
565
|
+
- "What should I follow up on?"
|
|
566
|
+
- "Run the daily check-in."
|
|
567
|
+
|
|
568
|
+
## RENDER (quick)
|
|
569
|
+
|
|
570
|
+
Compact markdown: org name + admin badge on line 1; active lens on
|
|
571
|
+
line 2; per-window quota usage as \`(used / cap)\` chips for
|
|
572
|
+
llm_completion \xB7 ai_rescore \xB7 web_fetch. Surface regeneration flag
|
|
573
|
+
prominently if mid-regen.
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
Show the user's account state \u2014 admin rights, language, last-active lens, current quota usage across daily/weekly/monthly windows for llm_completion / ai_rescore / web_fetch resources, and whether the org's intelligence is mid-regeneration. Quota windows also hint at the user's consumption pace: heavy recent activity (ai_rescore / web_fetch near their window limits) is a signal that Leadbay will deliver a larger fresh batch next time the user logs back in, since batch size is paced by real consumption.
|
|
578
|
+
|
|
579
|
+
**Top-ups always beat waiting.** When a quota window is hit, the user has two options: wait for the window reset (\`resets_at\` in each quota entry) OR top up AI credits. Top-ups clear the throttle IMMEDIATELY; they are not subject to the same window. When you tell the user about a 429 / quota exhaustion, ALWAYS surface both options \u2014 "wait until <reset>" or "top up now (I can generate the link)" \u2014 and let them pick. Never default-recommend "wait until tomorrow" when a 30-second top-up unblocks the same operation.
|
|
580
|
+
|
|
581
|
+
**Offer the top-up link via \`leadbay_create_topup_link\`.** When the user accepts the top-up offer, call \`leadbay_create_topup_link\` and surface the returned Stripe checkout URL as a clickable link. The user completes payment in their browser; nothing is charged just by generating the URL. For ongoing subscription changes (plan upgrade / payment method), use \`leadbay_open_billing_portal\` instead.
|
|
582
|
+
|
|
583
|
+
**After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. If the retry hits the wall again, only then re-offer top-up / wait. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
|
|
584
|
+
|
|
585
|
+
WHEN TO USE: at the start of a session to know what the agent can/can't do, after a 429 to explain to the user which resource window was exhausted and when it resets (and to offer the top-up alternative), and after the user signals a top-up so the agent can resume the interrupted workflow.
|
|
586
|
+
|
|
587
|
+
WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
|
|
541
588
|
`;
|
|
542
589
|
var leadbay_add_note = `Add a note to a lead. Notes are visible to the whole organization in Leadbay.
|
|
543
590
|
|
|
@@ -567,7 +614,7 @@ var leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk en
|
|
|
567
614
|
|
|
568
615
|
WHEN TO USE: poll this after leadbay_enrich_titles returns a \`bulk_id\`. Default \`include_contacts=false\` for cheap status polls; set \`include_contacts=true\` once \`all_done\` flips for the final read.
|
|
569
616
|
|
|
570
|
-
WHEN NOT TO USE: as a substitute for
|
|
617
|
+
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
|
|
571
618
|
`;
|
|
572
619
|
var leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', qualify_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
|
|
573
620
|
|
|
@@ -611,6 +658,32 @@ Do not enumerate the affected leads \u2014 that's the job of \`leadbay_pull_lead
|
|
|
611
658
|
|
|
612
659
|
## NEXT STEPS \u2014 after kicking off bulk qualification
|
|
613
660
|
|
|
661
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
662
|
+
|
|
663
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
664
|
+
|
|
665
|
+
\`\`\`
|
|
666
|
+
ask_user_input_v0({
|
|
667
|
+
questions: [{
|
|
668
|
+
question: "What next?",
|
|
669
|
+
type: "single_select",
|
|
670
|
+
options: [
|
|
671
|
+
"<Suggest column from row 1>",
|
|
672
|
+
"<Suggest column from row 2>",
|
|
673
|
+
"<Suggest column from row 3>"
|
|
674
|
+
]
|
|
675
|
+
}]
|
|
676
|
+
})
|
|
677
|
+
\`\`\`
|
|
678
|
+
|
|
679
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
680
|
+
|
|
681
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
|
|
614
687
|
Exactly two offers \u2014 keep it terse, this is a status tool:
|
|
615
688
|
|
|
616
689
|
| Observation | Suggest | Calls |
|
|
@@ -658,6 +731,23 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draf
|
|
|
658
731
|
|
|
659
732
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
660
733
|
`;
|
|
734
|
+
var leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.5/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
|
|
735
|
+
|
|
736
|
+
**When a quota window is hit, offer top-up as the FIRST option.** A top-up clears the throttle immediately (no need to wait for the daily/weekly/monthly window reset). The flow:
|
|
737
|
+
|
|
738
|
+
1. Operation returns QUOTA_EXCEEDED / 429.
|
|
739
|
+
2. Tell the user which window was hit (from \`leadbay_account_status\` / \`leadbay_get_quota\`) and offer BOTH options: "wait until \`<resets_at>\`" OR "top up at app.leadbay.ai (I can generate the link)".
|
|
740
|
+
3. If the user accepts top-up, call \`leadbay_create_topup_link\` and surface the returned \`url\` as a clickable link.
|
|
741
|
+
4. Tell the user to complete checkout in their browser. After they return saying "I topped up" / "bought credits" / "added credits", call \`leadbay_account_status\` to confirm the refreshed state, then RETRY the originally failed call.
|
|
742
|
+
|
|
743
|
+
Do NOT call this tool defensively before any quota error \u2014 only after a real 429 or in response to the user's explicit "I want to top up" / "I'll buy credits". The URL is single-use and rotating it without the user asking just clutters the conversation.
|
|
744
|
+
|
|
745
|
+
\`leadbay_open_billing_portal\` is the companion tool for ongoing subscription management (upgrade / downgrade / payment methods) via the Stripe customer portal.
|
|
746
|
+
|
|
747
|
+
WHEN TO USE: when a quota error has blocked the user's intent and they want to keep working today rather than wait for the window reset; OR when the user explicitly asks for a top-up link.
|
|
748
|
+
|
|
749
|
+
WHEN NOT TO USE: pre-flight (the agent is not paying \u2014 the user is); for subscription / plan changes use \`leadbay_open_billing_portal\` instead; for read-only quota diagnostics use \`leadbay_account_status\` / \`leadbay_get_quota\`.
|
|
750
|
+
`;
|
|
661
751
|
var leadbay_deselect_leads = `Remove leads from the user's transient selection.
|
|
662
752
|
|
|
663
753
|
WHEN TO USE: when narrowing a previously-built selection without clearing it entirely.
|
|
@@ -672,6 +762,44 @@ WHEN TO USE: low-level \u2014 when you need raw paginated wishlist access withou
|
|
|
672
762
|
|
|
673
763
|
WHEN NOT TO USE: as the agent's default lead-discovery entry point \u2014 use leadbay_pull_leads, which adds a one-line qualification summary per lead.
|
|
674
764
|
`;
|
|
765
|
+
var leadbay_dislike_lead = `## WHEN TO USE
|
|
766
|
+
|
|
767
|
+
Trigger phrases: "I don't like this lead", "thumbs down", "not relevant", "wrong industry", "too small", "skip this one permanently", "not a fit", "no to this one".
|
|
768
|
+
|
|
769
|
+
Do NOT use for: "remind me about this lead later / snooze it / not now" \u2192 \`leadbay_set_pushback\`; "thumbs up / save this one / this is a good fit" \u2192 \`leadbay_like_lead\`.
|
|
770
|
+
|
|
771
|
+
Prefer when: user expresses durable rejection of a specific lead; pass the lead's UUID as \`lead_id\`. If the user just wants to defer for now, route to \`leadbay_set_pushback\` instead.
|
|
772
|
+
|
|
773
|
+
Examples that SHOULD invoke this tool:
|
|
774
|
+
- "Thumbs down \u2014 wrong industry."
|
|
775
|
+
- "Dislike this one, never show me leads like this again."
|
|
776
|
+
- "Not a fit at all, remove it."
|
|
777
|
+
|
|
778
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
779
|
+
- "Skip this for now, ask me again next month."
|
|
780
|
+
- "I'll look at this one later."
|
|
781
|
+
- "I like this lead \u2014 save it."
|
|
782
|
+
|
|
783
|
+
## RENDER (quick)
|
|
784
|
+
|
|
785
|
+
One short confirmation line after the call returns ("\u{1F44E} Disliked **{company_name}** \u2014
|
|
786
|
+
negative signal sent."), then continue the current flow (don't dump the full
|
|
787
|
+
lead card again).
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
Mark a lead as disliked. This is the same action as clicking the thumbs-down on the Leadbay website. The signal is fed back into the scoring engine: disliked leads teach Leadbay what to filter out, improving the relevance of future batches.
|
|
792
|
+
|
|
793
|
+
Pass the lead's UUID as \`lead_id\`.
|
|
794
|
+
|
|
795
|
+
Dislike is a **permanent** negative signal \u2014 it influences scoring durably. If the user only wants to defer a lead ("not now", "remind me next month", "I'll look at this one later"), call \`leadbay_set_pushback\` instead; that's reversible and time-bounded.
|
|
796
|
+
|
|
797
|
+
WHEN TO USE: the user explicitly rejects a lead ("not relevant", "wrong industry", "too small", "thumbs down", "skip this", "not a fit"). Use proactively after \`leadbay_research_lead\` reveals disqualifying signals.
|
|
798
|
+
|
|
799
|
+
WHEN NOT TO USE: the user simply wants to defer a lead \u2014 use \`leadbay_set_pushback\` to snooze instead. Dislike is a permanent negative signal; pushback is a temporary deferral.
|
|
800
|
+
|
|
801
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
802
|
+
`;
|
|
675
803
|
var leadbay_dismiss_clarification = `Dismiss the pending clarification without answering. Leadbay proceeds with its best guess. Admin-only.
|
|
676
804
|
|
|
677
805
|
WHEN TO USE: when the user explicitly doesn't want to answer the disambiguation.
|
|
@@ -696,6 +824,172 @@ WHEN NOT TO USE: to enrich a single contact \u2014 that's leadbay_enrich_contact
|
|
|
696
824
|
|
|
697
825
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
698
826
|
`;
|
|
827
|
+
var leadbay_followups_map = `## WHEN TO USE
|
|
828
|
+
|
|
829
|
+
Trigger phrases: "I'm going to <city>", "this week's trip", "leads I should visit in person", "visit in person", "show me followups in <city / state / country>", "leads in Texas / California / France / Bavaria etc.", "leads near <city>", "map of leads", "show me on a map", "itinerary", "plan my itinerary", "trip itinerary", "where are my leads".
|
|
830
|
+
|
|
831
|
+
Do NOT use for: "default follow-up table / status view" \u2192 \`leadbay_pull_followups\`; "new leads / today's prospects" \u2192 \`leadbay_pull_leads\`.
|
|
832
|
+
|
|
833
|
+
Prefer when: user signals geographic / map-style intent \u2014 even a passive 'before my trip' mention counts
|
|
834
|
+
|
|
835
|
+
Examples that SHOULD invoke this tool:
|
|
836
|
+
- "I'm flying to New York Thursday \u2014 who should I meet in person?"
|
|
837
|
+
- "Show my leads on a map for the SF trip."
|
|
838
|
+
- "Who can I visit while I'm in Chicago next week?"
|
|
839
|
+
|
|
840
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
841
|
+
- "What should I follow up on this week?"
|
|
842
|
+
- "Show me today's new prospects."
|
|
843
|
+
- "Tell me about Acme Corp."
|
|
844
|
+
|
|
845
|
+
## RENDER (quick)
|
|
846
|
+
|
|
847
|
+
Two render surfaces \u2014 host picks. Primary: route to Claude's
|
|
848
|
+
\`places_map_display_v0\` widget with \`{name, address (lead.location.full),
|
|
849
|
+
latitude+longitude (lead.location.pos[0/1]), notes (short prose with
|
|
850
|
+
bare phone+email)}\` per lead. Fallback: per-lead markdown blocks
|
|
851
|
+
\`### **Company** \xB7 City, ST\` + one-sentence note + bare phone/email
|
|
852
|
+
\u2014 chat hosts auto-detect into place-card carousel. Detail below.
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
Plot the user's follow-up leads on an interactive map \u2014 the canonical surface for travel / in-person / "I'm going to <city>" intent. Wraps \`leadbay_pull_followups\` (same params, response, store-then-apply geo filter); named separately so the agent has an explicit entry-point for travel intent and can route to \`places_map_display_v0\` (when the host exposes it) or emit place-card prose blocks (which most chat hosts auto-detect into Google Place cards).
|
|
857
|
+
|
|
858
|
+
**Common city aliases resolve automatically** \u2014 \`NYC\` / \`New York\` \u2192 City of New York, \`SF\` / \`S.F.\` \u2192 San Francisco, \`LA\` / \`L.A.\` \u2192 Los Angeles, \`DC\` / \`Washington D.C.\` \u2192 Washington, \`Philly\` \u2192 Philadelphia, \`Vegas\` \u2192 Las Vegas, \`NOLA\` \u2192 New Orleans. Pass either an abbreviation, a city name, or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`.
|
|
859
|
+
|
|
860
|
+
**\`city\` is the universal geo arg \u2014 it resolves any admin level.** Despite the name, pass any place name there: states (\`"Texas"\`, \`"California"\`, \`"Bavaria"\`), countries (\`"France"\`, \`"United States"\`), regions (\`"New England"\`, \`"Bay Area"\`), neighborhoods (\`"Brooklyn"\`, \`"SoHo"\`), or cities. The \`/geo/search\` resolver indexes all levels \u2014 level 4 (state), level 2 (country), level 5 (city) \u2014 and the composite picks the best match. **Never** put a place name into \`keywords\` instead \u2014 that's a text-match against company descriptions, not a real geo filter (e.g. \`keywords: ["Texas"]\` returns \u22480 hits even when the user has dozens of Texas leads). If \`keywords: ["<PlaceName>"]\` returned empty, the correct next call is \`city: "<PlaceName>"\`, NOT the unfiltered Monitor view.
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## RENDER \u2014 host-native map widget (REQUIRED, preferred)
|
|
865
|
+
|
|
866
|
+
When the host exposes Claude's \`places_map_display_v0\`, route the leads there. It owns the visual surface (markers, place-card carousel, "Notes from Claude") and beats inline prose by miles.
|
|
867
|
+
|
|
868
|
+
**Call shape \u2014 pass the most precise data you have. The richer the address + coordinates, the more likely Google resolves a real business listing and shows structured property pills (phone, website, Directions button, rating) on each card. Cheap city-level data => basic pin + your notes string only.**
|
|
869
|
+
|
|
870
|
+
\`\`\`
|
|
871
|
+
places_map_display_v0({
|
|
872
|
+
locations: leads.map(l => ({
|
|
873
|
+
name: l.company_name ?? l.name,
|
|
874
|
+
// \u2605 REQUIRED \u2014 pos is [lat, lng] in our payload. Pass them split.
|
|
875
|
+
latitude: l.location.pos[0],
|
|
876
|
+
longitude: l.location.pos[1],
|
|
877
|
+
// \u2605 Pass the FULL detailed address from l.location.full
|
|
878
|
+
// ("1140, 6th Avenue, 10036, City of New York, New York, United States"),
|
|
879
|
+
// NOT a city-level fallback. Google's place lookup needs the street
|
|
880
|
+
// + ZIP to resolve a business listing \u2192 that's what unlocks the
|
|
881
|
+
// structured phone/website/rating pills on the card.
|
|
882
|
+
address: l.location.full ?? [l.location.city, l.location.state, l.location.country].filter(Boolean).join(", "),
|
|
883
|
+
notes: <one-sentence pitch \u2014 see notes recipe below>,
|
|
884
|
+
// place_id omitted \u2014 Leadbay's backend doesn't store Google Place
|
|
885
|
+
// IDs. Google resolves implicitly from name + lat/lng + address.
|
|
886
|
+
// If it doesn't find a listing (small B2B, no Google business page),
|
|
887
|
+
// the card falls back to your notes string only \u2014 that's why the
|
|
888
|
+
// notes string MUST be self-sufficient (phone + email inline).
|
|
889
|
+
})),
|
|
890
|
+
// travel_mode: "driving" if the user mentioned driving / trip, etc.
|
|
891
|
+
})
|
|
892
|
+
\`\`\`
|
|
893
|
+
|
|
894
|
+
Skip any lead whose \`location.pos\` is null \u2014 without lat/lng the widget can't pin it. (Surface them as a "+ N leads without coordinates" footer below the widget instead.)
|
|
895
|
+
|
|
896
|
+
**Notes recipe for each lead** \u2014 "Notes from Claude" on the place card.
|
|
897
|
+
|
|
898
|
+
CRITICAL REALITY OF THE CAROUSEL: it renders the notes string as **a single wrapped paragraph of plain text**. The carousel renderer:
|
|
899
|
+
- STRIPS markdown \u2014 \`[Name](url)\` shows as literal "Name (url)" with the URL visible mid-text;
|
|
900
|
+
- COLLAPSES newlines \u2014 vertical stacking does NOT work;
|
|
901
|
+
- TRUNCATES long URLs mid-string visibly;
|
|
902
|
+
- DOES auto-linkify bare phone numbers (\`+1 212-555-0100\` \u2192 tappable \`tel:\`) and bare email addresses (\`name@company.com\` \u2192 tappable \`mailto:\`) \u2014 those become the only "properties" the user can act on inside the card.
|
|
903
|
+
|
|
904
|
+
So the notes string MUST be short prose with bare-text channels only. Use exactly this shape (one sentence, \u2264 ~30 words):
|
|
905
|
+
|
|
906
|
+
\`\`\`
|
|
907
|
+
\u2605 <One-sentence sector/fit + why-now>. Reach <Contact First Last>, <role>: <bare phone>, <bare email>.
|
|
908
|
+
\`\`\`
|
|
909
|
+
|
|
910
|
+
Examples:
|
|
911
|
+
- \`\u2605 Strongest fit \u2014 active thread, 'trying to reach' from last Friday. Reach Troy Schirk, Principal & CIO: +1 312-550-2382, tschirk@atlasholdingsllc.com.\`
|
|
912
|
+
- \`\u2605 Mid-size HR/staffing match, multi-branch pattern. Reach Irving Enciso, Regional Ops Manager: 952-835-1288, info@employersolutionsgroup.com.\`
|
|
913
|
+
|
|
914
|
+
Rules:
|
|
915
|
+
- ONE sentence. No newlines. No emoji prefixes (\`\u{1F464}\u{1F4DE}\u2709\uFE0F\u{1F310}\` add clutter but do NOT create sections \u2014 the carousel ignores layout).
|
|
916
|
+
- NO markdown links anywhere in \`notes\`. Especially no LinkedIn URLs \u2014 they're long, they mid-truncate visibly, and the carousel renders them as raw text. Save LinkedIn for the chat prose below the widget.
|
|
917
|
+
- Phone + email inline as bare text. They auto-linkify; that's the user's tap target inside the card.
|
|
918
|
+
- Score callout (\`\u2605 Strongest fit\`, \`Score 83\`, etc.) uses \`ai_agent_lead_score\` when present, else \`score\`.
|
|
919
|
+
- Omit channels that aren't enriched yet \u2014 don't write "<no phone>".
|
|
920
|
+
|
|
921
|
+
## Chat prose AFTER the widget (where markdown DOES render)
|
|
922
|
+
|
|
923
|
+
The carousel is the spatial visual. The user still wants the rich contact detail somewhere \u2014 but the right surface for that is the **chat message after invoking the widget**, not the notes inside it. Chat renders markdown links, lists, emoji properly.
|
|
924
|
+
|
|
925
|
+
Below the widget invocation, emit a short structured summary like:
|
|
926
|
+
|
|
927
|
+
\`\`\`
|
|
928
|
+
**Atlas Holdings \u2014 Far Rockaway.** \u2605 Strongest fit. Active thread, "trying to reach" status from last Friday; AI angle is fresh (recent strategic investment signal).
|
|
929
|
+
Contact: **[Troy Schirk](<linkedin_page or constructed search URL>)**, Principal & CIO \xB7 \u260E +1 312-550-2382 \xB7 \u2709 tschirk@atlasholdingsllc.com
|
|
930
|
+
|
|
931
|
+
**Employer Solutions Services \u2014 Midtown.** \u2605 Mid-size HR/staffing match. Same address as the staffing-group sibling \u2014 single visit covers both.
|
|
932
|
+
Contact: **[Irving Enciso](<linkedin URL>)**, Regional Operations Manager \xB7 \u260E 952-835-1288 \xB7 \u2709 info@employersolutionsgroup.com
|
|
933
|
+
\`\`\`
|
|
934
|
+
|
|
935
|
+
Keep it short \u2014 1 lead per ~3 lines, top 3\u20135 most relevant. The LinkedIn-linked contact name lives here (chat markdown works), the channels are listed as \` \xB7 \`-separated pills. **Do NOT enumerate the same leads as a markdown table** \u2014 this list-form summary is the chat-side detail surface.
|
|
936
|
+
|
|
937
|
+
**LinkedIn URL priority** (used in the chat prose, not in carousel notes):
|
|
938
|
+
1. \`recommended_contact.linkedin_page\` when set, not the literal \`"null"\`, starts with \`https://\`;
|
|
939
|
+
2. Constructed search: \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\` with the company name stripped of \`Inc / LLC / Corp / GmbH / Ltd / Co / S.A. / S.L. / PLC / AG / SAS / SARL\` suffixes, URL-encoded. Append a trailing \` \xB0\` to the contact name to mark the fallback path.
|
|
940
|
+
|
|
941
|
+
Open with **one short intro sentence** in chat ("Five lead visits across NYC for your trip next week \u2014 three in Midtown, plus Long Island and one in NJ.") and then invoke the widget, then the chat-side list above. **No markdown table.**
|
|
942
|
+
|
|
943
|
+
**After the widget renders, end the turn with the NEXT STEPS surface** \u2014 not with a prose question. See "GATE \u2014 PREFER BUILT-IN HOST WIDGETS" below: surface 2\u20134 mutually-exclusive moves via \`ask_user_input_v0\` if the host exposes it, else as a short bulleted list. "Want me to plot these on a map or jump to outreach for Atlas?" is exactly the prose pattern to AVOID \u2014 it's a \`single_select\` with two options.
|
|
944
|
+
|
|
945
|
+
## RENDER \u2014 fallback for hosts without \`places_map_display_v0\`
|
|
946
|
+
|
|
947
|
+
If the host doesn't expose the native map, emit per-lead markdown blocks in this **exact** format \u2014 modern chat hosts (Claude.ai web, cowork) auto-detect addresses + company names and render them as a Google-Place-card carousel anyway:
|
|
948
|
+
|
|
949
|
+
\`\`\`
|
|
950
|
+
### **<Company Name>** \xB7 <City>, <State or Country>
|
|
951
|
+
|
|
952
|
+
\u2605 <Score callout>. <One-sentence sector fit + why-now>.
|
|
953
|
+
|
|
954
|
+
\u{1F464} [<Contact First Last>](<LinkedIn URL>) \u2014 <role>
|
|
955
|
+
\u{1F4DE} <bare phone>
|
|
956
|
+
\u2709\uFE0F <bare email>
|
|
957
|
+
\u{1F310} <company website>
|
|
958
|
+
\`\`\`
|
|
959
|
+
|
|
960
|
+
Same one-channel-per-line discipline \u2014 newlines between channels so the carousel renders them as scannable properties (vs a wall of prose). Same auto-linkify rules: bare phone, bare email, markdown-wrapped contact name.
|
|
961
|
+
|
|
962
|
+
The response payload carries everything you need: \`lead.company_name\` (or \`name\`), \`lead.location.city / country / full / pos\`, \`lead.score\`, \`lead.ai_agent_lead_score\`, \`lead.recommended_contact.{first_name, last_name, job_title, linkedin_page, email, phone_number}\`, \`lead.phone_numbers[]\`, \`lead.website\`, \`lead.short_description\`, \`lead.last_monitor_action\` + \`last_monitor_action_at\`. Score callout uses \`ai_agent_lead_score\` when present, else \`score\`.
|
|
963
|
+
|
|
964
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
965
|
+
|
|
966
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
967
|
+
|
|
968
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
969
|
+
|
|
970
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
971
|
+
|---|---|---|
|
|
972
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
973
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
974
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
975
|
+
|
|
976
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
977
|
+
|
|
978
|
+
**Rules:**
|
|
979
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
980
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
981
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
982
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
WHEN TO USE: the user mentions travel, in-person follow-up, "this week's trip", "visiting", "I'm going to", or any phrasing that benefits from a geographic view of pipeline. Also when the user explicitly asks for a map.
|
|
988
|
+
|
|
989
|
+
WHEN NOT TO USE: for the default follow-up table (status badges, AI take, history) \u2014 that's \`leadbay_pull_followups\`. For NEW leads from the Discover wishlist \u2014 that's \`leadbay_pull_leads\`.
|
|
990
|
+
|
|
991
|
+
The response shape is identical to \`leadbay_pull_followups\`: \`{leads, active_filters, pagination, total_excluded_by_pushback, _meta}\` on success; \`{status: "ambiguous_locations", location_ambiguities}\` when a passed \`city\` was ambiguous.
|
|
992
|
+
`;
|
|
699
993
|
var leadbay_get_clarification = `Check whether Leadbay has a pending clarification question \u2014 a question raised when refining the intelligence prompt produced contradictory or ambiguous criteria. Returns \`{pending: false, clarification: null}\` when nothing is pending (the backend returns 204).
|
|
700
994
|
|
|
701
995
|
WHEN TO USE: after leadbay_refine_prompt, to see if Leadbay needs the user to disambiguate.
|
|
@@ -706,7 +1000,7 @@ var leadbay_get_contacts = `Get contacts for a lead, including enriched email an
|
|
|
706
1000
|
|
|
707
1001
|
WHEN TO USE: to check enrichment status (\`contact.enrichment.done\`) on individual leads after a bulk enrichment was launched, or to find the \`contact_id\` needed by leadbay_enrich_contacts.
|
|
708
1002
|
|
|
709
|
-
WHEN NOT TO USE: as a substitute for
|
|
1003
|
+
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id, which already includes enriched contacts in its return.
|
|
710
1004
|
`;
|
|
711
1005
|
var leadbay_get_enrichment_job_titles = `List the actual job titles present across the leads currently in the user's selection \u2014 the candidate set the user can ask to enrich.
|
|
712
1006
|
|
|
@@ -724,7 +1018,7 @@ var leadbay_get_lead_activities = `Get prospecting activity history for a lead (
|
|
|
724
1018
|
|
|
725
1019
|
WHEN TO USE: to avoid redundant outreach and understand where this lead is in the sales process.
|
|
726
1020
|
|
|
727
|
-
WHEN NOT TO USE: when
|
|
1021
|
+
WHEN NOT TO USE: when leadbay_research_lead_by_id has already been called \u2014 it includes recent prospecting actions in its engagement block.
|
|
728
1022
|
`;
|
|
729
1023
|
var leadbay_get_lead_notes = `Read existing notes on a lead \u2014 context the human team or prior agent runs have already captured.
|
|
730
1024
|
|
|
@@ -736,7 +1030,7 @@ var leadbay_get_lead_profile = `Get a full lead profile including company detail
|
|
|
736
1030
|
|
|
737
1031
|
WHEN TO USE: low-level \u2014 for fine-grained access to the raw shape of the lead profile.
|
|
738
1032
|
|
|
739
|
-
WHEN NOT TO USE: as the agent's default lead-detail tool \u2014 use
|
|
1033
|
+
WHEN NOT TO USE: as the agent's default lead-detail tool \u2014 use leadbay_research_lead_by_id, which structures the data top-down (qualification first, then signals, then firmographics, then contacts, then engagement) and reshapes web_fetch.content into a stable array form.
|
|
740
1034
|
`;
|
|
741
1035
|
var leadbay_get_lens_filter = `Read the firmographic filter (sectors, sizes, locations) currently applied to a lens.
|
|
742
1036
|
|
|
@@ -772,7 +1066,7 @@ var leadbay_get_taste_profile = `Get the user's Ideal Buyer Profile, purchase-in
|
|
|
772
1066
|
|
|
773
1067
|
WHEN TO USE: at the very start of a session to understand what kind of leads the user is looking for.
|
|
774
1068
|
|
|
775
|
-
WHEN NOT TO USE: per-lead \u2014
|
|
1069
|
+
WHEN NOT TO USE: per-lead \u2014 leadbay_research_lead_by_id already includes the per-lead qualification answers (which are scored against these org-level questions).
|
|
776
1070
|
`;
|
|
777
1071
|
var leadbay_get_user_prompt = `Read the org's intelligence-refinement prompt (free-text instruction that steers lead recommendations beyond firmographics). Returns \`{prompt: null, set: false}\` when none is configured (the backend returns 204 in that case).
|
|
778
1072
|
|
|
@@ -784,13 +1078,13 @@ var leadbay_get_web_fetch = `Read the AI-generated web-research summary for a le
|
|
|
784
1078
|
|
|
785
1079
|
WHEN TO USE: when the agent already qualified this lead and wants the underlying research to reason from.
|
|
786
1080
|
|
|
787
|
-
WHEN NOT TO USE: as the first read on a lead \u2014 the
|
|
1081
|
+
WHEN NOT TO USE: as the first read on a lead \u2014 the leadbay_research_lead_by_id composite bundles this with qualification answers and reshapes the dict into a stable array form.
|
|
788
1082
|
`;
|
|
789
1083
|
var leadbay_import_and_qualify = `Import + qualify leads in one call. Pass either \`domains: [{domain, name?}]\` (Mode A) OR \`records[]\` with \`mappings\` (Mode B). At least one mapped field must be LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE. Discover the org's mappable surface via \`leadbay_list_mappable_fields\`. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
790
1084
|
|
|
791
1085
|
WHEN TO USE: agent has a list of companies (domains, or CSV-shaped rows from the user's CRM) and wants the full AI qualification \u2014 qualification answers, web-research signals \u2014 without orchestrating import + bulk_qualify_leads + lead_profile chains by hand.
|
|
792
1086
|
|
|
793
|
-
WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use
|
|
1087
|
+
WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use leadbay_research_lead_by_id); high-cadence or untrusted automation \u2014 this mutates user state and consumes ai_rescore + web_fetch quota.
|
|
794
1088
|
|
|
795
1089
|
Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, \`qualify_id\` (resumable handle). Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
|
|
796
1090
|
|
|
@@ -815,13 +1109,39 @@ The response carries either a completed result or an async handle. Render a brie
|
|
|
815
1109
|
|
|
816
1110
|
**When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
|
|
817
1111
|
|
|
818
|
-
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`
|
|
1112
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
819
1113
|
|
|
820
1114
|
|
|
821
1115
|
---
|
|
822
1116
|
|
|
823
1117
|
## NEXT STEPS \u2014 after an import
|
|
824
1118
|
|
|
1119
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
1120
|
+
|
|
1121
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
1122
|
+
|
|
1123
|
+
\`\`\`
|
|
1124
|
+
ask_user_input_v0({
|
|
1125
|
+
questions: [{
|
|
1126
|
+
question: "What next?",
|
|
1127
|
+
type: "single_select",
|
|
1128
|
+
options: [
|
|
1129
|
+
"<Suggest column from row 1>",
|
|
1130
|
+
"<Suggest column from row 2>",
|
|
1131
|
+
"<Suggest column from row 3>"
|
|
1132
|
+
]
|
|
1133
|
+
}]
|
|
1134
|
+
})
|
|
1135
|
+
\`\`\`
|
|
1136
|
+
|
|
1137
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
1138
|
+
|
|
1139
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
1140
|
+
|
|
1141
|
+
---
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
|
|
825
1145
|
| Observation | Suggest | Calls |
|
|
826
1146
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
827
1147
|
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
@@ -831,7 +1151,7 @@ Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_res
|
|
|
831
1151
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
832
1152
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
833
1153
|
`;
|
|
834
|
-
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads /
|
|
1154
|
+
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', handle_id}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
835
1155
|
|
|
836
1156
|
TWO MODES: (A) Domain-list shortcut \u2014 pass \`domains: [{domain, name?}]\`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
837
1157
|
|
|
@@ -839,7 +1159,7 @@ MUTATES USER STATE: each call creates a row in the user's CRM-imports list (visi
|
|
|
839
1159
|
|
|
840
1160
|
WHEN TO USE: you have a list of company domains from another system (CRM, analytics, email correspondents) and need stable Leadbay leadIds; or CRM-shaped rows with custom columns and want to drive the wizard with explicit field mappings.
|
|
841
1161
|
|
|
842
|
-
WHEN NOT TO USE: for prospect discovery (use leadbay_pull_leads); for one specific company's profile (use
|
|
1162
|
+
WHEN NOT TO USE: for prospect discovery (use leadbay_pull_leads); for one specific company's profile (use leadbay_research_lead_by_name_fuzzy); when you can't tolerate the side effects above; when you also want qualification in the same call (use leadbay_import_and_qualify).
|
|
843
1163
|
|
|
844
1164
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
845
1165
|
|
|
@@ -862,13 +1182,39 @@ The response carries either a completed result or an async handle. Render a brie
|
|
|
862
1182
|
|
|
863
1183
|
**When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
|
|
864
1184
|
|
|
865
|
-
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`
|
|
1185
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
866
1186
|
|
|
867
1187
|
|
|
868
1188
|
---
|
|
869
1189
|
|
|
870
1190
|
## NEXT STEPS \u2014 after an import
|
|
871
1191
|
|
|
1192
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
1193
|
+
|
|
1194
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
1195
|
+
|
|
1196
|
+
\`\`\`
|
|
1197
|
+
ask_user_input_v0({
|
|
1198
|
+
questions: [{
|
|
1199
|
+
question: "What next?",
|
|
1200
|
+
type: "single_select",
|
|
1201
|
+
options: [
|
|
1202
|
+
"<Suggest column from row 1>",
|
|
1203
|
+
"<Suggest column from row 2>",
|
|
1204
|
+
"<Suggest column from row 3>"
|
|
1205
|
+
]
|
|
1206
|
+
}]
|
|
1207
|
+
})
|
|
1208
|
+
\`\`\`
|
|
1209
|
+
|
|
1210
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
1211
|
+
|
|
1212
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
|
|
872
1218
|
| Observation | Suggest | Calls |
|
|
873
1219
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
874
1220
|
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
@@ -922,6 +1268,42 @@ WHEN TO USE: low-level.
|
|
|
922
1268
|
|
|
923
1269
|
WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles handles selection lifecycle, preview, launch, and cleanup.
|
|
924
1270
|
|
|
1271
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
1272
|
+
`;
|
|
1273
|
+
var leadbay_like_lead = `## WHEN TO USE
|
|
1274
|
+
|
|
1275
|
+
Trigger phrases: "I like this lead", "thumbs up", "this one looks good", "save this one", "this is a good fit", "more like this", "yes to this one".
|
|
1276
|
+
|
|
1277
|
+
Do NOT use for: "remind me about this lead later / snooze it" \u2192 \`leadbay_set_pushback\`; "not relevant / wrong fit / thumbs down" \u2192 \`leadbay_dislike_lead\`.
|
|
1278
|
+
|
|
1279
|
+
Prefer when: user expresses durable positive interest in a specific lead; pass the lead's UUID as \`lead_id\`
|
|
1280
|
+
|
|
1281
|
+
Examples that SHOULD invoke this tool:
|
|
1282
|
+
- "I like this lead \u2014 show me more like it."
|
|
1283
|
+
- "Thumbs up on Acme Corp, save it."
|
|
1284
|
+
- "This one's a perfect fit, keep them in the rotation."
|
|
1285
|
+
|
|
1286
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
1287
|
+
- "Skip this for now, I'll look at it next week."
|
|
1288
|
+
- "Not relevant \u2014 wrong industry."
|
|
1289
|
+
- "Just show me the top leads today."
|
|
1290
|
+
|
|
1291
|
+
## RENDER (quick)
|
|
1292
|
+
|
|
1293
|
+
One short confirmation line after the call returns ("\u{1F44D} Liked **{company_name}** \u2014
|
|
1294
|
+
positive signal sent."), then continue the current flow (don't dump the full
|
|
1295
|
+
lead card again).
|
|
1296
|
+
|
|
1297
|
+
---
|
|
1298
|
+
|
|
1299
|
+
Mark a lead as liked. This is the same action as clicking the thumbs-up on the Leadbay website. The signal is fed back into the scoring engine: liked leads improve the quality of future batches by teaching Leadbay what the user finds relevant.
|
|
1300
|
+
|
|
1301
|
+
Pass the lead's UUID as \`lead_id\`.
|
|
1302
|
+
|
|
1303
|
+
WHEN TO USE: the user explicitly approves of a lead ("this one looks good", "I like this", "thumbs up", "save this one", "this is a good fit"). Also use after \`leadbay_research_lead\` reveals strong signals.
|
|
1304
|
+
|
|
1305
|
+
WHEN NOT TO USE: the user is just reading or researching a lead without expressing approval. Use \`leadbay_dislike_lead\` for negative signals; use \`leadbay_set_pushback\` for "remind me later" / temporary snooze (like is durable; pushback is reversible).
|
|
1306
|
+
|
|
925
1307
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
926
1308
|
`;
|
|
927
1309
|
var leadbay_list_lenses = `List all available Leadbay lenses (saved lead-search configurations). Each lens defines a different target market or buyer segment. The lens with \`is_last_active=true\` is used by default for lead discovery.
|
|
@@ -930,6 +1312,14 @@ WHEN TO USE: when the user wants to switch lens or asks "what lenses do I have".
|
|
|
930
1312
|
|
|
931
1313
|
WHEN NOT TO USE: in normal flow \u2014 composites auto-resolve the active lens via \`/me.last_requested_lens\`.
|
|
932
1314
|
`;
|
|
1315
|
+
var leadbay_list_locations = `Search the geo / admin-area taxonomy by free-text name and return the matching admin_area ids. This is the primary way to turn a user's "leads in Berlin" / "filter to Lyon" intent into the \`{type: "location_ids", locations: [<id>]}\` shape that the backend filter expects.
|
|
1316
|
+
|
|
1317
|
+
The response has two arrays: \`results\` (top-10 prefix matches ranked by relevance) and \`parents\` (the admin-area chain referenced by \`results[].parent_ids\`, useful for disambiguation breadcrumbs). Each entry: \`{id, country, level, name, parent_ids}\`. The \`level\` is the admin depth \u2014 **5** = region, **6** = county, **7** = township-area, **8** = city/town.
|
|
1318
|
+
|
|
1319
|
+
WHEN TO USE: to resolve a free-text city/region name before passing it to a \`location_ids\` filter (e.g. on \`leadbay_pull_followups({set_filter})\` or \`leadbay_adjust_audience\`). The composite \`leadbay_pull_followups\` accepts \`city: <free-text>\` directly and runs this resolver internally \u2014 prefer that path; reach for this granular tool only when you need to surface candidates to the user before committing.
|
|
1320
|
+
|
|
1321
|
+
WHEN NOT TO USE: when you already have an admin_area id \u2014 pass it as \`city_id\` (composite path) or directly inside the FilterCriterion.
|
|
1322
|
+
`;
|
|
933
1323
|
var leadbay_list_mappable_fields = `List every CRM field the agent can target when calling leadbay_import_leads or leadbay_import_and_qualify. Returns two arrays: \`standard_fields\` (Leadbay's built-in StandardCrmFieldType enum \u2014 LEAD_NAME, LEAD_WEBSITE, LEAD_STATUS, contact + location + sector fields) and \`custom_fields\` (this org's user-defined fields \u2014 id, name, type, and the literal \`mapping_value\` you pass in \`mappings.fields\`). For custom fields, \`mapping_value\` is the wire-format string \`CUSTOM.<id>\` \u2014 pass it verbatim.
|
|
934
1324
|
|
|
935
1325
|
For contact exports, map person data to CONTACT_* fields and still provide parent-company identity via LEADBAY_ID/LEAD_WEBSITE/LEAD_NAME/CRM_ID/SIREN. When contact emails contain business domains, agents may derive a clean company-domain column for LEAD_WEBSITE only when the domain agrees with the row's company/deal/brand context, while preserving the original email as CONTACT_EMAIL. For import files, audit every meaningful source column. If no standard/contact field fits, preserve the data by creating or reusing a custom field unless the column is blank, duplicate plumbing, raw unparsed noise after useful extraction, or harmful to data quality. For HubSpot or other source-system deep links, create or reuse an EXTERNAL_ID/TEXT custom field with leadbay_create_custom_field, then map the source id/link to the returned \`mapping_value\`. Backend mapping_hints are advisory only; for contact files, do not accept hints such as first_name -> LEAD_NAME when the column is clearly a person field.
|
|
@@ -982,6 +1372,17 @@ WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrit
|
|
|
982
1372
|
|
|
983
1373
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
984
1374
|
`;
|
|
1375
|
+
var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
|
|
1376
|
+
|
|
1377
|
+
Sibling of \`leadbay_create_topup_link\`. Use cases differ:
|
|
1378
|
+
|
|
1379
|
+
- **Top-up** (\`leadbay_create_topup_link\`) = one-shot purchase of AI credits to clear an immediate quota throttle. Doesn't change the plan.
|
|
1380
|
+
- **Billing portal** (this tool) = ongoing subscription management. Plan upgrades persist; they raise the recurring window caps (daily/weekly/monthly) rather than buying a one-time bucket.
|
|
1381
|
+
|
|
1382
|
+
WHEN TO USE: when the user asks to upgrade / downgrade / change plan; manage payment methods; view invoices; or "open my billing".
|
|
1383
|
+
|
|
1384
|
+
WHEN NOT TO USE: for one-off credit top-ups (use \`leadbay_create_topup_link\`); for read-only quota state (use \`leadbay_account_status\` / \`leadbay_get_quota\`).
|
|
1385
|
+
`;
|
|
985
1386
|
var leadbay_pick_clarification = `Answer the pending clarification question \u2014 either by picking one of the offered options (\`option_id\`) or by typing a free-text answer. The answer is stored as the new user_prompt and triggers regeneration. Admin-only.
|
|
986
1387
|
|
|
987
1388
|
WHEN TO USE: low-level.
|
|
@@ -990,7 +1391,36 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
|
990
1391
|
|
|
991
1392
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
992
1393
|
`;
|
|
993
|
-
var leadbay_prepare_outreach =
|
|
1394
|
+
var leadbay_prepare_outreach = `## WHEN TO USE
|
|
1395
|
+
|
|
1396
|
+
Trigger phrases: "draft outreach for <Contact>", "write an email to <Contact>", "prep me to call <Contact>", "outreach package for <Company>", "I'm about to reach out to <X>".
|
|
1397
|
+
|
|
1398
|
+
Do NOT use for: "research a lead I haven't picked yet" \u2192 \`leadbay_research_lead_by_id\`; "log outreach I already sent" \u2192 \`leadbay_report_outreach\`; "enrich a batch of contacts" \u2192 \`leadbay_enrich_titles\`.
|
|
1399
|
+
|
|
1400
|
+
Prefer when: user is about to act \u2014 has picked the lead AND the contact. Single-lead, action-imminent context
|
|
1401
|
+
|
|
1402
|
+
Examples that SHOULD invoke this tool:
|
|
1403
|
+
- "Draft an email to Sarah at Acme."
|
|
1404
|
+
- "Help me write outreach for John Smith."
|
|
1405
|
+
- "I'm about to call Acme's CTO \u2014 prep me."
|
|
1406
|
+
|
|
1407
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
1408
|
+
- "I just sent that email \u2014 log it."
|
|
1409
|
+
- "Enrich the contacts on these 20 leads."
|
|
1410
|
+
- "Research the Acme lead before I commit."
|
|
1411
|
+
|
|
1412
|
+
## RENDER (quick)
|
|
1413
|
+
|
|
1414
|
+
Route the draft through \`message_compose_v1\` (Claude's email composer)
|
|
1415
|
+
with 2\u20133 strategic variants \u2014 labels describe STRATEGY, not tone
|
|
1416
|
+
("Push for alignment", "Reference M&A signal", "Soft intro \u2014 peer
|
|
1417
|
+
reference"). Above the composer, emit ONE short markdown paragraph
|
|
1418
|
+
with score callout + sector fit + linked contact name + bare phone /
|
|
1419
|
+
email. Do NOT paste the email body into chat prose alongside.
|
|
1420
|
+
|
|
1421
|
+
---
|
|
1422
|
+
|
|
1423
|
+
Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` (always in the post-enrichment shape \u2014 \`contact_id\`, \`first_name\`, \`last_name\`, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 with nulls where data isn't yet enriched), \`additional_contacts_count\` (other contacts at this company), and an \`enrichment\` block describing async state.
|
|
994
1424
|
|
|
995
1425
|
Optionally trigger contact enrichment in-flight with \`enrich:true\`. Enrichment is async (~60s). **Self-polling pattern (no separate tool needed):** re-call \`leadbay_prepare_outreach(leadId)\` without \`enrich\`; check \`enrichment.complete\`. When \`complete: true\`, the recommended contact now carries \`email\` and/or \`phone_number\`.
|
|
996
1426
|
|
|
@@ -1001,7 +1431,73 @@ IRON LAW \u2014 OUTCOME AFTER OUTREACH. The moment the user reports outreach hap
|
|
|
1001
1431
|
|
|
1002
1432
|
WHEN TO USE: when the agent is about to draft outreach for ONE specific lead and needs everything to compose \u2014 channels + angles + history context.
|
|
1003
1433
|
|
|
1004
|
-
WHEN NOT TO USE: across many leads \u2014 use leadbay_enrich_titles for bulk; for general lead detail use
|
|
1434
|
+
WHEN NOT TO USE: across many leads \u2014 use leadbay_enrich_titles for bulk; for general lead detail use leadbay_research_lead_by_id (richer signals); to actually log the outreach action use leadbay_report_outreach (requires verification).
|
|
1435
|
+
|
|
1436
|
+
---
|
|
1437
|
+
|
|
1438
|
+
## RENDER \u2014 host-native message composer is the PRIMARY surface
|
|
1439
|
+
|
|
1440
|
+
\`message_compose_v1\` is the canonical surface for outreach drafts on this tool. The composer gives the user inline edit + send affordances and beats any prose code-fenced draft. **If the host exposes \`message_compose_v1\`, route every draft through it.** Don't paste email body or call-opener body into chat prose alongside \u2014 the composer IS the visual.
|
|
1441
|
+
|
|
1442
|
+
Above the composer, emit ONE short markdown context paragraph: the lead's score callout + sector fit + recommended-contact name (LinkedIn-markdown-linked) + bare phone/email pills. That gives the user the "why this lead" context. The composer below carries the actionable draft.
|
|
1443
|
+
|
|
1444
|
+
**Single draft:**
|
|
1445
|
+
|
|
1446
|
+
\`\`\`
|
|
1447
|
+
message_compose_v1({
|
|
1448
|
+
kind: "email",
|
|
1449
|
+
summary_title: "Outreach to <Contact Name> at <Company>",
|
|
1450
|
+
variants: [{
|
|
1451
|
+
label: "Lead with the M&A signal",
|
|
1452
|
+
subject: "<one-line subject \u2014 references the angle>",
|
|
1453
|
+
body: "<5-8 sentence email; salesperson voice; references signal + a clear next step>"
|
|
1454
|
+
}]
|
|
1455
|
+
})
|
|
1456
|
+
\`\`\`
|
|
1457
|
+
|
|
1458
|
+
**Strategic options (preferred when split_ai_summary surfaces multiple angles):**
|
|
1459
|
+
|
|
1460
|
+
\`\`\`
|
|
1461
|
+
message_compose_v1({
|
|
1462
|
+
kind: "email",
|
|
1463
|
+
summary_title: "Three angles for <Company> outreach",
|
|
1464
|
+
variants: [
|
|
1465
|
+
{ label: "Push for alignment", subject: "...", body: "..." },
|
|
1466
|
+
{ label: "Reference the M&A signal", subject: "...", body: "..." },
|
|
1467
|
+
{ label: "Soft intro \u2014 peer reference", subject: "...", body: "..." }
|
|
1468
|
+
]
|
|
1469
|
+
})
|
|
1470
|
+
\`\`\`
|
|
1471
|
+
|
|
1472
|
+
Constraints:
|
|
1473
|
+
- **Labels describe STRATEGY, not tone.** "Push for alignment", "Reference M&A signal", "Lead with peer reference" \u2014 not "Friendly" / "Formal" / "Aggressive".
|
|
1474
|
+
- **2\u20133 variants when strategic options are clearly distinct.** One variant when you have a single best-angle draft.
|
|
1475
|
+
- Subject required for \`kind: "email"\`. Phone/call openers use \`kind: "other"\` with the opener in \`body\`.
|
|
1476
|
+
|
|
1477
|
+
The composer becomes the single visual. **Don't also paste the email body into chat prose** \u2014 that's just noise next to the composer.
|
|
1478
|
+
|
|
1479
|
+
For phone-only contacts (no email enriched), use \`kind: "other"\` with a 60-second call opener.
|
|
1480
|
+
|
|
1481
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
1482
|
+
|
|
1483
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
1484
|
+
|
|
1485
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
1486
|
+
|
|
1487
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
1488
|
+
|---|---|---|
|
|
1489
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
1490
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
1491
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
1492
|
+
|
|
1493
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
1494
|
+
|
|
1495
|
+
**Rules:**
|
|
1496
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
1497
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
1498
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
1499
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
1500
|
+
|
|
1005
1501
|
|
|
1006
1502
|
---
|
|
1007
1503
|
|
|
@@ -1029,7 +1525,7 @@ Present as the richest single-record card the MCP emits. The user is seconds-to-
|
|
|
1029
1525
|
**H5: \u{1F3AF} Angles & approach**
|
|
1030
1526
|
|
|
1031
1527
|
- Render \`lead.split_ai_summary.approach_angle\` as the lead-in.
|
|
1032
|
-
- 3\u20134 bullets distilling \`split_ai_summary.next_step\` and any signals from a prior \`
|
|
1528
|
+
- 3\u20134 bullets distilling \`split_ai_summary.next_step\` and any signals from a prior \`research_lead_by_id\` call into salesperson-voice talking points. Cite \`[source](url)\` inline when known.
|
|
1033
1529
|
- Final line: \`Recommended channel: <X> \u2014 <rationale>\`. Compute the recommendation from what data is available (email present \u2192 email; phone present \u2192 call; LinkedIn only \u2192 DM).
|
|
1034
1530
|
|
|
1035
1531
|
**H5: \u{1F4DC} History with [Contact name]**
|
|
@@ -1042,7 +1538,7 @@ Same shape as the contact history, but only include items NOT duplicated from th
|
|
|
1042
1538
|
|
|
1043
1539
|
**H5: \u{1F465} Other contacts** (only if \`additional_contacts_count > 0\`)
|
|
1044
1540
|
|
|
1045
|
-
One line: \`+N more contacts at this company \u2014 [see them all](
|
|
1541
|
+
One line: \`+N more contacts at this company \u2014 [see them all](leadbay_research_lead_by_id)\`.
|
|
1046
1542
|
|
|
1047
1543
|
**Closing line** (when enrichment is in progress): \`*Enrichment running \u2014 I'll refresh once email/phone lands.*\`
|
|
1048
1544
|
|
|
@@ -1078,6 +1574,32 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
1078
1574
|
|
|
1079
1575
|
## NEXT STEPS \u2014 after the outreach brief
|
|
1080
1576
|
|
|
1577
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
1578
|
+
|
|
1579
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
1580
|
+
|
|
1581
|
+
\`\`\`
|
|
1582
|
+
ask_user_input_v0({
|
|
1583
|
+
questions: [{
|
|
1584
|
+
question: "What next?",
|
|
1585
|
+
type: "single_select",
|
|
1586
|
+
options: [
|
|
1587
|
+
"<Suggest column from row 1>",
|
|
1588
|
+
"<Suggest column from row 2>",
|
|
1589
|
+
"<Suggest column from row 3>"
|
|
1590
|
+
]
|
|
1591
|
+
}]
|
|
1592
|
+
})
|
|
1593
|
+
\`\`\`
|
|
1594
|
+
|
|
1595
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
1596
|
+
|
|
1597
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
1598
|
+
|
|
1599
|
+
---
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
|
|
1081
1603
|
Offer 2\u20133 follow-ups. Choose based on enrichment state + available channels + history. Always offer the "log outreach" option once the user has clearly contacted someone.
|
|
1082
1604
|
|
|
1083
1605
|
| Observation | Suggest | Calls |
|
|
@@ -1088,7 +1610,7 @@ Offer 2\u20133 follow-ups. Choose based on enrichment state + available channels
|
|
|
1088
1610
|
| LinkedIn URL available | "Draft the LinkedIn DM" | (agent self-drafts inline) |
|
|
1089
1611
|
| Only company line, no direct phone | "Draft a switchboard script targeting [Contact]" | (agent self-drafts; flag uncertainty) |
|
|
1090
1612
|
| \`additional_contacts_count > 0\` | "Show me the other N contacts at this company" | leadbay_get_contacts(leadId) |
|
|
1091
|
-
| History is empty | "Pull the strategic overview before drafting" |
|
|
1613
|
+
| History is empty | "Pull the strategic overview before drafting" | leadbay_research_lead_by_id(leadId) |
|
|
1092
1614
|
| User reports they reached out | "Log this outreach \u2014 creates prospecting action + outcome" | leadbay_report_outreach(leadId, contact_id, ...) |
|
|
1093
1615
|
| User adds context for next time | "Save a note on the contact or company" | leadbay_add_note |
|
|
1094
1616
|
| After a successful exchange | "Update qualification answers based on what you learned" | leadbay_answer_clarification |
|
|
@@ -1109,7 +1631,36 @@ WHEN NOT TO USE: as a non-admin (will fail with 403); for personal lens changes
|
|
|
1109
1631
|
|
|
1110
1632
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
1111
1633
|
`;
|
|
1112
|
-
var leadbay_pull_followups =
|
|
1634
|
+
var leadbay_pull_followups = `## WHEN TO USE
|
|
1635
|
+
|
|
1636
|
+
Trigger phrases: "what should I follow up on", "leads I've already worked", "leads I haven't contacted", "what's overdue", "stale leads", "before my trip", "this month", "what's going on with my pipeline", "leads in <city / state / country>".
|
|
1637
|
+
|
|
1638
|
+
Do NOT use for: "show me leads / new leads / today's prospects" \u2192 \`leadbay_pull_leads\`; "I'm going to <city> / in person / trip" \u2192 \`leadbay_followups_map\`; "Texas / California / France etc. \u2014 place names go in \`city\`, NOT \`keywords\`" \u2192 \`leadbay_pull_followups\`.
|
|
1639
|
+
|
|
1640
|
+
Prefer when: user names a city / sector / recency window \u2192 pass \`city\` (or \`set_filter\`) so the result is narrowed AND persists for the next session
|
|
1641
|
+
|
|
1642
|
+
Examples that SHOULD invoke this tool:
|
|
1643
|
+
- "What should I follow up on this week?"
|
|
1644
|
+
- "Which leads have I not contacted in 30 days?"
|
|
1645
|
+
- "What's overdue in my pipeline?"
|
|
1646
|
+
|
|
1647
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
1648
|
+
- "Show me today's new leads."
|
|
1649
|
+
- "I'm flying to Berlin Thursday \u2014 who can I meet?"
|
|
1650
|
+
- "Draft an email to Sarah at Acme."
|
|
1651
|
+
|
|
1652
|
+
## RENDER (quick)
|
|
1653
|
+
|
|
1654
|
+
4-col markdown table sorted by \`last_monitor_action_at\` desc, NO
|
|
1655
|
+
score bar (status badges instead). Col 1 = status (\u{1F3AF}\u26A1\u{1F7E2}\u{1F4A4}\u2728\u{1F525}\u2744) +
|
|
1656
|
+
company link + location/size. Col 2 = AI take (3 lines from
|
|
1657
|
+
\`split_ai_summary\`). Col 3 = history + notes. Col 4 = contacts
|
|
1658
|
+
(\u2605 recommended, \u260E \u{1F4E7} pills). Active-filters chip line ABOVE the
|
|
1659
|
+
table. Detail + status priority below.
|
|
1660
|
+
|
|
1661
|
+
---
|
|
1662
|
+
|
|
1663
|
+
Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
|
|
1113
1664
|
|
|
1114
1665
|
Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\` to persist the filter server-side. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing the page restores it.
|
|
1115
1666
|
|
|
@@ -1126,7 +1677,9 @@ Practical mapping from user phrasing to criterion:
|
|
|
1126
1677
|
| "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
|
|
1127
1678
|
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
1128
1679
|
|
|
1129
|
-
Geo filtering
|
|
1680
|
+
Geo filtering needs \`admin_area_id\` resolution \u2014 backend rejects free-text in \`location_ids\`. Pass \`city: "<free-text>"\` and the composite calls \`/geo/search\` internally, picks the best match, merges its id into \`set_filter\`. Ambiguous matches return \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`. For multi-city cases, call \`leadbay_list_locations\` then pass \`set_filter.criteria\` with \`{type: "location_ids", is_excluded: false, locations: [...]}\`.
|
|
1681
|
+
|
|
1682
|
+
**Place names go through \`city\`, NEVER \`keywords\`.** This includes any geographic token the user names \u2014 cities (\`"Berlin"\`, \`"NYC"\`), states / provinces / regions (\`"Texas"\`, \`"California"\`, \`"Bavaria"\`), countries (\`"France"\`, \`"United States"\`), neighborhoods (\`"Brooklyn"\`, \`"SoHo"\`). The \`/geo/search\` resolver handles all admin levels \u2014 level 4 (state) and level 2 (country) resolve just as well as level 5 (city). If you put \`"Texas"\` in \`keywords\` you get a TEXT-MATCH against company descriptions (\u22480 hits) instead of a real state filter. If a place name resolves ambiguously, surface the choices to the user \u2014 do NOT silently fall back to keyword search or to the unfiltered Monitor view. If \`keywords: ["Texas"]\` returned empty, the next call is \`city: "Texas"\`, not \`keywords: []\`.
|
|
1130
1683
|
|
|
1131
1684
|
**Pushback exclusion.** Leads with active pushback (\`pushback_status\` set and \`pushback_until > today\`) are excluded from the response. The composite enforces this client-side; \`total_excluded_by_pushback\` in the output reports how many rows were dropped.
|
|
1132
1685
|
|
|
@@ -1134,7 +1687,7 @@ WHEN TO USE: re-engaging pipeline ("what should I follow up on", "stale leads"),
|
|
|
1134
1687
|
|
|
1135
1688
|
WHEN NOT TO USE: for NEW leads \u2014 that's \`leadbay_pull_leads\` (Discover).
|
|
1136
1689
|
|
|
1137
|
-
**Anti-confusion guardrail.**
|
|
1690
|
+
**Anti-confusion guardrail.** Iterating \`pull_leads\` pages looking for \`prospecting_actions_count > 0\` or \`notes_count > 0\` rows is the wrong entry point \u2014 the two read different tables. Leads with follow-up history live in \`pull_followups\`.
|
|
1138
1691
|
|
|
1139
1692
|
---
|
|
1140
1693
|
|
|
@@ -1227,6 +1780,32 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
1227
1780
|
|
|
1228
1781
|
## NEXT STEPS \u2014 after the follow-ups table
|
|
1229
1782
|
|
|
1783
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
1784
|
+
|
|
1785
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
1786
|
+
|
|
1787
|
+
\`\`\`
|
|
1788
|
+
ask_user_input_v0({
|
|
1789
|
+
questions: [{
|
|
1790
|
+
question: "What next?",
|
|
1791
|
+
type: "single_select",
|
|
1792
|
+
options: [
|
|
1793
|
+
"<Suggest column from row 1>",
|
|
1794
|
+
"<Suggest column from row 2>",
|
|
1795
|
+
"<Suggest column from row 3>"
|
|
1796
|
+
]
|
|
1797
|
+
}]
|
|
1798
|
+
})
|
|
1799
|
+
\`\`\`
|
|
1800
|
+
|
|
1801
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
1802
|
+
|
|
1803
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
1804
|
+
|
|
1805
|
+
---
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
|
|
1230
1809
|
Always include at least one filter-modification offer (users think in filters: by city, by recency, by action type). Filter modification goes through \`set_filter: FilterItem\` which the composite POSTs to \`/monitor/filter\` server-side.
|
|
1231
1810
|
|
|
1232
1811
|
| Observation | Suggest | Calls |
|
|
@@ -1236,16 +1815,47 @@ Always include at least one filter-modification offer (users think in filters: b
|
|
|
1236
1815
|
| \`pagination.has_more == true\` | "Pull the next page" | leadbay_pull_followups(page = current + 1) |
|
|
1237
1816
|
| \u22653 rows \u2728 (never-touched) | "Surface only never-touched leads" | set_filter with \`last_action_date.last_days = 0\` |
|
|
1238
1817
|
| \u22653 rows \u26A1 (Trying to reach) | "Focus on overdue commitments" | set_filter with \`last_action.types = ["EPILOGUE_COULD_NOT_REACH_STILL_TRYING"]\` |
|
|
1239
|
-
| User planning a trip / in a city | "Group by city for trip planning" |
|
|
1818
|
+
| User planning a trip / in a city | "Group by city for trip planning" | leadbay_pull_followups({city: "<their city>"}) \u2014 composite resolves admin_area_id via /geo/search |
|
|
1240
1819
|
| All rows last action > 60d | "Re-qualify \u2014 context may have changed" | leadbay_bulk_qualify_leads([leadId, ...]) |
|
|
1241
|
-
| One obvious priority row | "Take me to that lead's full brief" | leadbay_prepare_outreach(leadId) /
|
|
1820
|
+
| One obvious priority row | "Take me to that lead's full brief" | leadbay_prepare_outreach(leadId) / leadbay_research_lead_by_id(leadId) |
|
|
1242
1821
|
| User wants to defer a lead | "Snooze [Company] for 3 / 6 / 12 months" | leadbay_set_pushback({ lead_ids:[leadId], status:"3" }) |
|
|
1243
1822
|
| User completed outreach mid-flow | "Log the outreach + record the outcome" | leadbay_report_outreach |
|
|
1244
1823
|
| Discovery mode might fit better | "Looking for NEW leads instead? Switch to discovery." | leadbay_pull_leads |
|
|
1245
1824
|
|
|
1246
1825
|
Always offer at least one of: prep outreach, refilter, pushback. Pushback is the canonical way to honor "not now" / "next quarter" \u2014 leads with active pushback are excluded from this view until expiry.
|
|
1247
1826
|
`;
|
|
1248
|
-
var leadbay_pull_leads =
|
|
1827
|
+
var leadbay_pull_leads = `## WHEN TO USE
|
|
1828
|
+
|
|
1829
|
+
Trigger phrases: "show me leads", "today's prospects", "what's new today", "let's prospect", "best new leads", "my batch".
|
|
1830
|
+
|
|
1831
|
+
Do NOT use for: "leads I should follow up with" \u2192 \`leadbay_pull_followups\`; "leads I've already worked" \u2192 \`leadbay_pull_followups\`; "I'm going to <city>" \u2192 \`leadbay_followups_map\`; "in person" \u2192 \`leadbay_followups_map\`.
|
|
1832
|
+
|
|
1833
|
+
Prefer when: user has named a specific lens \u2014 pass \`lensId\` and pin it for the rest of the session
|
|
1834
|
+
|
|
1835
|
+
Examples that SHOULD invoke this tool:
|
|
1836
|
+
- "Show me today's leads."
|
|
1837
|
+
- "What's in my inbox this morning?"
|
|
1838
|
+
- "Pull my best new prospects."
|
|
1839
|
+
|
|
1840
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
1841
|
+
- "Which leads should I follow up with this week?"
|
|
1842
|
+
- "Show leads I've already contacted."
|
|
1843
|
+
- "I'm flying to Berlin Thursday \u2014 who should I meet?"
|
|
1844
|
+
|
|
1845
|
+
## RENDER (quick)
|
|
1846
|
+
|
|
1847
|
+
3-col markdown table sorted by \`score\` desc \u2014 DO NOT print the numeric
|
|
1848
|
+
score. Col 1 = inline-code 10-segment bar (\`\u25B0\` firmographic, \`\u2756\` AI
|
|
1849
|
+
booster cap at the right end of the filled run, \`\u25B1\` empty;
|
|
1850
|
+
filled=round(score/10), ai=round(avg_boost/3.3)) + \`<br>\` + linked
|
|
1851
|
+
company \xB7 location \xB7 size. Col 2 = why-fits \u226420 words. Col 3 = linked
|
|
1852
|
+
contact + title. Full algorithm + linking rules below.
|
|
1853
|
+
|
|
1854
|
+
---
|
|
1855
|
+
|
|
1856
|
+
Pull up new leads from the user's last-active lens \u2014 the canonical "show me today's prospects" tool.
|
|
1857
|
+
|
|
1858
|
+
Leadbay works like an inbox: each time the user logs back in, a fresh batch is delivered, paced by how many leads they've actually acted on recently. Pulling more won't produce more; user outreach/skips/saves does. Each returned lead carries a one-line \`qualification_summary\` built from leadbay_ai_agent_responses, plus the rich tags / scores / engagement counters / in-flight flags from the lead summary.
|
|
1249
1859
|
|
|
1250
1860
|
Roughly the top 10 of the batch come pre-qualified (populated qualification_summary + ai_agent_lead_score); leads below the top ~10 carry only the basic firmographic \`score\` \u2014 not worse, just resource-saved by the system. Call leadbay_bulk_qualify_leads to deepen any of them on demand \u2014 a healthy daily rhythm is to bulk-qualify the rows without \u2756 caps so tomorrow's top-10 list is richer.
|
|
1251
1861
|
|
|
@@ -1300,7 +1910,7 @@ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is nul
|
|
|
1300
1910
|
**Column 2 \u2014 Why it fits**
|
|
1301
1911
|
|
|
1302
1912
|
- One sentence, \u2264 20 words.
|
|
1303
|
-
- Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`
|
|
1913
|
+
- Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
|
|
1304
1914
|
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
1305
1915
|
- No bullet lists, no line breaks inside the cell.
|
|
1306
1916
|
|
|
@@ -1340,14 +1950,40 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
1340
1950
|
|
|
1341
1951
|
## NEXT STEPS \u2014 after rendering the pull_leads table
|
|
1342
1952
|
|
|
1343
|
-
|
|
1953
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
1954
|
+
|
|
1955
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
1956
|
+
|
|
1957
|
+
\`\`\`
|
|
1958
|
+
ask_user_input_v0({
|
|
1959
|
+
questions: [{
|
|
1960
|
+
question: "What next?",
|
|
1961
|
+
type: "single_select",
|
|
1962
|
+
options: [
|
|
1963
|
+
"<Suggest column from row 1>",
|
|
1964
|
+
"<Suggest column from row 2>",
|
|
1965
|
+
"<Suggest column from row 3>"
|
|
1966
|
+
]
|
|
1967
|
+
}]
|
|
1968
|
+
})
|
|
1969
|
+
\`\`\`
|
|
1970
|
+
|
|
1971
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
1972
|
+
|
|
1973
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
1974
|
+
|
|
1975
|
+
---
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
Pick 2\u20133 items below based on what was actually observed in the response. The table is the source of truth for which moves are valid.
|
|
1344
1980
|
|
|
1345
1981
|
| Observation | Suggest | Calls |
|
|
1346
1982
|
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
1347
1983
|
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
1348
1984
|
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
1349
|
-
| User points at a single row | "Research [Company] in depth" |
|
|
1350
|
-
| User
|
|
1985
|
+
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
|
|
1986
|
+
| User only has a name (no leadId in context) | "Look up [Company] by name" | leadbay_research_lead_by_name_fuzzy(companyName) |
|
|
1351
1987
|
| Top row has phone AND email | "Prepare an outreach for [Contact] \u2014 call + email" | leadbay_prepare_outreach(leadId) |
|
|
1352
1988
|
| Top row has email but no phone | "Draft an outreach email for [Contact]" | leadbay_prepare_outreach(leadId) |
|
|
1353
1989
|
| Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
|
|
@@ -1369,7 +2005,7 @@ var leadbay_qualify_status = `Retrieve the current state of an import_and_qualif
|
|
|
1369
2005
|
|
|
1370
2006
|
WHEN TO USE: after leadbay_import_and_qualify or leadbay_bulk_qualify_leads returned a \`qualify_id\` with non-empty \`still_running[]\`, call this tool a few minutes later (or hours) to retrieve the now-completed qualifications without re-running the import or re-spending qualify quota.
|
|
1371
2007
|
|
|
1372
|
-
WHEN NOT TO USE: as a substitute for
|
|
2008
|
+
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that's a deeper per-lead profile and includes contacts. This tool is purely the qualification answers + signals_count.
|
|
1373
2009
|
`;
|
|
1374
2010
|
var leadbay_recall_ordered_titles = `Show job titles the org has previously enriched, so the agent can repeat the same titles for new leads (or skip already-saturated ones). Two implementation paths: (1) PREFERRED \u2014 a selection-scoped preview call that reads \`previously_enriched_titles\` from the backend (newer prod field). (2) FALLBACK \u2014 live aggregation across each lead's enriched contacts. The composite picks transparently.
|
|
1375
2011
|
|
|
@@ -1413,13 +2049,64 @@ WHEN NOT TO USE: BEFORE doing the outreach (use \`dry_run:true\` to validate arg
|
|
|
1413
2049
|
|
|
1414
2050
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
1415
2051
|
`;
|
|
1416
|
-
var
|
|
2052
|
+
var leadbay_research_lead_by_id = `## WHEN TO USE
|
|
2053
|
+
|
|
2054
|
+
Trigger phrases: "tell me about this lead", "deep dive on the lead I just picked", "is <lead> a fit", "should I pursue <lead>", "everything you know about lead <UUID>", "research this lead".
|
|
1417
2055
|
|
|
1418
|
-
|
|
2056
|
+
Do NOT use for: "user names a company in prose without a Leadbay lead id" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
1419
2057
|
|
|
1420
|
-
|
|
2058
|
+
Prefer when: user already picked a row from pull_leads/pull_followups \u2014 you have the UUID. Pass it via \`leadId\`.
|
|
1421
2059
|
|
|
1422
|
-
|
|
2060
|
+
Examples that SHOULD invoke this tool:
|
|
2061
|
+
- "Tell me everything about that lead I just picked."
|
|
2062
|
+
- "Deep dive on the lead at the top of my list."
|
|
2063
|
+
- "Is this one actually a fit \u2014 what does the AI think?"
|
|
2064
|
+
|
|
2065
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
2066
|
+
- "Look up Acme Corp for me."
|
|
2067
|
+
- "What do we know about Globex?"
|
|
2068
|
+
- "Show me today's leads."
|
|
2069
|
+
|
|
2070
|
+
---
|
|
2071
|
+
|
|
2072
|
+
Tell me everything decision-relevant about a single lead, identified by its
|
|
2073
|
+
Leadbay UUID. Bundles the lens-scoped lead profile, the AI qualification
|
|
2074
|
+
answers (the agent's knowledge-base food), the structured web-research signals
|
|
2075
|
+
(with hot flags + sources), the two-tier contact set (\`enriched\` + \`org\`), the
|
|
2076
|
+
unified \`recent_activities\` timeline, the engagement counts, and a
|
|
2077
|
+
\`_meta.has_reachable_contact\` hint that drives NEXT STEPS. Order is
|
|
2078
|
+
deliberate: qualification first, then signals, then firmographics, then
|
|
2079
|
+
contacts, then recent activity.
|
|
2080
|
+
|
|
2081
|
+
Scoring has two layers: the basic \`score\` (firmographic, always present,
|
|
2082
|
+
already decent) and the AI qualification layer (\`ai_agent_lead_score\` +
|
|
2083
|
+
per-question answers + web_fetch signals). The AI layer is pre-populated for
|
|
2084
|
+
roughly the top 10 of each daily batch, and on-demand (via
|
|
2085
|
+
leadbay_bulk_qualify_leads) for anything below that. Combine both layers when
|
|
2086
|
+
judging a lead.
|
|
2087
|
+
|
|
2088
|
+
The companion tool **leadbay_research_lead_by_name_fuzzy** wraps this one for
|
|
2089
|
+
the case where the user names a company in prose without a UUID \u2014 it
|
|
2090
|
+
fuzzy-resolves the name against the active lens's wishlist, then delegates
|
|
2091
|
+
here. Both return the same shape; the fuzzy wrapper just adds
|
|
2092
|
+
\`_meta.resolved_from\` and \`_meta.match_candidates\` so you can offer
|
|
2093
|
+
disambiguation.
|
|
2094
|
+
|
|
2095
|
+
WHEN TO USE: when picking up a single lead from
|
|
2096
|
+
leadbay_pull_leads (or any list that exposed a leadId) to decide whether to
|
|
2097
|
+
act on it.
|
|
2098
|
+
|
|
2099
|
+
WHEN NOT TO USE: across many leads at once \u2014 that's
|
|
2100
|
+
leadbay_pull_leads' job. (This composite supersedes the lower-level
|
|
2101
|
+
leadbay_get_lead_profile in agent flow; the granular tool stays available for
|
|
2102
|
+
fine-grained access.)
|
|
2103
|
+
|
|
2104
|
+
**Concurrency note**: this is a composite that reads many sub-resources per
|
|
2105
|
+
call. Call it **sequentially** or in small batches (\u22643 parallel) when
|
|
2106
|
+
researching multiple leads. Firing 10+ in parallel can saturate the transport
|
|
2107
|
+
and produce misleading \`"Tool permission stream closed"\` errors that look like
|
|
2108
|
+
permission failures but are really backpressure. On a transient
|
|
2109
|
+
stream/timeout failure, retry the same lead once before moving on.
|
|
1423
2110
|
|
|
1424
2111
|
---
|
|
1425
2112
|
|
|
@@ -1427,7 +2114,7 @@ WHEN NOT TO USE: when you already have the lead_id and need the bundled deeper l
|
|
|
1427
2114
|
|
|
1428
2115
|
Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
|
|
1429
2116
|
|
|
1430
|
-
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`
|
|
2117
|
+
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead_by_id\`, or decide whether to start outreach.
|
|
1431
2118
|
|
|
1432
2119
|
**MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
|
|
1433
2120
|
|
|
@@ -1502,49 +2189,278 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
1502
2189
|
|
|
1503
2190
|
## NEXT STEPS \u2014 after the research card
|
|
1504
2191
|
|
|
1505
|
-
|
|
2192
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
2193
|
+
|
|
2194
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
2195
|
+
|
|
2196
|
+
\`\`\`
|
|
2197
|
+
ask_user_input_v0({
|
|
2198
|
+
questions: [{
|
|
2199
|
+
question: "What next?",
|
|
2200
|
+
type: "single_select",
|
|
2201
|
+
options: [
|
|
2202
|
+
"<Suggest column from row 1>",
|
|
2203
|
+
"<Suggest column from row 2>",
|
|
2204
|
+
"<Suggest column from row 3>"
|
|
2205
|
+
]
|
|
2206
|
+
}]
|
|
2207
|
+
})
|
|
2208
|
+
\`\`\`
|
|
2209
|
+
|
|
2210
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
2211
|
+
|
|
2212
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
2213
|
+
|
|
2214
|
+
---
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
|
|
2218
|
+
Offer 2\u20133 follow-ups that match what the lead's response actually contains.
|
|
1506
2219
|
|
|
1507
|
-
|
|
2220
|
+
**Primary branching signal**: \`contacts.reachable[]\` vs \`contacts.candidates[]\`.
|
|
2221
|
+
- \`contacts.reachable[]\` = people with an email or phone right now. Message them directly.
|
|
2222
|
+
- \`contacts.candidates[]\` = people identified at this company (LinkedIn-only), \`enrichment_done: false\`. Cannot be messaged without first calling \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach({enrich:true})\`).
|
|
2223
|
+
- \`_meta.has_reachable_contact\` is the boolean shortcut \u2014 true iff \`reachable.length > 0\` or the recommended_contact has a channel.
|
|
1508
2224
|
|
|
1509
|
-
|
|
1510
|
-
|--------------------------------------------------------|----------------------------------------------------|----------------------------------------------------|
|
|
1511
|
-
| \`qualification[]\` is empty | "Run AI qualification on this lead" | leadbay_bulk_qualify_leads([leadId]) |
|
|
1512
|
-
| \u22651 hot recent item in \u{1F4C8} business signals | "Prepare outreach referencing [signal headline]" | leadbay_prepare_outreach(leadId) |
|
|
1513
|
-
| \`contacts_count > len(contacts)\` shown | "Pull the full contact list (N more)" | leadbay_get_contacts(leadId) |
|
|
1514
|
-
| \`web_insights_fetched_at\` > 30 days | "Re-run the web research \u2014 this is stale" | leadbay_research_company(leadId) \u2014 refresh |
|
|
1515
|
-
| User wants the deeper lens-scoped bundle | "Pull the full lead profile (research_lead)" | leadbay_research_lead(leadId) |
|
|
1516
|
-
| User is exploring multiple companies | "Back to the lead list" | leadbay_pull_leads |
|
|
1517
|
-
| \`qualification[]\` non-empty | "Expand the AI qualification answers" | (render qualification[] as a sub-card) |
|
|
2225
|
+
Always offer a cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
1518
2226
|
|
|
1519
|
-
|
|
2227
|
+
### MODE A \u2014 Nobody reachable yet (\`contacts.reachable\` is empty)
|
|
1520
2228
|
|
|
1521
|
-
|
|
2229
|
+
| Observation | Suggest | Calls |
|
|
2230
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2231
|
+
| \`contacts.candidates[]\` non-empty | "Enrich N candidate contacts to acquire emails / phones" | leadbay_enrich_titles({ leadIds: [leadId] }) |
|
|
2232
|
+
| User wants outreach now anyway | "Enrich + draft outreach in one shot" | leadbay_prepare_outreach({ leadId, enrich: true }) |
|
|
2233
|
+
| \`qualification[]\` is empty | "Run AI qualification on this lead first" | leadbay_bulk_qualify_leads([leadId]) |
|
|
2234
|
+
| \`web_insights_fetched_at\` older than 30 days | "Refresh the web research \u2014 this is stale" | leadbay_research_lead_by_id({ leadId }) \u2014 re-runs the fetch |
|
|
2235
|
+
| User is exploring multiple companies | "Back to the lead list" | leadbay_pull_leads |
|
|
1522
2236
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
| \`phone_numbers[]\` non-empty | "Show full call notes + a 60-second opener" | leadbay_prepare_outreach(leadId) |
|
|
1526
|
-
| Recommended contact has an email | "Draft the outreach email" | leadbay_prepare_outreach(leadId) |
|
|
1527
|
-
| Neither phone nor email for recommended contact | "Order contact enrichment first" | leadbay_prepare_outreach(leadId, enrich:true) or leadbay_enrich_titles |
|
|
1528
|
-
| After the user reports a touchpoint | "Log the call/email outcome" | leadbay_report_outreach |
|
|
1529
|
-
| Adding pre-call context | "Add a note to this lead" | leadbay_add_note |
|
|
2237
|
+
End MODE A with the pivot offer: \`"Want the strategic overview before
|
|
2238
|
+
enriching? (already shown above)"\`
|
|
1530
2239
|
|
|
1531
|
-
|
|
2240
|
+
### MODE B \u2014 At least one reachable contact (\`contacts.reachable[]\` non-empty OR recommended_contact has channels)
|
|
2241
|
+
|
|
2242
|
+
| Observation | Suggest | Calls |
|
|
2243
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2244
|
+
| Recommended contact has an email | "Draft the outreach email" | leadbay_prepare_outreach({ leadId }) |
|
|
2245
|
+
| \`firmographics.phone_numbers[]\` non-empty | "Show full call notes + a 60-second opener" | leadbay_prepare_outreach({ leadId }) |
|
|
2246
|
+
| \`recent_activities[]\` non-empty | "Log a follow-up touchpoint" | leadbay_report_outreach |
|
|
2247
|
+
| Adding pre-call context | "Add a note to this lead" | leadbay_add_note |
|
|
2248
|
+
| \`qualification[]\` non-empty | "Expand the AI qualification answers" | (render qualification[] as a sub-card) |
|
|
2249
|
+
|
|
2250
|
+
End MODE B with the pivot offer: \`"Want to qualify deeper before reaching
|
|
2251
|
+
out?"\`
|
|
2252
|
+
|
|
2253
|
+
### Cross-mode rows (always available)
|
|
2254
|
+
|
|
2255
|
+
| Observation | Suggest | Calls |
|
|
2256
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2257
|
+
| User is done with this lead | "Back to the inbox" | leadbay_pull_leads |
|
|
1532
2258
|
`;
|
|
1533
|
-
var
|
|
2259
|
+
var leadbay_research_lead_by_name_fuzzy = `## WHEN TO USE
|
|
2260
|
+
|
|
2261
|
+
Trigger phrases: "look up <Company>", "research <Company>", "what do we know about <Company>", "find <Company> in my pipeline", "tell me about <Company>".
|
|
2262
|
+
|
|
2263
|
+
Do NOT use for: "user picked a row from a previous list (you have leadId)" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
2264
|
+
|
|
2265
|
+
Prefer when: user names a company in prose without a Leadbay id \u2014 composite fuzzy-matches against the active lens's wishlist.
|
|
2266
|
+
|
|
2267
|
+
Examples that SHOULD invoke this tool:
|
|
2268
|
+
- "Look up Acme Corp for me."
|
|
2269
|
+
- "What do we know about Globex?"
|
|
2270
|
+
- "Find Initech in my pipeline."
|
|
2271
|
+
|
|
2272
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
2273
|
+
- "Tell me about that lead I just picked."
|
|
2274
|
+
- "Show me today's prospects."
|
|
2275
|
+
- "Draft outreach to Acme's CTO."
|
|
2276
|
+
|
|
2277
|
+
---
|
|
2278
|
+
|
|
2279
|
+
Thin wrapper around **leadbay_research_lead_by_id**. Accepts a \`companyName\`,
|
|
2280
|
+
fuzzy-matches it against the top 50 of the active (or supplied) lens's
|
|
2281
|
+
wishlist, picks the highest-scoring substring hit, and delegates the actual
|
|
2282
|
+
research to \`leadbay_research_lead_by_id\`. Returns the **same payload shape**
|
|
2283
|
+
as \`_by_id\`, with two additions on \`_meta\`:
|
|
2284
|
+
|
|
2285
|
+
- \`_meta.resolved_from\`: \`"companyName"\` (so the agent knows the entry point).
|
|
2286
|
+
- \`_meta.resolved_query\`: the original \`companyName\` string.
|
|
2287
|
+
- \`_meta.match_candidates[]\`: up to 4 next-best substring matches as
|
|
2288
|
+
\`{leadId, name, score}\` \u2014 surface these when ambiguity is plausible so the
|
|
2289
|
+
user can redirect.
|
|
2290
|
+
|
|
2291
|
+
On zero matches, throws \`LEAD_NOT_FOUND\` with a \`nearest_names[]\` payload (the
|
|
2292
|
+
top 5 by score from the lens, regardless of substring) so the agent can offer
|
|
2293
|
+
"did you mean\u2026" disambiguation.
|
|
2294
|
+
|
|
2295
|
+
WHEN TO USE: when the user references a company by
|
|
2296
|
+
name and you don't yet have its \`lead_id\`. If \`_meta.match_candidates\` is
|
|
2297
|
+
non-empty, offer the next-best matches in NEXT STEPS so the user can correct
|
|
2298
|
+
a wrong fuzzy hit.
|
|
2299
|
+
|
|
2300
|
+
WHEN NOT TO USE: when you already have the UUID \u2014 use
|
|
2301
|
+
leadbay_research_lead_by_id directly. This wrapper costs one extra
|
|
2302
|
+
\`discoverLeads\` round-trip; skipping it when you can is cheaper.
|
|
2303
|
+
|
|
2304
|
+
---
|
|
2305
|
+
|
|
2306
|
+
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
2307
|
+
|
|
2308
|
+
Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
|
|
2309
|
+
|
|
2310
|
+
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead_by_id\`, or decide whether to start outreach.
|
|
2311
|
+
|
|
2312
|
+
**MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
|
|
2313
|
+
|
|
2314
|
+
Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
2315
|
+
|
|
2316
|
+
### Common structure (both modes)
|
|
2317
|
+
|
|
2318
|
+
- **Header** (H4 or H5): \`<10-segment score bar>\` \`[Company name](website)\`. Use the score-bar algorithm; the bar lives in a single inline-code span. Prefix \`https://\` to website if it's a bare hostname.
|
|
2319
|
+
- **Pill row** (immediately below the header): short location \xB7 compact size \xB7 social pill chips iterated over \`social_urls\` (each non-null platform becomes \`[<platform-label>](<url>)\`) \xB7 \`[website-domain](website)\` \xB7 \`\u260E phone\` when \`phone_numbers[]\` is non-empty (use the first number). All \` \xB7 \`-separated.
|
|
2320
|
+
- **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
|
|
2321
|
+
- **Staleness line**: italic, \`"Researched <relative time>"\` from \`web_insights_fetched_at\`. Use \`"today"\` / \`"yesterday"\` / \`"N days ago"\` up to 30 days, then absolute date. Prefix with \`\u26A0\` if older than 30 days.
|
|
2322
|
+
- **Contacts table** (always at the bottom):
|
|
2323
|
+
\`\`\`
|
|
2324
|
+
| | Name | Title | LinkedIn |
|
|
2325
|
+
\`\`\`
|
|
2326
|
+
Markers in column 1:
|
|
2327
|
+
- \`\u2605\` \u2014 \`recommended_contact\` match.
|
|
2328
|
+
- \`\u{1F48E}\` \u2014 name fuzzy-matches a \`hot: true\` entry in \`web_insights\` key_people. (Use \`\u{1F48E}\`, not \`\u{1F525}\`, to avoid glyph collision with the follow-up status badge.)
|
|
2329
|
+
Sort \`\u2605\` first, then \`\u{1F48E}\`-only rows, then API order. Link the name via \`linkedin_page\` first; fall back to LinkedIn people-search with \`<First>+<Last>+<Company>\`. Append \`\xB0\` only when the fallback is in use AND \`social_presence.linkedin == false\`. Cap to 6 rows; if \`contacts_count > shown\`, end with \`"+N more \u2014 ask to see the full list"\`.
|
|
2330
|
+
|
|
2331
|
+
### MODE A body (Discovery, fuller, scannable)
|
|
2332
|
+
|
|
2333
|
+
Render each non-empty \`web_insights\` section as H5 with the emoji + label intact. Section order: \`\u{1F3E2} company profile\` \u2192 \`\u{1F4C8} business signals\` \u2192 \`\u{1F4A1} prospecting clues\` \u2192 \`\u{1F9E9} strategic positioning\` \u2192 \`\u{1F50E} technologies & innovation\`. Inside each, bullet 3\u20135 items. Sort \`hot: true\` items first. **Bold** the description text of hot items; leave cold items plain. Render \`source\` as \`[source](url)\` at the end; include \`date\` when present. Omit empty sections. Skip \`\u{1F517} social links\` (already in the pill row) and \`\u{1F464} key people\` (already in the contacts table).
|
|
2334
|
+
|
|
2335
|
+
### MODE B body (Contact preparation, tighter)
|
|
2336
|
+
|
|
2337
|
+
Render exactly two H5 sections:
|
|
2338
|
+
|
|
2339
|
+
##### \u{1F3AF} Conversation hooks
|
|
2340
|
+
|
|
2341
|
+
Distill the 3 most recent / most hot signals from \`\u{1F4C8} business signals\` and \`\u{1F4A1} prospecting clues\` into one-sentence talking points in salesperson voice. Strip the academic framing. Cite the source inline.
|
|
2342
|
+
|
|
2343
|
+
##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
|
|
2344
|
+
|
|
2345
|
+
2-line summary: their title + any context from \`web_insights\` key_people. If they appear in a hot signal ("X appointed CEO"), surface that prominently.
|
|
2346
|
+
|
|
2347
|
+
Skip \u{1F3E2} profile, \u{1F9E9} strategic positioning, \u{1F50E} technologies in MODE B \u2014 context the user doesn't need for the next 30 seconds.
|
|
2348
|
+
|
|
2349
|
+
If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
|
|
2350
|
+
|
|
2351
|
+
**Hide:** \`id\`, \`lead.id\`, \`contact.id\`, \`lead.location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`recommended_contact_title\` (duplicates \`recommended_contact.job_title\`), empty arrays, fields whose value is the string \`"null"\`, \`contact.source\` (internal), insights whose \`source\` is empty.
|
|
2352
|
+
|
|
2353
|
+
**Legend (print once below the card):** \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster \xB7 \`\u25B1\` unfilled \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \xB0 = no company LinkedIn (fallback link only) \`\`
|
|
2354
|
+
|
|
2355
|
+
## Linking a contact's name
|
|
2356
|
+
|
|
2357
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
2358
|
+
|
|
2359
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
2360
|
+
|
|
2361
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
2362
|
+
|
|
2363
|
+
\`\`\`
|
|
2364
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
2365
|
+
\`\`\`
|
|
2366
|
+
|
|
2367
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
2368
|
+
|
|
2369
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
2370
|
+
|
|
2371
|
+
## Linking the company
|
|
2372
|
+
|
|
2373
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
2374
|
+
|
|
2375
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
2376
|
+
|
|
2377
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
|
|
2381
|
+
---
|
|
2382
|
+
|
|
2383
|
+
## NEXT STEPS \u2014 after the research card
|
|
2384
|
+
|
|
2385
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
2386
|
+
|
|
2387
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
2388
|
+
|
|
2389
|
+
\`\`\`
|
|
2390
|
+
ask_user_input_v0({
|
|
2391
|
+
questions: [{
|
|
2392
|
+
question: "What next?",
|
|
2393
|
+
type: "single_select",
|
|
2394
|
+
options: [
|
|
2395
|
+
"<Suggest column from row 1>",
|
|
2396
|
+
"<Suggest column from row 2>",
|
|
2397
|
+
"<Suggest column from row 3>"
|
|
2398
|
+
]
|
|
2399
|
+
}]
|
|
2400
|
+
})
|
|
2401
|
+
\`\`\`
|
|
2402
|
+
|
|
2403
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
2404
|
+
|
|
2405
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
2406
|
+
|
|
2407
|
+
---
|
|
2408
|
+
|
|
1534
2409
|
|
|
1535
|
-
Scoring has two layers: the basic \`score\` (firmographic, always present, already decent) and the AI qualification layer (\`ai_agent_lead_score\` + per-question answers + web_fetch signals). The AI layer is pre-populated for roughly the top 10 of each daily batch, and on-demand (via leadbay_bulk_qualify_leads) for anything below that. Combine both layers when judging a lead.
|
|
1536
2410
|
|
|
1537
|
-
|
|
2411
|
+
Offer 2\u20133 follow-ups that match what the lead's response actually contains.
|
|
1538
2412
|
|
|
1539
|
-
|
|
2413
|
+
**Primary branching signal**: \`contacts.reachable[]\` vs \`contacts.candidates[]\`.
|
|
2414
|
+
- \`contacts.reachable[]\` = people with an email or phone right now. Message them directly.
|
|
2415
|
+
- \`contacts.candidates[]\` = people identified at this company (LinkedIn-only), \`enrichment_done: false\`. Cannot be messaged without first calling \`leadbay_enrich_titles\` (or \`leadbay_prepare_outreach({enrich:true})\`).
|
|
2416
|
+
- \`_meta.has_reachable_contact\` is the boolean shortcut \u2014 true iff \`reachable.length > 0\` or the recommended_contact has a channel.
|
|
1540
2417
|
|
|
1541
|
-
|
|
2418
|
+
Always offer a cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
2419
|
+
|
|
2420
|
+
### MODE A \u2014 Nobody reachable yet (\`contacts.reachable\` is empty)
|
|
2421
|
+
|
|
2422
|
+
| Observation | Suggest | Calls |
|
|
2423
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2424
|
+
| \`contacts.candidates[]\` non-empty | "Enrich N candidate contacts to acquire emails / phones" | leadbay_enrich_titles({ leadIds: [leadId] }) |
|
|
2425
|
+
| User wants outreach now anyway | "Enrich + draft outreach in one shot" | leadbay_prepare_outreach({ leadId, enrich: true }) |
|
|
2426
|
+
| \`qualification[]\` is empty | "Run AI qualification on this lead first" | leadbay_bulk_qualify_leads([leadId]) |
|
|
2427
|
+
| \`web_insights_fetched_at\` older than 30 days | "Refresh the web research \u2014 this is stale" | leadbay_research_lead_by_id({ leadId }) \u2014 re-runs the fetch |
|
|
2428
|
+
| User is exploring multiple companies | "Back to the lead list" | leadbay_pull_leads |
|
|
2429
|
+
|
|
2430
|
+
End MODE A with the pivot offer: \`"Want the strategic overview before
|
|
2431
|
+
enriching? (already shown above)"\`
|
|
2432
|
+
|
|
2433
|
+
### MODE B \u2014 At least one reachable contact (\`contacts.reachable[]\` non-empty OR recommended_contact has channels)
|
|
2434
|
+
|
|
2435
|
+
| Observation | Suggest | Calls |
|
|
2436
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2437
|
+
| Recommended contact has an email | "Draft the outreach email" | leadbay_prepare_outreach({ leadId }) |
|
|
2438
|
+
| \`firmographics.phone_numbers[]\` non-empty | "Show full call notes + a 60-second opener" | leadbay_prepare_outreach({ leadId }) |
|
|
2439
|
+
| \`recent_activities[]\` non-empty | "Log a follow-up touchpoint" | leadbay_report_outreach |
|
|
2440
|
+
| Adding pre-call context | "Add a note to this lead" | leadbay_add_note |
|
|
2441
|
+
| \`qualification[]\` non-empty | "Expand the AI qualification answers" | (render qualification[] as a sub-card) |
|
|
2442
|
+
|
|
2443
|
+
End MODE B with the pivot offer: \`"Want to qualify deeper before reaching
|
|
2444
|
+
out?"\`
|
|
2445
|
+
|
|
2446
|
+
### Cross-mode rows (always available)
|
|
2447
|
+
|
|
2448
|
+
| Observation | Suggest | Calls |
|
|
2449
|
+
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
2450
|
+
| User is done with this lead | "Back to the inbox" | leadbay_pull_leads |
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
2454
|
+
|
|
2455
|
+
| Observation | Suggest | Calls |
|
|
2456
|
+
|--------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|
|
|
2457
|
+
| \`_meta.match_candidates\` non-empty | "Did you mean **<next-best.name>**?" | leadbay_research_lead_by_id(leadId=<next-best.leadId>) |
|
|
1542
2458
|
`;
|
|
1543
2459
|
var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
|
|
1544
2460
|
|
|
1545
2461
|
WHEN TO USE: before importing user-supplied files when domains, names, CRM IDs, registry numbers, or Leadbay IDs may be inconsistently formatted; when the agent needs to pre-resolve messy rows, inspect ambiguous candidates, or prepare LEADBAY_ID values for the import composites. For contact-only files, first derive company website/domain from business contact emails where possible, while ignoring consumer mailbox domains. Deterministic matches get a LEADBAY_ID column inserted so the standard import commits immediately. Ambiguous rows are deliberately left without LEADBAY_ID; inspect candidates and choose one only when the evidence is good. Rows with websites but no match can still be imported; Leadbay may crawl and match them later, and leadbay_import_status can surface late matches.
|
|
1546
2462
|
|
|
1547
|
-
WHEN NOT TO USE: for prospect discovery from scratch (use leadbay_pull_leads); for one known company profile (use
|
|
2463
|
+
WHEN NOT TO USE: for prospect discovery from scratch (use leadbay_pull_leads); for one known company profile (use leadbay_research_lead_by_name_fuzzy / leadbay_research_lead_by_id); or when the file already has clean, final LEADBAY_ID/CRM_ID/SIREN mappings and no row-level identity disambiguation is needed.
|
|
1548
2464
|
|
|
1549
2465
|
---
|
|
1550
2466
|
|
|
@@ -2380,30 +3296,77 @@ var listSectors = {
|
|
|
2380
3296
|
idempotentHint: true,
|
|
2381
3297
|
openWorldHint: true
|
|
2382
3298
|
},
|
|
2383
|
-
description: leadbay_list_sectors,
|
|
2384
|
-
inputSchema: {
|
|
3299
|
+
description: leadbay_list_sectors,
|
|
3300
|
+
inputSchema: {
|
|
3301
|
+
type: "object",
|
|
3302
|
+
properties: {
|
|
3303
|
+
lang: { type: "string", description: "BCP-47 language tag (default: en)" },
|
|
3304
|
+
includeInvisible: {
|
|
3305
|
+
type: "boolean",
|
|
3306
|
+
description: "Include sectors hidden from the UI (default false; ~91k items if true)"
|
|
3307
|
+
}
|
|
3308
|
+
},
|
|
3309
|
+
additionalProperties: false
|
|
3310
|
+
},
|
|
3311
|
+
execute: async (client, params) => {
|
|
3312
|
+
let lang = params.lang;
|
|
3313
|
+
if (!lang) {
|
|
3314
|
+
try {
|
|
3315
|
+
const me = await client.resolveMe();
|
|
3316
|
+
lang = me.language ?? "en";
|
|
3317
|
+
} catch {
|
|
3318
|
+
lang = "en";
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
const includeInvisible = params.includeInvisible ? "true" : "false";
|
|
3322
|
+
const path = `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=${includeInvisible}`;
|
|
3323
|
+
return await client.request("GET", path);
|
|
3324
|
+
}
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
// ../core/dist/tools/list-locations.js
|
|
3328
|
+
var listLocations = {
|
|
3329
|
+
name: "leadbay_list_locations",
|
|
3330
|
+
annotations: {
|
|
3331
|
+
title: "Search the geo / admin-area taxonomy",
|
|
3332
|
+
readOnlyHint: true,
|
|
3333
|
+
destructiveHint: false,
|
|
3334
|
+
idempotentHint: true,
|
|
3335
|
+
openWorldHint: true
|
|
3336
|
+
},
|
|
3337
|
+
description: leadbay_list_locations,
|
|
3338
|
+
inputSchema: {
|
|
3339
|
+
type: "object",
|
|
3340
|
+
properties: {
|
|
3341
|
+
q: {
|
|
3342
|
+
type: "string",
|
|
3343
|
+
description: "Free-text city / region name (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). Returns top-10 prefix matches sorted by relevance, each with an admin_area id usable in FilterCriterion.location_ids."
|
|
3344
|
+
}
|
|
3345
|
+
},
|
|
3346
|
+
required: ["q"],
|
|
3347
|
+
additionalProperties: false
|
|
3348
|
+
},
|
|
3349
|
+
outputSchema: {
|
|
2385
3350
|
type: "object",
|
|
2386
3351
|
properties: {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
3352
|
+
results: {
|
|
3353
|
+
type: "array",
|
|
3354
|
+
description: "Matches sorted by relevance. Each entry: {id, country, level, name, parent_ids}. `level` is admin depth (5=region, 6=county, 7=township-area, 8=city/town).",
|
|
3355
|
+
items: { type: "object" }
|
|
3356
|
+
},
|
|
3357
|
+
parents: {
|
|
3358
|
+
type: "array",
|
|
3359
|
+
description: "Parent admin areas referenced by `results[].parent_ids`, returned for breadcrumb / hover-disambiguation rendering.",
|
|
3360
|
+
items: { type: "object" }
|
|
2391
3361
|
}
|
|
2392
3362
|
},
|
|
2393
|
-
|
|
3363
|
+
required: ["results", "parents"]
|
|
2394
3364
|
},
|
|
2395
3365
|
execute: async (client, params) => {
|
|
2396
|
-
|
|
2397
|
-
if (!
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
lang = me.language ?? "en";
|
|
2401
|
-
} catch {
|
|
2402
|
-
lang = "en";
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
const includeInvisible = params.includeInvisible ? "true" : "false";
|
|
2406
|
-
const path = `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=${includeInvisible}`;
|
|
3366
|
+
const q = (params.q ?? "").trim();
|
|
3367
|
+
if (!q)
|
|
3368
|
+
return { results: [], parents: [] };
|
|
3369
|
+
const path = `/geo/search?q=${encodeURIComponent(q)}`;
|
|
2407
3370
|
return await client.request("GET", path);
|
|
2408
3371
|
}
|
|
2409
3372
|
};
|
|
@@ -2587,7 +3550,7 @@ var getWebFetch = {
|
|
|
2587
3550
|
},
|
|
2588
3551
|
outputSchema: {
|
|
2589
3552
|
type: "object",
|
|
2590
|
-
description: "Raw LeadWebFetchPayload as returned by /leads/{id}/web_fetch. Permissive shape \u2014 backend dict structure is documented in detail by
|
|
3553
|
+
description: "Raw LeadWebFetchPayload as returned by /leads/{id}/web_fetch. Permissive shape \u2014 backend dict structure is documented in detail by leadbay_research_lead_by_id which reshapes it.",
|
|
2591
3554
|
properties: {
|
|
2592
3555
|
content: {
|
|
2593
3556
|
description: "Backend dict content (object or string), or null when no fetch yet."
|
|
@@ -4188,6 +5151,60 @@ function coerceCsvValue(v) {
|
|
|
4188
5151
|
return "";
|
|
4189
5152
|
}
|
|
4190
5153
|
|
|
5154
|
+
// ../core/dist/tools/create-topup-link.js
|
|
5155
|
+
var createTopupLink = {
|
|
5156
|
+
name: "leadbay_create_topup_link",
|
|
5157
|
+
annotations: {
|
|
5158
|
+
title: "Generate Stripe checkout URL for AI-credits top-up",
|
|
5159
|
+
readOnlyHint: true,
|
|
5160
|
+
destructiveHint: false,
|
|
5161
|
+
idempotentHint: false,
|
|
5162
|
+
openWorldHint: true
|
|
5163
|
+
},
|
|
5164
|
+
description: leadbay_create_topup_link,
|
|
5165
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5166
|
+
outputSchema: {
|
|
5167
|
+
type: "object",
|
|
5168
|
+
properties: {
|
|
5169
|
+
url: {
|
|
5170
|
+
type: "string",
|
|
5171
|
+
description: "Stripe-hosted checkout URL. Surface as a clickable link; the user completes payment in their browser. Top-up clears the throttle immediately after Stripe webhook lands."
|
|
5172
|
+
}
|
|
5173
|
+
},
|
|
5174
|
+
required: ["url"]
|
|
5175
|
+
},
|
|
5176
|
+
execute: async (client) => {
|
|
5177
|
+
return await client.request("POST", "/stripe/topup_checkout", {});
|
|
5178
|
+
}
|
|
5179
|
+
};
|
|
5180
|
+
|
|
5181
|
+
// ../core/dist/tools/open-billing-portal.js
|
|
5182
|
+
var openBillingPortal = {
|
|
5183
|
+
name: "leadbay_open_billing_portal",
|
|
5184
|
+
annotations: {
|
|
5185
|
+
title: "Generate Stripe customer-portal URL for subscription management",
|
|
5186
|
+
readOnlyHint: true,
|
|
5187
|
+
destructiveHint: false,
|
|
5188
|
+
idempotentHint: false,
|
|
5189
|
+
openWorldHint: true
|
|
5190
|
+
},
|
|
5191
|
+
description: leadbay_open_billing_portal,
|
|
5192
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5193
|
+
outputSchema: {
|
|
5194
|
+
type: "object",
|
|
5195
|
+
properties: {
|
|
5196
|
+
url: {
|
|
5197
|
+
type: "string",
|
|
5198
|
+
description: "Stripe customer-portal URL. Surface as a clickable link; the user manages subscription / payment methods / invoices in their browser."
|
|
5199
|
+
}
|
|
5200
|
+
},
|
|
5201
|
+
required: ["url"]
|
|
5202
|
+
},
|
|
5203
|
+
execute: async (client) => {
|
|
5204
|
+
return await client.request("GET", "/stripe/portal");
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
|
|
4191
5208
|
// ../core/dist/tools/select-leads.js
|
|
4192
5209
|
var selectLeads = {
|
|
4193
5210
|
name: "leadbay_select_leads",
|
|
@@ -4994,87 +6011,63 @@ var createCustomField = {
|
|
|
4994
6011
|
}
|
|
4995
6012
|
};
|
|
4996
6013
|
|
|
4997
|
-
// ../core/dist/
|
|
4998
|
-
var
|
|
4999
|
-
name: "
|
|
6014
|
+
// ../core/dist/tools/like-lead.js
|
|
6015
|
+
var likeLead = {
|
|
6016
|
+
name: "leadbay_like_lead",
|
|
5000
6017
|
annotations: {
|
|
5001
|
-
title: "
|
|
5002
|
-
readOnlyHint:
|
|
6018
|
+
title: "Like a lead",
|
|
6019
|
+
readOnlyHint: false,
|
|
5003
6020
|
destructiveHint: false,
|
|
5004
6021
|
idempotentHint: true,
|
|
5005
6022
|
openWorldHint: true
|
|
5006
6023
|
},
|
|
5007
|
-
description:
|
|
6024
|
+
description: leadbay_like_lead,
|
|
6025
|
+
optional: true,
|
|
6026
|
+
write: true,
|
|
5008
6027
|
inputSchema: {
|
|
5009
6028
|
type: "object",
|
|
5010
6029
|
properties: {
|
|
5011
|
-
|
|
5012
|
-
type: "string",
|
|
5013
|
-
description: "Company name to research (one of companyName or leadId required). Matches the top-scoring lead with this name."
|
|
5014
|
-
},
|
|
5015
|
-
leadId: {
|
|
6030
|
+
lead_id: {
|
|
5016
6031
|
type: "string",
|
|
5017
|
-
description: "
|
|
6032
|
+
description: "UUID of the lead to like."
|
|
5018
6033
|
}
|
|
5019
6034
|
},
|
|
6035
|
+
required: ["lead_id"],
|
|
5020
6036
|
additionalProperties: false
|
|
5021
6037
|
},
|
|
5022
|
-
|
|
6038
|
+
execute: async (client, params) => {
|
|
6039
|
+
await client.requestVoid("POST", `/leads/${params.lead_id}/like`);
|
|
6040
|
+
return { applied: true, lead_id: params.lead_id, action: "liked" };
|
|
6041
|
+
}
|
|
6042
|
+
};
|
|
6043
|
+
|
|
6044
|
+
// ../core/dist/tools/dislike-lead.js
|
|
6045
|
+
var dislikeLead = {
|
|
6046
|
+
name: "leadbay_dislike_lead",
|
|
6047
|
+
annotations: {
|
|
6048
|
+
title: "Dislike a lead",
|
|
6049
|
+
readOnlyHint: false,
|
|
6050
|
+
destructiveHint: true,
|
|
6051
|
+
idempotentHint: true,
|
|
6052
|
+
openWorldHint: true
|
|
6053
|
+
},
|
|
6054
|
+
description: leadbay_dislike_lead,
|
|
6055
|
+
optional: true,
|
|
6056
|
+
write: true,
|
|
6057
|
+
inputSchema: {
|
|
5023
6058
|
type: "object",
|
|
5024
6059
|
properties: {
|
|
5025
|
-
|
|
5026
|
-
type: "
|
|
5027
|
-
description: "
|
|
5028
|
-
},
|
|
5029
|
-
qualification: {
|
|
5030
|
-
type: ["array", "null"],
|
|
5031
|
-
description: "Per-question AI qualification answers ({question, score, response, computed_at, outdated_at}), or null if none.",
|
|
5032
|
-
items: { type: "object" }
|
|
5033
|
-
},
|
|
5034
|
-
contacts: {
|
|
5035
|
-
type: "array",
|
|
5036
|
-
description: "Merged org + paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
5037
|
-
items: { type: "object" }
|
|
5038
|
-
},
|
|
5039
|
-
web_insights: {
|
|
5040
|
-
description: "Latest /web_fetch content (string) or null when no fetch is available."
|
|
5041
|
-
},
|
|
5042
|
-
web_insights_fetched_at: {
|
|
5043
|
-
description: "ISO timestamp of the latest /web_fetch (string) or null."
|
|
5044
|
-
},
|
|
5045
|
-
recent_activities: {
|
|
5046
|
-
type: "array",
|
|
5047
|
-
description: "Recent activities for this lead (top 20). Each is the activity payload as returned by /leads/{id}/activities.",
|
|
5048
|
-
items: { type: "object" }
|
|
6060
|
+
lead_id: {
|
|
6061
|
+
type: "string",
|
|
6062
|
+
description: "UUID of the lead to dislike."
|
|
5049
6063
|
}
|
|
5050
6064
|
},
|
|
5051
|
-
required: ["
|
|
6065
|
+
required: ["lead_id"],
|
|
6066
|
+
additionalProperties: false
|
|
5052
6067
|
},
|
|
5053
|
-
execute: async (client, params
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
}
|
|
5057
|
-
let leadId = params.leadId;
|
|
5058
|
-
if (!leadId && params.companyName) {
|
|
5059
|
-
const results = await discoverLeads.execute(client, { count: 50, page: 0 }, ctx);
|
|
5060
|
-
const needle = params.companyName.toLowerCase();
|
|
5061
|
-
const match = results.leads.find((l) => l.name.toLowerCase().includes(needle));
|
|
5062
|
-
if (!match) {
|
|
5063
|
-
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${params.companyName}" in the current lens`, "Call leadbay_pull_leads (optionally with a broader lensId) to see what's available, then call this with leadId.");
|
|
5064
|
-
}
|
|
5065
|
-
leadId = match.id;
|
|
5066
|
-
}
|
|
5067
|
-
const [profile, activities] = await Promise.allSettled([
|
|
5068
|
-
getLeadProfile.execute(client, { leadId }, ctx),
|
|
5069
|
-
getLeadActivities.execute(client, { leadId, count: 20 }, ctx)
|
|
5070
|
-
]);
|
|
5071
|
-
if (profile.status === "rejected") {
|
|
5072
|
-
throw profile.reason;
|
|
5073
|
-
}
|
|
5074
|
-
return {
|
|
5075
|
-
...profile.value,
|
|
5076
|
-
recent_activities: activities.status === "fulfilled" ? activities.value.activities : []
|
|
5077
|
-
};
|
|
6068
|
+
execute: async (client, params) => {
|
|
6069
|
+
await client.requestVoid("POST", `/leads/${params.lead_id}/dislike`);
|
|
6070
|
+
return { applied: true, lead_id: params.lead_id, action: "disliked" };
|
|
5078
6071
|
}
|
|
5079
6072
|
};
|
|
5080
6073
|
|
|
@@ -5441,6 +6434,119 @@ var pullLeads = {
|
|
|
5441
6434
|
}
|
|
5442
6435
|
};
|
|
5443
6436
|
|
|
6437
|
+
// ../core/dist/composite/_geo-helpers.js
|
|
6438
|
+
var looksLikeId = (s) => /^\d+$/.test(s);
|
|
6439
|
+
var CITY_ALIASES = {
|
|
6440
|
+
nyc: "City of New York",
|
|
6441
|
+
"ny city": "City of New York",
|
|
6442
|
+
"new york": "City of New York",
|
|
6443
|
+
// user typing "New York" almost always means the city
|
|
6444
|
+
"new york city": "City of New York",
|
|
6445
|
+
manhattan: "City of New York",
|
|
6446
|
+
"the big apple": "City of New York",
|
|
6447
|
+
la: "Los Angeles",
|
|
6448
|
+
"l.a.": "Los Angeles",
|
|
6449
|
+
sf: "San Francisco",
|
|
6450
|
+
"s.f.": "San Francisco",
|
|
6451
|
+
"san fran": "San Francisco",
|
|
6452
|
+
dc: "Washington",
|
|
6453
|
+
"d.c.": "Washington",
|
|
6454
|
+
"washington d.c.": "Washington",
|
|
6455
|
+
"washington dc": "Washington",
|
|
6456
|
+
philly: "Philadelphia",
|
|
6457
|
+
vegas: "Las Vegas",
|
|
6458
|
+
nola: "New Orleans"
|
|
6459
|
+
};
|
|
6460
|
+
function expandAlias(text) {
|
|
6461
|
+
const key = text.trim().toLowerCase();
|
|
6462
|
+
return CITY_ALIASES[key] ?? text;
|
|
6463
|
+
}
|
|
6464
|
+
function scoreMatch(text, match) {
|
|
6465
|
+
const t = text.trim().toLowerCase();
|
|
6466
|
+
const n = match.name.trim().toLowerCase();
|
|
6467
|
+
if (n === t)
|
|
6468
|
+
return 1;
|
|
6469
|
+
if (n.startsWith(t))
|
|
6470
|
+
return 0.6 + 0.2 * (t.length / n.length);
|
|
6471
|
+
const wantTokens = new Set(t.split(/\s+/));
|
|
6472
|
+
const haveTokens = new Set(n.split(/\s+/));
|
|
6473
|
+
let intersect = 0;
|
|
6474
|
+
for (const w of wantTokens)
|
|
6475
|
+
if (haveTokens.has(w))
|
|
6476
|
+
intersect += 1;
|
|
6477
|
+
const union = (/* @__PURE__ */ new Set([...wantTokens, ...haveTokens])).size;
|
|
6478
|
+
return union > 0 ? intersect / union : 0;
|
|
6479
|
+
}
|
|
6480
|
+
async function resolveLocations(client, texts) {
|
|
6481
|
+
const direct = texts.filter((t) => looksLikeId(t));
|
|
6482
|
+
const free = texts.filter((t) => t && !looksLikeId(t));
|
|
6483
|
+
if (free.length === 0)
|
|
6484
|
+
return { resolved: direct, ambiguities: [] };
|
|
6485
|
+
const resolved = [...direct];
|
|
6486
|
+
const ambiguities = [];
|
|
6487
|
+
for (const originalText of free) {
|
|
6488
|
+
const text = expandAlias(originalText);
|
|
6489
|
+
const path = `/geo/search?q=${encodeURIComponent(text)}`;
|
|
6490
|
+
let response;
|
|
6491
|
+
try {
|
|
6492
|
+
response = await client.request("GET", path);
|
|
6493
|
+
} catch {
|
|
6494
|
+
ambiguities.push({ location_text: originalText, matches: [] });
|
|
6495
|
+
continue;
|
|
6496
|
+
}
|
|
6497
|
+
const results = response.results ?? [];
|
|
6498
|
+
if (results.length === 0) {
|
|
6499
|
+
ambiguities.push({ location_text: originalText, matches: [] });
|
|
6500
|
+
continue;
|
|
6501
|
+
}
|
|
6502
|
+
const levelPreference = (level) => {
|
|
6503
|
+
switch (level) {
|
|
6504
|
+
case 5:
|
|
6505
|
+
return 4;
|
|
6506
|
+
// city proper
|
|
6507
|
+
case 8:
|
|
6508
|
+
return 3;
|
|
6509
|
+
// standard city/town
|
|
6510
|
+
case 6:
|
|
6511
|
+
return 2;
|
|
6512
|
+
// county
|
|
6513
|
+
case 7:
|
|
6514
|
+
return 1;
|
|
6515
|
+
// township-area
|
|
6516
|
+
case 4:
|
|
6517
|
+
return 0;
|
|
6518
|
+
// state/region
|
|
6519
|
+
default:
|
|
6520
|
+
return 0;
|
|
6521
|
+
}
|
|
6522
|
+
};
|
|
6523
|
+
const ranked = results.map((r) => ({
|
|
6524
|
+
id: r.id,
|
|
6525
|
+
name: r.name,
|
|
6526
|
+
country: r.country,
|
|
6527
|
+
level: r.level,
|
|
6528
|
+
score: scoreMatch(text, r)
|
|
6529
|
+
})).sort((a, b) => {
|
|
6530
|
+
if (b.score !== a.score)
|
|
6531
|
+
return b.score - a.score;
|
|
6532
|
+
return levelPreference(b.level) - levelPreference(a.level);
|
|
6533
|
+
});
|
|
6534
|
+
const top = ranked[0];
|
|
6535
|
+
const runnerUp = ranked[1];
|
|
6536
|
+
const isConfident = top.score >= 0.95 || // exact-name match
|
|
6537
|
+
ranked.length === 1 || top.score >= 0.66 && (!runnerUp || top.score - runnerUp.score >= 0.34);
|
|
6538
|
+
if (isConfident) {
|
|
6539
|
+
resolved.push(top.id);
|
|
6540
|
+
} else {
|
|
6541
|
+
ambiguities.push({
|
|
6542
|
+
location_text: originalText,
|
|
6543
|
+
matches: ranked.slice(0, 5)
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
return { resolved, ambiguities };
|
|
6548
|
+
}
|
|
6549
|
+
|
|
5444
6550
|
// ../core/dist/composite/pull-followups.js
|
|
5445
6551
|
function normalizeLinkedinPage4(v) {
|
|
5446
6552
|
if (v == null)
|
|
@@ -5460,6 +6566,23 @@ function augmentContact(c) {
|
|
|
5460
6566
|
linkedin_page: normalizeLinkedinPage4(c.linkedin_page ?? null)
|
|
5461
6567
|
};
|
|
5462
6568
|
}
|
|
6569
|
+
function mergeLocationIds(filter, ids) {
|
|
6570
|
+
const criteria = filter?.criteria ? [...filter.criteria] : [];
|
|
6571
|
+
const idx = criteria.findIndex((c) => c?.type === "location_ids" && c?.is_excluded === false);
|
|
6572
|
+
if (idx >= 0) {
|
|
6573
|
+
const cur = criteria[idx];
|
|
6574
|
+
const existing = Array.isArray(cur.locations) ? cur.locations : [];
|
|
6575
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...existing, ...ids]));
|
|
6576
|
+
criteria[idx] = { ...cur, locations: merged };
|
|
6577
|
+
} else {
|
|
6578
|
+
criteria.push({
|
|
6579
|
+
type: "location_ids",
|
|
6580
|
+
is_excluded: false,
|
|
6581
|
+
locations: ids
|
|
6582
|
+
});
|
|
6583
|
+
}
|
|
6584
|
+
return { criteria };
|
|
6585
|
+
}
|
|
5463
6586
|
var pullFollowups = {
|
|
5464
6587
|
name: "leadbay_pull_followups",
|
|
5465
6588
|
annotations: {
|
|
@@ -5503,6 +6626,14 @@ var pullFollowups = {
|
|
|
5503
6626
|
items: { type: "object" }
|
|
5504
6627
|
}
|
|
5505
6628
|
}
|
|
6629
|
+
},
|
|
6630
|
+
city: {
|
|
6631
|
+
type: "string",
|
|
6632
|
+
description: "Free-text city / region (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). The composite resolves it to an admin_area id via GET /geo/search and merges it into the active Monitor filter as a `location_ids` FilterCriterion. Ambiguous matches surface as `status: 'ambiguous_locations'` with `location_ambiguities[]` \u2014 the agent picks an id and re-calls via `city_id`."
|
|
6633
|
+
},
|
|
6634
|
+
city_id: {
|
|
6635
|
+
type: "string",
|
|
6636
|
+
description: "Pre-resolved admin_area id (numeric string). Use when the user / agent has already picked one of the ambiguity candidates. Bypasses the resolver."
|
|
5506
6637
|
}
|
|
5507
6638
|
},
|
|
5508
6639
|
additionalProperties: false
|
|
@@ -5527,6 +6658,15 @@ var pullFollowups = {
|
|
|
5527
6658
|
type: "number",
|
|
5528
6659
|
description: "Composite-derived count of leads in the page that were excluded because their `pushback_status` is active. The backend may or may not pre-filter; this exposes the count when the composite has to drop them itself."
|
|
5529
6660
|
},
|
|
6661
|
+
status: {
|
|
6662
|
+
type: "string",
|
|
6663
|
+
description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; the agent picks an id from `location_ambiguities` and re-calls with `city_id`. Absent on the happy path."
|
|
6664
|
+
},
|
|
6665
|
+
location_ambiguities: {
|
|
6666
|
+
type: "array",
|
|
6667
|
+
description: "Per ambiguous city: {location_text, matches:[{id, name, country, level, score}]}. Only present when `status === 'ambiguous_locations'`.",
|
|
6668
|
+
items: { type: "object" }
|
|
6669
|
+
},
|
|
5530
6670
|
_meta: {
|
|
5531
6671
|
type: "object",
|
|
5532
6672
|
description: "Operator context: region + last-call latency.",
|
|
@@ -5544,9 +6684,35 @@ var pullFollowups = {
|
|
|
5544
6684
|
const liked = params.liked ?? false;
|
|
5545
6685
|
const page = params.page ?? 0;
|
|
5546
6686
|
const count = Math.min(params.count ?? 20, 200);
|
|
5547
|
-
|
|
6687
|
+
let effectiveSetFilter = params.set_filter;
|
|
6688
|
+
const geoTexts = [];
|
|
6689
|
+
if (params.city)
|
|
6690
|
+
geoTexts.push(params.city);
|
|
6691
|
+
if (params.city_id)
|
|
6692
|
+
geoTexts.push(params.city_id);
|
|
6693
|
+
if (geoTexts.length > 0) {
|
|
6694
|
+
const { resolved, ambiguities } = await resolveLocations(client, geoTexts);
|
|
6695
|
+
if (ambiguities.length > 0) {
|
|
6696
|
+
return {
|
|
6697
|
+
status: "ambiguous_locations",
|
|
6698
|
+
location_ambiguities: ambiguities,
|
|
6699
|
+
leads: [],
|
|
6700
|
+
active_filters: null,
|
|
6701
|
+
pagination: null,
|
|
6702
|
+
total_excluded_by_pushback: 0,
|
|
6703
|
+
_meta: {
|
|
6704
|
+
region: client.region,
|
|
6705
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
6706
|
+
}
|
|
6707
|
+
};
|
|
6708
|
+
}
|
|
6709
|
+
if (resolved.length > 0) {
|
|
6710
|
+
effectiveSetFilter = mergeLocationIds(effectiveSetFilter, resolved);
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
if (effectiveSetFilter) {
|
|
5548
6714
|
try {
|
|
5549
|
-
await client.requestVoid("POST", "/monitor/filter",
|
|
6715
|
+
await client.requestVoid("POST", "/monitor/filter", effectiveSetFilter);
|
|
5550
6716
|
} catch (err) {
|
|
5551
6717
|
ctx?.logger?.warn?.(`pull_followups: POST /monitor/filter failed: ${err?.message ?? err?.code ?? err}`);
|
|
5552
6718
|
}
|
|
@@ -5606,7 +6772,28 @@ var pullFollowups = {
|
|
|
5606
6772
|
}
|
|
5607
6773
|
};
|
|
5608
6774
|
|
|
5609
|
-
// ../core/dist/composite/
|
|
6775
|
+
// ../core/dist/composite/followups-map.js
|
|
6776
|
+
var followupsMap = {
|
|
6777
|
+
name: "leadbay_followups_map",
|
|
6778
|
+
// NO ui binding — see the file-level comment above. The agent
|
|
6779
|
+
// routes into host-native widgets via the description's RENDER
|
|
6780
|
+
// directives instead of an auto-rendered MCP Apps widget.
|
|
6781
|
+
annotations: {
|
|
6782
|
+
title: "Plot follow-up leads on a map (travel / in-person intent)",
|
|
6783
|
+
readOnlyHint: true,
|
|
6784
|
+
destructiveHint: false,
|
|
6785
|
+
idempotentHint: true,
|
|
6786
|
+
openWorldHint: true
|
|
6787
|
+
},
|
|
6788
|
+
description: leadbay_followups_map,
|
|
6789
|
+
// Delegate everything else verbatim — same params, same output, same
|
|
6790
|
+
// city resolver / set_filter / pushback exclusion behavior.
|
|
6791
|
+
inputSchema: pullFollowups.inputSchema,
|
|
6792
|
+
outputSchema: pullFollowups.outputSchema,
|
|
6793
|
+
execute: pullFollowups.execute
|
|
6794
|
+
};
|
|
6795
|
+
|
|
6796
|
+
// ../core/dist/composite/research-lead-by-id.js
|
|
5610
6797
|
function normalizeLinkedinPage5(v) {
|
|
5611
6798
|
if (v == null)
|
|
5612
6799
|
return null;
|
|
@@ -5661,18 +6848,33 @@ ${firm.short_description}`);
|
|
|
5661
6848
|
}
|
|
5662
6849
|
}
|
|
5663
6850
|
const contacts = shape.contacts ?? {};
|
|
5664
|
-
const
|
|
5665
|
-
if (
|
|
6851
|
+
const reachable = Array.isArray(contacts.reachable) ? contacts.reachable : [];
|
|
6852
|
+
if (reachable.length > 0) {
|
|
5666
6853
|
out.push(`
|
|
5667
|
-
## Contacts
|
|
5668
|
-
for (const c of
|
|
6854
|
+
## Contacts \u2014 reachable now`);
|
|
6855
|
+
for (const c of reachable.slice(0, 10)) {
|
|
5669
6856
|
const fn = c.first_name ?? "";
|
|
5670
6857
|
const ln = c.last_name ?? "";
|
|
5671
6858
|
const title = c.job_title ?? "\u2014";
|
|
5672
|
-
const
|
|
5673
|
-
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}** \u2014 ${title} \xB7 ${
|
|
6859
|
+
const channel = c.email ?? c.phone_number ?? "\u2014";
|
|
6860
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}** \u2014 ${title} \xB7 ${channel}`);
|
|
5674
6861
|
}
|
|
5675
6862
|
}
|
|
6863
|
+
const candidates = Array.isArray(contacts.candidates) ? contacts.candidates : [];
|
|
6864
|
+
if (candidates.length > 0) {
|
|
6865
|
+
out.push(`
|
|
6866
|
+
## Contacts \u2014 candidates (need enrichment)`);
|
|
6867
|
+
for (const c of candidates.slice(0, 10)) {
|
|
6868
|
+
const fn = c.first_name ?? "";
|
|
6869
|
+
const ln = c.last_name ?? "";
|
|
6870
|
+
const title = c.job_title ?? "\u2014";
|
|
6871
|
+
const li = c.linkedin_page ? `LinkedIn` : "no LinkedIn";
|
|
6872
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}** \u2014 ${title} \xB7 ${li}`);
|
|
6873
|
+
}
|
|
6874
|
+
if (candidates.length > 10)
|
|
6875
|
+
out.push(`- _${candidates.length - 10} more \u2026_`);
|
|
6876
|
+
}
|
|
6877
|
+
const recentActivities = Array.isArray(shape.recent_activities) ? shape.recent_activities : [];
|
|
5676
6878
|
const engagement = shape.engagement ?? {};
|
|
5677
6879
|
const counts = [
|
|
5678
6880
|
["notes", engagement.notes_count],
|
|
@@ -5680,7 +6882,7 @@ ${firm.short_description}`);
|
|
|
5680
6882
|
["prospecting", engagement.prospecting_actions_count]
|
|
5681
6883
|
];
|
|
5682
6884
|
const activeCounts = counts.filter(([, v]) => typeof v === "number" && v > 0);
|
|
5683
|
-
if (activeCounts.length > 0 || engagement.liked || engagement.disliked) {
|
|
6885
|
+
if (activeCounts.length > 0 || engagement.liked || engagement.disliked || recentActivities.length > 0) {
|
|
5684
6886
|
out.push(`
|
|
5685
6887
|
## Engagement`);
|
|
5686
6888
|
if (engagement.liked)
|
|
@@ -5690,6 +6892,15 @@ ${firm.short_description}`);
|
|
|
5690
6892
|
for (const [k, v] of activeCounts) {
|
|
5691
6893
|
out.push(`- ${k}: ${v}`);
|
|
5692
6894
|
}
|
|
6895
|
+
if (recentActivities.length > 0) {
|
|
6896
|
+
out.push(`
|
|
6897
|
+
### Recent activity`);
|
|
6898
|
+
for (const a of recentActivities.slice(0, 10)) {
|
|
6899
|
+
const type = a.type ?? "?";
|
|
6900
|
+
const date = a.date ?? "";
|
|
6901
|
+
out.push(`- ${type} \xB7 ${date}`);
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
5693
6904
|
}
|
|
5694
6905
|
if (shape.truncated) {
|
|
5695
6906
|
out.push(`
|
|
@@ -5729,16 +6940,23 @@ function reshapeWebFetchContent(content) {
|
|
|
5729
6940
|
});
|
|
5730
6941
|
return sections;
|
|
5731
6942
|
}
|
|
5732
|
-
|
|
5733
|
-
|
|
6943
|
+
function isReachable(c) {
|
|
6944
|
+
if (!c)
|
|
6945
|
+
return false;
|
|
6946
|
+
const email = typeof c.email === "string" ? c.email.trim() : "";
|
|
6947
|
+
const phone = typeof c.phone_number === "string" ? c.phone_number.trim() : "";
|
|
6948
|
+
return email.length > 0 || phone.length > 0;
|
|
6949
|
+
}
|
|
6950
|
+
var researchLeadById = {
|
|
6951
|
+
name: "leadbay_research_lead_by_id",
|
|
5734
6952
|
annotations: {
|
|
5735
|
-
title: "Research a Leadbay lead in depth",
|
|
6953
|
+
title: "Research a Leadbay lead in depth (by UUID)",
|
|
5736
6954
|
readOnlyHint: true,
|
|
5737
6955
|
destructiveHint: false,
|
|
5738
6956
|
idempotentHint: true,
|
|
5739
6957
|
openWorldHint: true
|
|
5740
6958
|
},
|
|
5741
|
-
description:
|
|
6959
|
+
description: leadbay_research_lead_by_id,
|
|
5742
6960
|
inputSchema: {
|
|
5743
6961
|
type: "object",
|
|
5744
6962
|
properties: {
|
|
@@ -5889,16 +7107,31 @@ var researchLead = {
|
|
|
5889
7107
|
},
|
|
5890
7108
|
contacts: {
|
|
5891
7109
|
type: "object",
|
|
5892
|
-
description: "Two-tier contact set
|
|
7110
|
+
description: "Two-tier contact set, partitioned by reachability \u2014 agent-friendly framing of the backend's paid-vs-org split. `reachable`: contacts with an email or phone right now (org-directory entries that ship with channels, PLUS paid contacts whose enrichment has completed). The agent can message these without buying enrichment. `candidates`: paid-contact entries WITHOUT resolved channels yet \u2014 typically LinkedIn URL only, `enrichment_done: false`. The agent must call leadbay_enrich_titles (or leadbay_prepare_outreach with enrich:true) before these become messagable.",
|
|
5893
7111
|
properties: {
|
|
5894
|
-
|
|
5895
|
-
|
|
7112
|
+
reachable: { type: "array", items: { type: "object" } },
|
|
7113
|
+
candidates: { type: "array", items: { type: "object" } }
|
|
5896
7114
|
},
|
|
5897
7115
|
additionalProperties: false
|
|
5898
7116
|
},
|
|
7117
|
+
recent_activities: {
|
|
7118
|
+
type: "array",
|
|
7119
|
+
description: "Unified activity timeline (top 20 most recent) from /leads/{id}/activities. Each entry: {type, date}. Replaces the per-category recent_notes/recent_epilogue/recent_prospecting arrays from the prior schema \u2014 counts stay on `engagement` as the cheap signal.",
|
|
7120
|
+
items: {
|
|
7121
|
+
type: "object",
|
|
7122
|
+
properties: {
|
|
7123
|
+
type: { type: "string" },
|
|
7124
|
+
date: { type: "string" }
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
},
|
|
7128
|
+
web_insights_fetched_at: {
|
|
7129
|
+
type: ["string", "null"],
|
|
7130
|
+
description: "ISO timestamp of the latest /web_fetch run. Use as a staleness signal \u2014 if older than 30 days, offer to refresh."
|
|
7131
|
+
},
|
|
5899
7132
|
engagement: {
|
|
5900
7133
|
type: "object",
|
|
5901
|
-
description: "What humans/prior agent runs already did: liked/disliked flags, recommended_contact, counts (notes/epilogue/prospecting)
|
|
7134
|
+
description: "What humans/prior agent runs already did: liked/disliked flags, recommended_contact, and counts (notes/epilogue/prospecting). Recent items live in `recent_activities` at the top level.",
|
|
5902
7135
|
properties: {
|
|
5903
7136
|
liked: { type: "boolean" },
|
|
5904
7137
|
disliked: { type: "boolean" },
|
|
@@ -5906,27 +7139,38 @@ var researchLead = {
|
|
|
5906
7139
|
recommended_contact: { type: ["object", "null"] },
|
|
5907
7140
|
notes_count: { type: "number" },
|
|
5908
7141
|
epilogue_actions_count: { type: "number" },
|
|
5909
|
-
prospecting_actions_count: { type: "number" }
|
|
5910
|
-
recent_notes: { type: "array", items: { type: "object" } },
|
|
5911
|
-
recent_epilogue: { type: "array", items: { type: "object" } },
|
|
5912
|
-
recent_prospecting: { type: "array", items: { type: "object" } }
|
|
7142
|
+
prospecting_actions_count: { type: "number" }
|
|
5913
7143
|
},
|
|
5914
7144
|
additionalProperties: false
|
|
5915
7145
|
},
|
|
5916
7146
|
_meta: {
|
|
5917
7147
|
type: "object",
|
|
5918
|
-
description: "Operator context: region (us/fr/custom), lens_id (the lens used for the lead-by-id fetch), web_fetch_in_progress (true if the backend is still hydrating signals).",
|
|
7148
|
+
description: "Operator context: region (us/fr/custom), lens_id (the lens used for the lead-by-id fetch), web_fetch_in_progress (true if the backend is still hydrating signals), has_reachable_contact (true if at least one contact or recommended_contact has email or phone \u2014 drives NEXT STEPS routing between enrichment vs outreach). When the call was routed via leadbay_research_lead_by_name_fuzzy, also: resolved_from='companyName', resolved_query='<needle>', match_candidates=[{leadId,name,score}].",
|
|
5919
7149
|
properties: {
|
|
5920
7150
|
region: { type: "string" },
|
|
5921
7151
|
lens_id: { type: "number" },
|
|
5922
|
-
web_fetch_in_progress: { type: "boolean" }
|
|
7152
|
+
web_fetch_in_progress: { type: "boolean" },
|
|
7153
|
+
has_reachable_contact: { type: "boolean" },
|
|
7154
|
+
resolved_from: { type: ["string", "null"] },
|
|
7155
|
+
resolved_query: { type: ["string", "null"] },
|
|
7156
|
+
match_candidates: {
|
|
7157
|
+
type: ["array", "null"],
|
|
7158
|
+
items: { type: "object" }
|
|
7159
|
+
}
|
|
5923
7160
|
},
|
|
5924
7161
|
additionalProperties: false
|
|
5925
7162
|
}
|
|
5926
7163
|
},
|
|
5927
|
-
required: [
|
|
7164
|
+
required: [
|
|
7165
|
+
"qualification",
|
|
7166
|
+
"signals",
|
|
7167
|
+
"firmographics",
|
|
7168
|
+
"contacts",
|
|
7169
|
+
"engagement",
|
|
7170
|
+
"recent_activities"
|
|
7171
|
+
]
|
|
5928
7172
|
},
|
|
5929
|
-
execute: async (client, params,
|
|
7173
|
+
execute: async (client, params, _ctx) => {
|
|
5930
7174
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
5931
7175
|
const leadId = params.leadId;
|
|
5932
7176
|
void client.request("POST", "/interactions", [
|
|
@@ -5934,28 +7178,18 @@ var researchLead = {
|
|
|
5934
7178
|
{ type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
|
|
5935
7179
|
]).catch(() => {
|
|
5936
7180
|
});
|
|
5937
|
-
const [profileR, qualR, contactsR, webFetchR] = await Promise.allSettled([
|
|
7181
|
+
const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
|
|
5938
7182
|
client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
|
|
5939
7183
|
client.request("GET", `/leads/${leadId}/ai_agent_responses`),
|
|
5940
7184
|
client.request("GET", `/leads/${leadId}/enrich/contacts?IncludeEnriched=true`),
|
|
5941
|
-
client.request("GET", `/leads/${leadId}/web_fetch`)
|
|
7185
|
+
client.request("GET", `/leads/${leadId}/web_fetch`),
|
|
7186
|
+
client.request("GET", `/leads/${leadId}/activities?count=20`),
|
|
7187
|
+
client.request("GET", `/leads/${leadId}/contacts?IncludeEnriched=true`)
|
|
5942
7188
|
]);
|
|
5943
7189
|
if (profileR.status === "rejected") {
|
|
5944
7190
|
throw profileR.reason;
|
|
5945
7191
|
}
|
|
5946
7192
|
const lead = profileR.value;
|
|
5947
|
-
const wantNotes = (lead.notes_count ?? 0) > 0;
|
|
5948
|
-
const wantEpilogue = (lead.epilogue_actions_count ?? 0) > 0;
|
|
5949
|
-
const wantProspecting = (lead.prospecting_actions_count ?? 0) > 0;
|
|
5950
|
-
const wantOrgContacts = (lead.org_contacts_count ?? 0) > 0;
|
|
5951
|
-
const engagementFetches = await Promise.allSettled([
|
|
5952
|
-
wantNotes ? client.request("GET", `/leads/${leadId}/notes`) : Promise.resolve(null),
|
|
5953
|
-
wantEpilogue ? client.request("GET", `/leads/${leadId}/epilogue_responses?count=10&page=0`) : Promise.resolve(null),
|
|
5954
|
-
wantProspecting ? client.request("GET", `/leads/${leadId}/prospecting_actions?count=10&page=0`) : Promise.resolve(null),
|
|
5955
|
-
wantOrgContacts ? client.request("GET", `/leads/${leadId}/contacts?IncludeEnriched=true`) : Promise.resolve(null)
|
|
5956
|
-
]);
|
|
5957
|
-
const [notesR, epilogueR, prospR, orgContactsR] = engagementFetches;
|
|
5958
|
-
const valOrNull = (r) => r.status === "fulfilled" ? r.value ?? null : null;
|
|
5959
7193
|
let signals = reshapeWebFetchContent(webFetchR.status === "fulfilled" ? webFetchR.value?.content ?? null : null);
|
|
5960
7194
|
if (params.concise) {
|
|
5961
7195
|
signals = signals.map((s) => ({
|
|
@@ -5964,7 +7198,42 @@ var researchLead = {
|
|
|
5964
7198
|
})).filter((s) => s.entries.length > 0);
|
|
5965
7199
|
}
|
|
5966
7200
|
const paidContacts = contactsR.status === "fulfilled" ? contactsR.value : [];
|
|
5967
|
-
const orgContacts =
|
|
7201
|
+
const orgContacts = orgContactsR.status === "fulfilled" ? orgContactsR.value : [];
|
|
7202
|
+
const shapePaid = (c) => ({
|
|
7203
|
+
id: c.id,
|
|
7204
|
+
first_name: c.first_name,
|
|
7205
|
+
last_name: c.last_name,
|
|
7206
|
+
job_title: c.job_title,
|
|
7207
|
+
email: c.email,
|
|
7208
|
+
phone_number: c.phone_number,
|
|
7209
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page),
|
|
7210
|
+
recommended: c.recommended,
|
|
7211
|
+
enrichment_done: c.enrichment?.done ?? false,
|
|
7212
|
+
source: "paid"
|
|
7213
|
+
});
|
|
7214
|
+
const shapeOrg = (c) => ({
|
|
7215
|
+
id: c.id,
|
|
7216
|
+
first_name: c.first_name,
|
|
7217
|
+
last_name: c.last_name,
|
|
7218
|
+
job_title: c.job_title,
|
|
7219
|
+
email: c.email,
|
|
7220
|
+
phone_number: c.phone_number ?? null,
|
|
7221
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null),
|
|
7222
|
+
recommended: c.recommended,
|
|
7223
|
+
enrichment_done: true,
|
|
7224
|
+
source: "org"
|
|
7225
|
+
});
|
|
7226
|
+
const allContacts = [
|
|
7227
|
+
...paidContacts.map(shapePaid),
|
|
7228
|
+
...orgContacts.map(shapeOrg)
|
|
7229
|
+
];
|
|
7230
|
+
const reachableContacts = allContacts.filter((c) => isReachable(c));
|
|
7231
|
+
const candidateContacts = allContacts.filter((c) => !isReachable(c));
|
|
7232
|
+
const recommendedContact = lead.recommended_contact ? {
|
|
7233
|
+
...lead.recommended_contact,
|
|
7234
|
+
linkedin_page: normalizeLinkedinPage5(lead.recommended_contact.linkedin_page ?? null)
|
|
7235
|
+
} : null;
|
|
7236
|
+
const hasReachableContact = reachableContacts.length > 0 || isReachable(recommendedContact);
|
|
5968
7237
|
const TRUNCATE_CHAR_BUDGET = 25e3;
|
|
5969
7238
|
let truncated = false;
|
|
5970
7239
|
let truncationHint = null;
|
|
@@ -5987,14 +7256,10 @@ var researchLead = {
|
|
|
5987
7256
|
}));
|
|
5988
7257
|
}
|
|
5989
7258
|
}
|
|
7259
|
+
const recentActivities = activitiesR.status === "fulfilled" ? activitiesR.value.items.slice(0, 20).map((a) => ({ type: a.type, date: a.date })) : [];
|
|
7260
|
+
const webFetchFetchedAt = webFetchR.status === "fulfilled" ? webFetchR.value?.fetch_at ?? null : null;
|
|
5990
7261
|
return {
|
|
5991
|
-
// 1) qualification
|
|
5992
|
-
// boost_score is the canonical field (per AiAgentResponse.score). The valid
|
|
5993
|
-
// set is the discrete -10/0/10/20 boost (see types.ts comment), NOT a 0-10
|
|
5994
|
-
// average — the eval doc flagged the old `score_0_to_10` field name as
|
|
5995
|
-
// misleading. We now ship `boost_score` as canonical alongside an explicit
|
|
5996
|
-
// `score_scale` annotation; `score_0_to_10` is kept as a deprecated alias
|
|
5997
|
-
// for one minor version (0.6.x) and removed in 0.7.0. See MIGRATION.md.
|
|
7262
|
+
// 1) qualification
|
|
5998
7263
|
qualification: qualR.status === "fulfilled" ? qualR.value.map((r) => ({
|
|
5999
7264
|
question: r.question,
|
|
6000
7265
|
boost_score: r.score,
|
|
@@ -6005,7 +7270,7 @@ var researchLead = {
|
|
|
6005
7270
|
response: r.response,
|
|
6006
7271
|
computed_at: r.computed_at
|
|
6007
7272
|
})) : [],
|
|
6008
|
-
// 2) signals
|
|
7273
|
+
// 2) signals
|
|
6009
7274
|
signals: signalsForReturn,
|
|
6010
7275
|
truncated,
|
|
6011
7276
|
truncation_hint: truncationHint,
|
|
@@ -6030,59 +7295,40 @@ var researchLead = {
|
|
|
6030
7295
|
social_urls: lead.social_urls ?? null,
|
|
6031
7296
|
registry_ids: lead.registry_ids ?? null
|
|
6032
7297
|
},
|
|
6033
|
-
// 4) contacts
|
|
6034
|
-
// B6: defensively coerce the literal string "null" to a real null —
|
|
6035
|
-
// some backend serializers emit it for un-enriched contacts.
|
|
7298
|
+
// 4) contacts — partitioned by reachability (not by source endpoint)
|
|
6036
7299
|
contacts: {
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
enrichment_done: c.enrichment?.done ?? false
|
|
6047
|
-
})),
|
|
6048
|
-
org: orgContacts.map((c) => ({
|
|
6049
|
-
id: c.id,
|
|
6050
|
-
first_name: c.first_name,
|
|
6051
|
-
last_name: c.last_name,
|
|
6052
|
-
job_title: c.job_title,
|
|
6053
|
-
email: c.email,
|
|
6054
|
-
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null)
|
|
6055
|
-
}))
|
|
6056
|
-
},
|
|
6057
|
-
// 5) engagement — what humans/prior agent runs already did.
|
|
6058
|
-
// B8: `recommended_contact_title` dropped — it duplicates
|
|
6059
|
-
// `recommended_contact.job_title` and just confused agents.
|
|
7300
|
+
reachable: reachableContacts,
|
|
7301
|
+
candidates: candidateContacts
|
|
7302
|
+
},
|
|
7303
|
+
// 5) unified recent activity timeline
|
|
7304
|
+
recent_activities: recentActivities,
|
|
7305
|
+
// 6) staleness signal (for "refresh research" NEXT STEPS row)
|
|
7306
|
+
web_insights_fetched_at: webFetchFetchedAt,
|
|
7307
|
+
// 7) engagement — counts + curation flags only; recent items live in
|
|
7308
|
+
// recent_activities[].
|
|
6060
7309
|
engagement: {
|
|
6061
7310
|
liked: lead.liked,
|
|
6062
7311
|
disliked: lead.disliked,
|
|
6063
7312
|
new: lead.new ?? false,
|
|
6064
|
-
recommended_contact:
|
|
6065
|
-
...lead.recommended_contact,
|
|
6066
|
-
// B1+B7: propagate linkedin_page when the backend includes it.
|
|
6067
|
-
linkedin_page: normalizeLinkedinPage5(lead.recommended_contact.linkedin_page ?? null)
|
|
6068
|
-
} : null,
|
|
7313
|
+
recommended_contact: recommendedContact,
|
|
6069
7314
|
notes_count: lead.notes_count ?? 0,
|
|
6070
7315
|
epilogue_actions_count: lead.epilogue_actions_count ?? 0,
|
|
6071
|
-
prospecting_actions_count: lead.prospecting_actions_count ?? 0
|
|
6072
|
-
recent_notes: valOrNull(notesR)?.slice(0, 3) ?? [],
|
|
6073
|
-
recent_epilogue: valOrNull(epilogueR)?.items?.slice(0, 3) ?? [],
|
|
6074
|
-
recent_prospecting: valOrNull(prospR)?.items?.slice(0, 5) ?? []
|
|
7316
|
+
prospecting_actions_count: lead.prospecting_actions_count ?? 0
|
|
6075
7317
|
},
|
|
6076
7318
|
_meta: {
|
|
6077
7319
|
region: client.region,
|
|
6078
7320
|
lens_id: lensId,
|
|
6079
|
-
web_fetch_in_progress: webFetchR.status === "fulfilled" ? webFetchR.value?.in_progress : false
|
|
7321
|
+
web_fetch_in_progress: webFetchR.status === "fulfilled" ? webFetchR.value?.in_progress : false,
|
|
7322
|
+
has_reachable_contact: hasReachableContact,
|
|
7323
|
+
resolved_from: params._resolved?.from ?? null,
|
|
7324
|
+
resolved_query: params._resolved?.query ?? null,
|
|
7325
|
+
match_candidates: params._resolved?.candidates ?? null
|
|
6080
7326
|
}
|
|
6081
7327
|
};
|
|
6082
7328
|
}
|
|
6083
7329
|
};
|
|
6084
|
-
var _innerExecute =
|
|
6085
|
-
|
|
7330
|
+
var _innerExecute = researchLeadById.execute;
|
|
7331
|
+
researchLeadById.execute = async (client, params, ctx) => {
|
|
6086
7332
|
const result = await _innerExecute(client, params, ctx);
|
|
6087
7333
|
if (params.response_format !== "markdown")
|
|
6088
7334
|
return result;
|
|
@@ -6097,6 +7343,92 @@ researchLead.execute = async (client, params, ctx) => {
|
|
|
6097
7343
|
return envelope;
|
|
6098
7344
|
};
|
|
6099
7345
|
|
|
7346
|
+
// ../core/dist/composite/research-lead-by-name-fuzzy.js
|
|
7347
|
+
function rankSubstringMatches(needle, candidates) {
|
|
7348
|
+
const n = needle.toLowerCase();
|
|
7349
|
+
const hits = candidates.filter((c) => typeof c.name === "string" && c.name.toLowerCase().includes(n));
|
|
7350
|
+
hits.sort((a, b) => {
|
|
7351
|
+
const aScore = a.score ?? -Infinity;
|
|
7352
|
+
const bScore = b.score ?? -Infinity;
|
|
7353
|
+
return bScore - aScore;
|
|
7354
|
+
});
|
|
7355
|
+
return hits;
|
|
7356
|
+
}
|
|
7357
|
+
var researchLeadByNameFuzzy = {
|
|
7358
|
+
name: "leadbay_research_lead_by_name_fuzzy",
|
|
7359
|
+
annotations: {
|
|
7360
|
+
title: "Look up a lead by company name (fuzzy)",
|
|
7361
|
+
readOnlyHint: true,
|
|
7362
|
+
destructiveHint: false,
|
|
7363
|
+
idempotentHint: true,
|
|
7364
|
+
openWorldHint: true
|
|
7365
|
+
},
|
|
7366
|
+
description: leadbay_research_lead_by_name_fuzzy,
|
|
7367
|
+
inputSchema: {
|
|
7368
|
+
type: "object",
|
|
7369
|
+
properties: {
|
|
7370
|
+
companyName: {
|
|
7371
|
+
type: "string",
|
|
7372
|
+
description: "Company name to look up. Substring fuzzy-match against the top 50 of the active lens's wishlist; ties broken by descending lead score."
|
|
7373
|
+
},
|
|
7374
|
+
lensId: {
|
|
7375
|
+
type: "number",
|
|
7376
|
+
description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
|
|
7377
|
+
},
|
|
7378
|
+
concise: {
|
|
7379
|
+
type: "boolean",
|
|
7380
|
+
description: "Forwarded to leadbay_research_lead_by_id. If true, trims signals to hot=true items only."
|
|
7381
|
+
},
|
|
7382
|
+
response_format: {
|
|
7383
|
+
type: "string",
|
|
7384
|
+
enum: ["json", "markdown"],
|
|
7385
|
+
description: "Forwarded to leadbay_research_lead_by_id. Default 'json'."
|
|
7386
|
+
}
|
|
7387
|
+
},
|
|
7388
|
+
required: ["companyName"],
|
|
7389
|
+
additionalProperties: false
|
|
7390
|
+
},
|
|
7391
|
+
// Output shape matches leadbay_research_lead_by_id; the only additions are
|
|
7392
|
+
// _meta.resolved_from / resolved_query / match_candidates which are
|
|
7393
|
+
// documented on _by_id's output schema. Defer to _by_id for the schema —
|
|
7394
|
+
// duplicating it would just rot.
|
|
7395
|
+
outputSchema: {
|
|
7396
|
+
type: "object",
|
|
7397
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName', _meta.resolved_query='<needle>', and _meta.match_candidates=[{leadId,name,score}] populated.",
|
|
7398
|
+
additionalProperties: true
|
|
7399
|
+
},
|
|
7400
|
+
execute: async (client, params, ctx) => {
|
|
7401
|
+
if (!params.companyName || typeof params.companyName !== "string") {
|
|
7402
|
+
throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as a string. If you already have the lead UUID, call leadbay_research_lead_by_id directly.");
|
|
7403
|
+
}
|
|
7404
|
+
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
7405
|
+
const results = await discoverLeads.execute(client, { lensId, count: 50, page: 0 }, ctx);
|
|
7406
|
+
const allLeads = results.leads;
|
|
7407
|
+
const ranked = rankSubstringMatches(params.companyName, allLeads);
|
|
7408
|
+
if (ranked.length === 0) {
|
|
7409
|
+
const nearestNames = [...allLeads].sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity)).slice(0, 5).map((l) => ({ leadId: l.id, name: l.name, score: l.score }));
|
|
7410
|
+
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${params.companyName}" in the current lens (top-50 wishlist)`, `Call leadbay_pull_leads to see what's available. Top-scoring leads currently in this lens: ${nearestNames.map((n) => n.name).join(", ")}.`);
|
|
7411
|
+
}
|
|
7412
|
+
const [primary, ...rest] = ranked;
|
|
7413
|
+
const candidates = rest.slice(0, 4).map((m) => ({
|
|
7414
|
+
leadId: m.id,
|
|
7415
|
+
name: m.name,
|
|
7416
|
+
score: m.score
|
|
7417
|
+
}));
|
|
7418
|
+
return await researchLeadById.execute(client, {
|
|
7419
|
+
leadId: primary.id,
|
|
7420
|
+
lensId,
|
|
7421
|
+
concise: params.concise,
|
|
7422
|
+
response_format: params.response_format,
|
|
7423
|
+
_resolved: {
|
|
7424
|
+
from: "companyName",
|
|
7425
|
+
query: params.companyName,
|
|
7426
|
+
candidates
|
|
7427
|
+
}
|
|
7428
|
+
}, ctx);
|
|
7429
|
+
}
|
|
7430
|
+
};
|
|
7431
|
+
|
|
6100
7432
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
6101
7433
|
var recallOrderedTitles = {
|
|
6102
7434
|
name: "leadbay_recall_ordered_titles",
|
|
@@ -8929,7 +10261,7 @@ var enrichTitles = {
|
|
|
8929
10261
|
launched_at: bulkRecord?.launched_at,
|
|
8930
10262
|
durability: bulkRecord?.durability,
|
|
8931
10263
|
message: bulkRecord ? "Enrichment job launched. Backend has no server-side bulk_id yet; MCP minted a client-side bulk_id (persisted to disk by default) so you can poll via leadbay_bulk_enrich_status." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
|
|
8932
|
-
next_action: bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call
|
|
10264
|
+
next_action: bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead_by_id or leadbay_get_contacts on the leads you care about."
|
|
8933
10265
|
};
|
|
8934
10266
|
} finally {
|
|
8935
10267
|
try {
|
|
@@ -9211,9 +10543,9 @@ function bestMatches(text, taxonomy) {
|
|
|
9211
10543
|
return ranked.slice(0, 5);
|
|
9212
10544
|
}
|
|
9213
10545
|
async function resolveSectors(client, texts) {
|
|
9214
|
-
const
|
|
9215
|
-
const direct = texts.filter(
|
|
9216
|
-
const free = texts.filter((s) => !
|
|
10546
|
+
const looksLikeId2 = (s) => /^\d+$/.test(s);
|
|
10547
|
+
const direct = texts.filter(looksLikeId2);
|
|
10548
|
+
const free = texts.filter((s) => !looksLikeId2(s));
|
|
9217
10549
|
if (free.length === 0)
|
|
9218
10550
|
return { resolved: direct, ambiguities: [] };
|
|
9219
10551
|
const me = await client.resolveMe().catch(() => null);
|
|
@@ -10032,6 +11364,7 @@ var granularReadTools = [
|
|
|
10032
11364
|
getLensFilter,
|
|
10033
11365
|
getLensScoring,
|
|
10034
11366
|
listSectors,
|
|
11367
|
+
listLocations,
|
|
10035
11368
|
getUserPrompt,
|
|
10036
11369
|
getClarification,
|
|
10037
11370
|
getLeadNotes,
|
|
@@ -10040,7 +11373,9 @@ var granularReadTools = [
|
|
|
10040
11373
|
getWebFetch,
|
|
10041
11374
|
getSelectionIds,
|
|
10042
11375
|
getEnrichmentJobTitles,
|
|
10043
|
-
listMappableFields
|
|
11376
|
+
listMappableFields,
|
|
11377
|
+
createTopupLink,
|
|
11378
|
+
openBillingPortal
|
|
10044
11379
|
];
|
|
10045
11380
|
var granularWriteTools = [
|
|
10046
11381
|
qualifyLead,
|
|
@@ -10065,7 +11400,9 @@ var granularWriteTools = [
|
|
|
10065
11400
|
removePushback,
|
|
10066
11401
|
previewBulkEnrichment,
|
|
10067
11402
|
launchBulkEnrichment,
|
|
10068
|
-
createCustomField
|
|
11403
|
+
createCustomField,
|
|
11404
|
+
likeLead,
|
|
11405
|
+
dislikeLead
|
|
10069
11406
|
];
|
|
10070
11407
|
var granularTools = [
|
|
10071
11408
|
login,
|
|
@@ -10078,7 +11415,9 @@ granularTools.forEach((t) => {
|
|
|
10078
11415
|
var compositeReadTools = [
|
|
10079
11416
|
pullLeads,
|
|
10080
11417
|
pullFollowups,
|
|
10081
|
-
|
|
11418
|
+
followupsMap,
|
|
11419
|
+
researchLeadById,
|
|
11420
|
+
researchLeadByNameFuzzy,
|
|
10082
11421
|
recallOrderedTitles,
|
|
10083
11422
|
accountStatus,
|
|
10084
11423
|
bulkEnrichStatus,
|
|
@@ -10089,8 +11428,14 @@ var compositeReadTools = [
|
|
|
10089
11428
|
// it for discoverability; expose it always-on so agents can find custom fields
|
|
10090
11429
|
// without needing LEADBAY_MCP_ADVANCED=1.
|
|
10091
11430
|
listMappableFields,
|
|
10092
|
-
//
|
|
10093
|
-
|
|
11431
|
+
// Billing / top-up tools — granular-shaped but ALWAYS exposed because
|
|
11432
|
+
// they're the canonical recovery path from a QUOTA_EXCEEDED wall. If
|
|
11433
|
+
// they were gated behind LEADBAY_MCP_ADVANCED=1 the agent would
|
|
11434
|
+
// know about the wall but not the door out. Read-only from the
|
|
11435
|
+
// agent's POV (creating a Stripe session URL doesn't charge anyone;
|
|
11436
|
+
// the user pays in their browser).
|
|
11437
|
+
createTopupLink,
|
|
11438
|
+
openBillingPortal,
|
|
10094
11439
|
prepareOutreach
|
|
10095
11440
|
];
|
|
10096
11441
|
var compositeWriteTools = [
|
|
@@ -10107,7 +11452,11 @@ var compositeWriteTools = [
|
|
|
10107
11452
|
createCustomField,
|
|
10108
11453
|
// addNote is granular-shaped but file-import prompts depend on it to preserve
|
|
10109
11454
|
// meaningful source-file notes after imports return lead ids.
|
|
10110
|
-
addNote
|
|
11455
|
+
addNote,
|
|
11456
|
+
// likeLead/dislikeLead are granular-shaped but should always be available
|
|
11457
|
+
// to the agent without requiring LEADBAY_MCP_ADVANCED=1.
|
|
11458
|
+
likeLead,
|
|
11459
|
+
dislikeLead
|
|
10111
11460
|
];
|
|
10112
11461
|
var compositeTools = [
|
|
10113
11462
|
...compositeReadTools,
|
|
@@ -10137,6 +11486,7 @@ export {
|
|
|
10137
11486
|
getLensFilter,
|
|
10138
11487
|
getLensScoring,
|
|
10139
11488
|
listSectors,
|
|
11489
|
+
listLocations,
|
|
10140
11490
|
getUserPrompt,
|
|
10141
11491
|
getClarification,
|
|
10142
11492
|
getLeadNotes,
|
|
@@ -10147,6 +11497,8 @@ export {
|
|
|
10147
11497
|
getEnrichmentJobTitles,
|
|
10148
11498
|
importLeads,
|
|
10149
11499
|
listMappableFields,
|
|
11500
|
+
createTopupLink,
|
|
11501
|
+
openBillingPortal,
|
|
10150
11502
|
selectLeads,
|
|
10151
11503
|
deselectLeads,
|
|
10152
11504
|
clearSelection,
|
|
@@ -10167,11 +11519,14 @@ export {
|
|
|
10167
11519
|
previewBulkEnrichment,
|
|
10168
11520
|
launchBulkEnrichment,
|
|
10169
11521
|
createCustomField,
|
|
10170
|
-
|
|
11522
|
+
likeLead,
|
|
11523
|
+
dislikeLead,
|
|
10171
11524
|
prepareOutreach,
|
|
10172
11525
|
pullLeads,
|
|
10173
11526
|
pullFollowups,
|
|
10174
|
-
|
|
11527
|
+
followupsMap,
|
|
11528
|
+
researchLeadById,
|
|
11529
|
+
researchLeadByNameFuzzy,
|
|
10175
11530
|
recallOrderedTitles,
|
|
10176
11531
|
accountStatus,
|
|
10177
11532
|
bulkQualifyLeads,
|