@leadbay/mcp 0.39.3 → 0.39.5
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/dist/bin.js +697 -250
- package/dist/http-server.js +689 -243
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/http-server.js
CHANGED
|
@@ -4843,6 +4843,8 @@ to pick, then re-call with the id/exact name.
|
|
|
4843
4843
|
|
|
4844
4844
|
Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
|
|
4845
4845
|
|
|
4846
|
+
**A complaint about relevance is not a filter request.** "The leads aren't relevant", "they don't look like my customers", "I keep getting the wrong companies" says the OUTPUT is wrong \u2014 it does not say which sector to add. Read \`leadbay_get_qualification_questions\` first (it returns the questions, the ideal buyer profile AND the targeting prompt), and find out WHICH leads were wrong, ideally by pulling some and pointing at them. Sector and size are only one of the places the answer can live; editing them blind fixes the wrong thing and leaves the real cause in place.
|
|
4847
|
+
|
|
4846
4848
|
**Targeting a lens \u2014 READ THIS.** By default this edits the user's ACTIVE lens. **If the user names a lens** ("add fintech to my **Joinery** lens", "in my Nordics lens, exclude retail"), you MUST pass \`lensName\` with that name (\`lensName:"Joinery"\`). Do NOT silently edit the active lens when a different one was named \u2014 that corrupts the wrong audience and is a top friction source. The name resolves against the user's lenses (case-insensitive, exact then unique-substring); it is edit-only and does NOT change which lens is active. An unmatched name returns \`status:"lens_not_found"\` with the lens list, and a name matching several returns \`status:"ambiguous_lens"\` with the candidates \u2014 surface them and re-call with the exact \`lensName\` or a \`lensId\`. Use \`leadbay_my_lenses\` if the user first wants to SEE or SWITCH lenses. To CREATE a brand-new lens, use \`leadbay_new_lens\` \u2014 not this tool.
|
|
4847
4849
|
|
|
4848
4850
|
**Geography \u2014 scope a sales territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Texas"]\`, \`["Austin"]\`, or admin-area ids) to restrict the lens to a region, and \`exclude_locations\` to carve one out. Free text auto-resolves via \`/geo/search\` at any level from state down to city \u2014 state, *r\xE9gion*, *d\xE9partement*, county, city. Unresolved/ambiguous text returns \`status:"ambiguous_locations"\` with candidates \u2014 surface them and re-call the chosen id via the SAME axis it came from: an INCLUDE pick \u2192 \`location_ids\`; an EXCLUDE pick \u2192 \`exclude_locations\` (**NOT** \`location_ids\`, which would include the area the user asked to exclude). The returned \`message\` names the right param per text. This is how a director scopes a rep's territory and then asks for net-new accounts there.
|
|
@@ -5714,7 +5716,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
|
|
|
5714
5716
|
Prefer when: the user describes a target profile or names a count of NEW companies \u2014 craft the example_lead per the seed rules below BEFORE calling; never pass the user's raw sentence as query.
|
|
5715
5717
|
|
|
5716
5718
|
Examples that SHOULD invoke this tool:
|
|
5717
|
-
- "Find me 10 gyms around Dallas that would buy our flooring, with
|
|
5719
|
+
- "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
|
|
5718
5720
|
- "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
|
|
5719
5721
|
- "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
|
|
5720
5722
|
|
|
@@ -5725,11 +5727,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
5725
5727
|
|
|
5726
5728
|
## RENDER (quick)
|
|
5727
5729
|
|
|
5728
|
-
3-col table of delivered leads in returned order: col 1 = 10-segment fit
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
(matched/examined/delivered/stop reason)
|
|
5732
|
-
delivered. Full algorithm below.
|
|
5730
|
+
3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
|
|
5731
|
+
+ linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
|
|
5732
|
+
contact + found channels. ALWAYS close with the honest funnel line
|
|
5733
|
+
(matched/examined/delivered/stop reason), especially on 0 delivered.
|
|
5733
5734
|
|
|
5734
5735
|
---
|
|
5735
5736
|
|
|
@@ -5738,27 +5739,25 @@ company universe, applies hard filters, skips what the org already knows
|
|
|
5738
5739
|
(\`novelty: org\`), optionally qualifies against the org's own intelligence
|
|
5739
5740
|
(questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
|
|
5740
5741
|
contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
|
|
5741
|
-
\`still_running\` + \`next_poll\` \u2014 hand
|
|
5742
|
+
\`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
|
|
5742
5743
|
\u226430 min, results kept 30 days.
|
|
5743
5744
|
|
|
5744
5745
|
**Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
|
|
5745
5746
|
\`channels: []\`) is FREE: company profile + fit score + cached research +
|
|
5746
5747
|
contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
|
|
5747
5748
|
\`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
|
|
5748
|
-
on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
|
|
5749
|
-
carries \`confirm: true\` \u2014 nothing is submitted and you get
|
|
5749
|
+
on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
|
|
5750
|
+
WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
|
|
5750
5751
|
\`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
|
|
5751
5752
|
\`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
|
|
5752
|
-
\`confirm: false\` vetoes.
|
|
5753
|
-
|
|
5754
|
-
not.
|
|
5753
|
+
\`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
|
|
5754
|
+
is free, exploring it with \`qualify: true\` is not.
|
|
5755
5755
|
|
|
5756
5756
|
**Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
|
|
5757
|
-
exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
\`leadbay_refine_prompt\`.
|
|
5757
|
+
exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
|
|
5758
|
+
Violators survive, use quota and get delivered: post-filter them yourself and
|
|
5759
|
+
say the tier didn't enforce it. Durable enforcement \u2192
|
|
5760
|
+
\`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
|
|
5762
5761
|
|
|
5763
5762
|
### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
|
|
5764
5763
|
|
|
@@ -5778,13 +5777,13 @@ measured:
|
|
|
5778
5777
|
model, what they sell or operate, who they serve, observable scale. Write
|
|
5779
5778
|
it like the first paragraph of their About-Us page.
|
|
5780
5779
|
- STRONG: "Operator of full-service fitness centers offering strength
|
|
5781
|
-
areas, group classes and personal training to members across
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5780
|
+
areas, group classes and personal training to members across clubs."
|
|
5781
|
+
- WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
|
|
5782
|
+
4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
|
|
5783
|
+
"expanding", "just raised" never appear in registry text, so they match
|
|
5784
|
+
nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
|
|
5785
|
+
\`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
|
|
5786
|
+
"B2B software company operating an in-house outbound sales team."
|
|
5788
5787
|
5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
|
|
5789
5788
|
6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
|
|
5790
5789
|
\`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
|
|
@@ -5794,24 +5793,22 @@ measured:
|
|
|
5794
5793
|
never a country: this workspace's own is dropped, any other is refused.
|
|
5795
5794
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
5796
5795
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
5797
|
-
Use \`query\` only for signal an example can't express.
|
|
5798
5796
|
8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
|
|
5799
5797
|
warehouses") needs one search each with its own description and
|
|
5800
|
-
\`request_id\` \u2014 a blended seed lands between the clusters
|
|
5801
|
-
neither.
|
|
5798
|
+
\`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
|
|
5802
5799
|
|
|
5803
5800
|
|
|
5804
5801
|
**Parameter notes**
|
|
5805
5802
|
- \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
|
|
5806
5803
|
ask (same live job, no double launch); NEW for a changed ask. Derive from ask
|
|
5807
5804
|
+ archetype + date: \`gyms-dallas-2026-07-28\`.
|
|
5808
|
-
- Never lower \`min_ai_score\`
|
|
5809
|
-
|
|
5810
|
-
- \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
|
|
5811
|
-
|
|
5805
|
+
- Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
|
|
5806
|
+
the AI just scored as junk.
|
|
5807
|
+
- \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
|
|
5808
|
+
don't hammer).
|
|
5812
5809
|
|
|
5813
5810
|
**Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
|
|
5814
|
-
zero delivered gets a cause and a next move (
|
|
5811
|
+
zero delivered gets a cause and a next move (RENDERING).
|
|
5815
5812
|
|
|
5816
5813
|
---
|
|
5817
5814
|
|
|
@@ -6292,26 +6289,28 @@ WHEN NOT TO USE: when the lead summary's \`prospecting_actions_count\` is 0.
|
|
|
6292
6289
|
`;
|
|
6293
6290
|
var leadbay_get_qualification_questions = `## WHEN TO USE
|
|
6294
6291
|
|
|
6295
|
-
Trigger phrases: "what are my qualification questions", "what questions does Leadbay ask about each lead", "show me the org qualification questions", "how are my leads being qualified", "what's the qualification criteria".
|
|
6292
|
+
Trigger phrases: "what are my qualification questions", "what questions does Leadbay ask about each lead", "show me the org qualification questions", "how are my leads being qualified", "what's the qualification criteria", "(before any settings change) what is configured today", "why am I getting these leads", "the leads aren't relevant \u2014 what are my settings".
|
|
6296
6293
|
|
|
6297
|
-
Do NOT use for: "how did this lead score on the qualification questions" \u2192 \`leadbay_research_lead_by_id\`; "
|
|
6294
|
+
Do NOT use for: "how did this lead score on the qualification questions" \u2192 \`leadbay_research_lead_by_id\`; "change / add / remove a qualification question" \u2192 \`leadbay_set_qualification_questions\`; "answer a pending clarification" \u2192 \`leadbay_answer_clarification\`.
|
|
6298
6295
|
|
|
6299
|
-
Prefer when: user wants the ORG-level qualification
|
|
6296
|
+
Prefer when: user wants the ORG-level qualification settings, or you are about to change any of them \u2014 read this FIRST to see whether their rule is already covered
|
|
6300
6297
|
|
|
6301
6298
|
Examples that SHOULD invoke this tool:
|
|
6302
6299
|
- "What qualification questions does Leadbay use to score my leads?"
|
|
6303
6300
|
- "Show me my org's qualification questions."
|
|
6301
|
+
- "Why do I keep getting these companies? What are my settings?"
|
|
6304
6302
|
|
|
6305
6303
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6306
6304
|
- "How did Acme Corp answer the qualification questions?"
|
|
6307
|
-
- "
|
|
6305
|
+
- "Add a question about install crews."
|
|
6308
6306
|
|
|
6309
6307
|
## RENDER (quick)
|
|
6310
6308
|
|
|
6311
|
-
Numbered list of the questions (chat-native markdown), each one line
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6309
|
+
Numbered list of the questions (chat-native markdown), each one line,
|
|
6310
|
+
verbatim. Below it, the ideal buyer profile summary + anti-patterns and the
|
|
6311
|
+
targeting prompt when set. When \`is_admin\` is true, append the \`hint\` as a
|
|
6312
|
+
footnote (points at leadbay_set_qualification_questions). When the question
|
|
6313
|
+
list is empty, say so explicitly and render the \`hint\` instead.
|
|
6315
6314
|
|
|
6316
6315
|
---
|
|
6317
6316
|
|
|
@@ -6324,7 +6323,15 @@ Returns:
|
|
|
6324
6323
|
|
|
6325
6324
|
- **\`qualification_questions\`** \u2014 the catalog. Each: \`{question, created_at,
|
|
6326
6325
|
lang}\`. Ordered as the backend returns them.
|
|
6327
|
-
- **\`count\`** \u2014 number of configured questions.
|
|
6326
|
+
- **\`count\`** \u2014 number of configured questions. **Zero is a finding, not a
|
|
6327
|
+
blank** \u2014 an org with no questions scores every lead on firmographics alone.
|
|
6328
|
+
Say so and offer a starter set.
|
|
6329
|
+
- **\`ideal_buyer_profile\`** \u2014 \`{summary, key_characteristics, anti_patterns}\`
|
|
6330
|
+
or null. The questions score against THIS. A rule the user states is often
|
|
6331
|
+
already an \`anti_pattern\` here.
|
|
6332
|
+
- **\`targeting_prompt\`** \u2014 the org's free-text instruction to the AI agent, or
|
|
6333
|
+
null. Qualitative rules that no single yes/no can express live here; change
|
|
6334
|
+
it with \`leadbay_refine_prompt\`.
|
|
6328
6335
|
- **\`is_admin\`** \u2014 whether the current user is an org admin. Modifying the
|
|
6329
6336
|
questions (\`leadbay_set_qualification_questions\`) is an org-admin action; for
|
|
6330
6337
|
admins a \`hint\` points there.
|
|
@@ -6332,25 +6339,132 @@ Returns:
|
|
|
6332
6339
|
when no questions are configured.
|
|
6333
6340
|
|
|
6334
6341
|
This tool only READS. To change the questions, use
|
|
6335
|
-
**leadbay_set_qualification_questions
|
|
6336
|
-
|
|
6337
|
-
|
|
6342
|
+
**leadbay_set_qualification_questions**; to change the targeting prompt, use
|
|
6343
|
+
**leadbay_refine_prompt**; for sector / size / territory, use
|
|
6344
|
+
**leadbay_adjust_audience**. **leadbay_research_lead_by_id** shows how a
|
|
6345
|
+
SPECIFIC lead answered these questions.
|
|
6346
|
+
|
|
6347
|
+
### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
|
|
6348
|
+
|
|
6349
|
+
**Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
|
|
6350
|
+
"I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
|
|
6351
|
+
claim about the user's ACCOUNT, and it is false unless a tool call made it
|
|
6352
|
+
true. The rule lives in the org's settings or it does not exist: your context
|
|
6353
|
+
window ends with this conversation, and the next session, the scheduled run and
|
|
6354
|
+
the user's colleague all read the account, not your memory. If you have not
|
|
6355
|
+
called a tool, do not say the rule is in place.
|
|
6356
|
+
|
|
6357
|
+
When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
|
|
6358
|
+
liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
|
|
6359
|
+
own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
|
|
6360
|
+
describing their account, not just this batch. Filter the batch and they say
|
|
6361
|
+
it again next week; their questions, buyer profile and targeting prompt never
|
|
6362
|
+
move.
|
|
6363
|
+
|
|
6364
|
+
**1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
|
|
6365
|
+
first. It returns the question set PLUS the ideal buyer profile and the
|
|
6366
|
+
targeting prompt those questions sit beside. You cannot judge whether a rule is
|
|
6367
|
+
already covered without seeing them.
|
|
6368
|
+
|
|
6369
|
+
**2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
|
|
6370
|
+
|
|
6371
|
+
| What the user stated | Where it belongs |
|
|
6372
|
+
|---|---|
|
|
6373
|
+
| A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
|
|
6374
|
+
| A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
|
|
6375
|
+
| A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
|
|
6376
|
+
| Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
|
|
6377
|
+
| CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
|
|
6378
|
+
| A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
|
|
6379
|
+
| An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
|
|
6380
|
+
|
|
6381
|
+
**3 \u2014 Decide whether to change anything at all.** Touching a question
|
|
6382
|
+
re-scores every lead in the pipeline and draws on the org's quota, so a change
|
|
6383
|
+
that surfaces the same companies is a pure loss. Four reasons to write NOTHING
|
|
6384
|
+
and say why:
|
|
6385
|
+
|
|
6386
|
+
1. **An existing question already covers the rule.** Quote that question back
|
|
6387
|
+
and stop. **Never reword a question that already means the same thing** \u2014
|
|
6388
|
+
even when the user asks you to "clarify" or "improve" the wording. A reword
|
|
6389
|
+
is a removal plus an addition: it re-scores every lead, spends quota, and
|
|
6390
|
+
surfaces exactly the same companies. Offer instead to find out whether any
|
|
6391
|
+
question is testing the WRONG thing.
|
|
6392
|
+
2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
|
|
6393
|
+
3. **An existing question already tests that dimension.** Two questions on one
|
|
6394
|
+
dimension waste a slot and add no signal.
|
|
6395
|
+
4. **The question the user asked for is not decisive.** See step 4: say so,
|
|
6396
|
+
offer the sharper version, and write only what they then choose. Adding a
|
|
6397
|
+
question you know separates nothing is worse than adding none.
|
|
6398
|
+
|
|
6399
|
+
**The ceiling is 5 questions.** Read the count before you answer an "add a
|
|
6400
|
+
question" request: at 5 the honest answer is not "sure, I'll add it". Say in
|
|
6401
|
+
that same turn that the set is full, list the five, and let the USER name which
|
|
6402
|
+
one goes. Never pre-pick the casualty.
|
|
6403
|
+
|
|
6404
|
+
**4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
|
|
6405
|
+
|
|
6406
|
+
1. **The estimative marker is literal and mandatory.** English questions start
|
|
6407
|
+
\`Is the company likely to \u2026\`. French questions start
|
|
6408
|
+
\`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
|
|
6409
|
+
scorer works from public text it cannot verify, so a verifiable question
|
|
6410
|
+
scores almost everything as no.
|
|
6411
|
+
- \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
|
|
6412
|
+
- \u274C \`Does the company run its own maintenance crew?\`
|
|
6413
|
+
- \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
|
|
6414
|
+
- \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
|
|
6415
|
+
- \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
|
|
6416
|
+
2. **It tests the lead as a BUYER.** Before you write a question, ask: would
|
|
6417
|
+
a company answering yes write a cheque to THIS user? A question that only
|
|
6418
|
+
describes what the lead's own business does \u2014 "Is the company likely to
|
|
6419
|
+
manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
|
|
6420
|
+
category test, not a buying test, and it scores the user's competitors and
|
|
6421
|
+
suppliers as well as their prospects. Test the need the user's offering
|
|
6422
|
+
meets: "Is the company likely to run consumer campaigns that need paid media
|
|
6423
|
+
placement?"
|
|
6424
|
+
3. **Estimable from public material** \u2014 the company's website, its about page,
|
|
6425
|
+
its job postings, its registry entry. Never its budget, its internal plans
|
|
6426
|
+
or its future intentions. When the user's rule is un-observable ("has budget
|
|
6427
|
+
for copywriting"), propose the observable proxy, and tell them you swapped
|
|
6428
|
+
it and why.
|
|
6429
|
+
4. **One dimension each.** \`Is the company likely to operate a cold-storage
|
|
6430
|
+
plant AND run its own maintenance crew?\` is two questions. Split it into
|
|
6431
|
+
two, or pick the one that discriminates better.
|
|
6432
|
+
5. **Decisive.** It should split companies in general roughly 30/70 while the
|
|
6433
|
+
user's own customers answer yes. A question nearly everyone answers yes to
|
|
6434
|
+
\u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
|
|
6435
|
+
write it as asked: say it would add no signal, and offer the sharper version
|
|
6436
|
+
you would write instead.
|
|
6437
|
+
6. **\u2264120 characters, in the user's language.**
|
|
6438
|
+
|
|
6439
|
+
An org with **zero** questions scores every lead on firmographics alone. That
|
|
6440
|
+
is a finding worth stating, and the fix is a starter set of **3** questions on
|
|
6441
|
+
three different dimensions \u2014 not one. Propose all three at once.
|
|
6442
|
+
|
|
6443
|
+
**5 \u2014 The change is the user's call, not yours.** This holds for the questions,
|
|
6444
|
+
the targeting prompt and the buyer profile alike. Show the exact text you
|
|
6445
|
+
propose and what it will change, then get an explicit yes before calling
|
|
6446
|
+
\`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
|
|
6447
|
+
\`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
|
|
6448
|
+
needs \`confirm:true\`. Do not write an org setting in the same turn the user
|
|
6449
|
+
first stated the rule \u2014 and once they have said yes, actually write it:
|
|
6450
|
+
describing the change is not making it.
|
|
6451
|
+
|
|
6452
|
+
**Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
|
|
6453
|
+
jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
|
|
6454
|
+
ask. Deliver the leads first, then raise the setting.
|
|
6338
6455
|
|
|
6339
|
-
Companion tools: **leadbay_set_qualification_questions** to modify the questions;
|
|
6340
|
-
**leadbay_get_taste_profile** when the user also wants the Ideal Buyer Profile +
|
|
6341
|
-
purchase-intent tags; **leadbay_research_lead_by_id** for how a SPECIFIC lead
|
|
6342
|
-
answered these questions; **leadbay_refine_prompt** to shape the AI agent's
|
|
6343
|
-
behaviour.
|
|
6344
6456
|
|
|
6345
6457
|
### RENDERING
|
|
6346
6458
|
|
|
6347
6459
|
Render \`qualification_questions\` as a numbered list \u2014 one question per line, in
|
|
6348
6460
|
the order returned. Lead with a short heading like **"Qualification questions
|
|
6349
|
-
(N)"**.
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6461
|
+
(N)"**. Then, when present, the \`ideal_buyer_profile\` summary with its
|
|
6462
|
+
\`anti_patterns\` as a short bulleted list, and the \`targeting_prompt\` as a
|
|
6463
|
+
blockquote. When \`qualification_questions\` is empty, say **"You have no
|
|
6464
|
+
qualification questions \u2014 leads are scored on firmographics alone"** and render
|
|
6465
|
+
the \`hint\`. When \`is_admin\` is true and there are questions, append the \`hint\`
|
|
6466
|
+
as a one-line footnote. Do not invent questions or reword them \u2014 render
|
|
6467
|
+
verbatim.
|
|
6354
6468
|
`;
|
|
6355
6469
|
var leadbay_get_quota = `Read quota / spend across daily, weekly, and monthly windows. The response has two scope groups: **\`user\`** (present for every caller) and **\`org\`** (admin-only \u2014 \`null\` for non-admins). **Read from \`user\` first**, falling back to \`org\` only when \`user\` is absent. Each group carries \`spend[]\` (the dollar-spend gauge: \`current_units\` / \`max_units\` in dollar_cents \u2192 % used = the ratio, $ = \`/100\`) and \`resources[]\` (per-resource usage: \`{resource_type, count (used), max_units (cap or null), window_type, resets_at}\`). \`spend[]\` is empty for orgs with no OVERALL_SPEND quota \u2014 fall back to the \`resources[]\` counts then. There is also a top-level \`topup\` ({remaining_cents, total_credit_cents}) when present. Resource types may arrive lowercase (\`lens_extra_refill\`) or uppercase \u2014 match case-insensitively. Present quota as a percentage / dollar figure, never raw "credits".
|
|
6356
6470
|
|
|
@@ -8438,20 +8552,22 @@ WHEN NOT TO USE: when you already know the exact titles you want to enrich.
|
|
|
8438
8552
|
`;
|
|
8439
8553
|
var leadbay_refine_prompt = `## WHEN TO USE
|
|
8440
8554
|
|
|
8441
|
-
Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised".
|
|
8555
|
+
Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised", "stop showing me <kind of company>", "we only sell to the private sector, not the public one", "harden the exclusion on <business model>, not the keyword", "competitors / resellers are never prospects for us".
|
|
8442
8556
|
|
|
8443
|
-
Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`.
|
|
8557
|
+
Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "a company trait one yes/no question could estimate from their website" \u2192 \`leadbay_set_qualification_questions\`; "exclude <named company>" \u2192 \`leadbay_dislike_lead\`.
|
|
8444
8558
|
|
|
8445
|
-
Prefer when: ADMIN-ONLY.
|
|
8559
|
+
Prefer when: ADMIN-ONLY. A qualitative ORIENTATION too broad for one yes/no \u2014 a segment to avoid, a business model to rule out. One estimable company trait goes to set_qualification_questions; a named company to dislike_lead.
|
|
8446
8560
|
|
|
8447
8561
|
Examples that SHOULD invoke this tool:
|
|
8448
8562
|
- "Focus on hospitals that run their own IT in-house."
|
|
8449
8563
|
- "Prioritize companies that have recently expanded headcount."
|
|
8564
|
+
- "On ne vend qu'au priv\xE9 \u2014 arr\xEAte de me remonter des h\xF4pitaux publics."
|
|
8450
8565
|
|
|
8451
8566
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8452
8567
|
- "Create a lens specialized in automobile."
|
|
8453
8568
|
- "Add fintech to my Joinery lens."
|
|
8454
8569
|
- "Show me my lenses."
|
|
8570
|
+
- "Exclude Groupe Solidum, we had an unpaid invoice with them."
|
|
8455
8571
|
|
|
8456
8572
|
## RENDER (quick)
|
|
8457
8573
|
|
|
@@ -8462,6 +8578,121 @@ clarification was raised, surface its question (route via ask_user_input_v0).
|
|
|
8462
8578
|
|
|
8463
8579
|
Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
|
|
8464
8580
|
|
|
8581
|
+
**Read \`leadbay_get_qualification_questions\` first.** It returns the current
|
|
8582
|
+
targeting prompt alongside the questions and the buyer profile. Setting a
|
|
8583
|
+
prompt REPLACES the previous one \u2014 write the combined instruction, not just the
|
|
8584
|
+
new clause, or you silently drop rules the user set earlier.
|
|
8585
|
+
|
|
8586
|
+
### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
|
|
8587
|
+
|
|
8588
|
+
**Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
|
|
8589
|
+
"I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
|
|
8590
|
+
claim about the user's ACCOUNT, and it is false unless a tool call made it
|
|
8591
|
+
true. The rule lives in the org's settings or it does not exist: your context
|
|
8592
|
+
window ends with this conversation, and the next session, the scheduled run and
|
|
8593
|
+
the user's colleague all read the account, not your memory. If you have not
|
|
8594
|
+
called a tool, do not say the rule is in place.
|
|
8595
|
+
|
|
8596
|
+
When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
|
|
8597
|
+
liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
|
|
8598
|
+
own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
|
|
8599
|
+
describing their account, not just this batch. Filter the batch and they say
|
|
8600
|
+
it again next week; their questions, buyer profile and targeting prompt never
|
|
8601
|
+
move.
|
|
8602
|
+
|
|
8603
|
+
**1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
|
|
8604
|
+
first. It returns the question set PLUS the ideal buyer profile and the
|
|
8605
|
+
targeting prompt those questions sit beside. You cannot judge whether a rule is
|
|
8606
|
+
already covered without seeing them.
|
|
8607
|
+
|
|
8608
|
+
**2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
|
|
8609
|
+
|
|
8610
|
+
| What the user stated | Where it belongs |
|
|
8611
|
+
|---|---|
|
|
8612
|
+
| A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
|
|
8613
|
+
| A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
|
|
8614
|
+
| A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
|
|
8615
|
+
| Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
|
|
8616
|
+
| CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
|
|
8617
|
+
| A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
|
|
8618
|
+
| An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
|
|
8619
|
+
|
|
8620
|
+
**3 \u2014 Decide whether to change anything at all.** Touching a question
|
|
8621
|
+
re-scores every lead in the pipeline and draws on the org's quota, so a change
|
|
8622
|
+
that surfaces the same companies is a pure loss. Four reasons to write NOTHING
|
|
8623
|
+
and say why:
|
|
8624
|
+
|
|
8625
|
+
1. **An existing question already covers the rule.** Quote that question back
|
|
8626
|
+
and stop. **Never reword a question that already means the same thing** \u2014
|
|
8627
|
+
even when the user asks you to "clarify" or "improve" the wording. A reword
|
|
8628
|
+
is a removal plus an addition: it re-scores every lead, spends quota, and
|
|
8629
|
+
surfaces exactly the same companies. Offer instead to find out whether any
|
|
8630
|
+
question is testing the WRONG thing.
|
|
8631
|
+
2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
|
|
8632
|
+
3. **An existing question already tests that dimension.** Two questions on one
|
|
8633
|
+
dimension waste a slot and add no signal.
|
|
8634
|
+
4. **The question the user asked for is not decisive.** See step 4: say so,
|
|
8635
|
+
offer the sharper version, and write only what they then choose. Adding a
|
|
8636
|
+
question you know separates nothing is worse than adding none.
|
|
8637
|
+
|
|
8638
|
+
**The ceiling is 5 questions.** Read the count before you answer an "add a
|
|
8639
|
+
question" request: at 5 the honest answer is not "sure, I'll add it". Say in
|
|
8640
|
+
that same turn that the set is full, list the five, and let the USER name which
|
|
8641
|
+
one goes. Never pre-pick the casualty.
|
|
8642
|
+
|
|
8643
|
+
**4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
|
|
8644
|
+
|
|
8645
|
+
1. **The estimative marker is literal and mandatory.** English questions start
|
|
8646
|
+
\`Is the company likely to \u2026\`. French questions start
|
|
8647
|
+
\`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
|
|
8648
|
+
scorer works from public text it cannot verify, so a verifiable question
|
|
8649
|
+
scores almost everything as no.
|
|
8650
|
+
- \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
|
|
8651
|
+
- \u274C \`Does the company run its own maintenance crew?\`
|
|
8652
|
+
- \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
|
|
8653
|
+
- \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
|
|
8654
|
+
- \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
|
|
8655
|
+
2. **It tests the lead as a BUYER.** Before you write a question, ask: would
|
|
8656
|
+
a company answering yes write a cheque to THIS user? A question that only
|
|
8657
|
+
describes what the lead's own business does \u2014 "Is the company likely to
|
|
8658
|
+
manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
|
|
8659
|
+
category test, not a buying test, and it scores the user's competitors and
|
|
8660
|
+
suppliers as well as their prospects. Test the need the user's offering
|
|
8661
|
+
meets: "Is the company likely to run consumer campaigns that need paid media
|
|
8662
|
+
placement?"
|
|
8663
|
+
3. **Estimable from public material** \u2014 the company's website, its about page,
|
|
8664
|
+
its job postings, its registry entry. Never its budget, its internal plans
|
|
8665
|
+
or its future intentions. When the user's rule is un-observable ("has budget
|
|
8666
|
+
for copywriting"), propose the observable proxy, and tell them you swapped
|
|
8667
|
+
it and why.
|
|
8668
|
+
4. **One dimension each.** \`Is the company likely to operate a cold-storage
|
|
8669
|
+
plant AND run its own maintenance crew?\` is two questions. Split it into
|
|
8670
|
+
two, or pick the one that discriminates better.
|
|
8671
|
+
5. **Decisive.** It should split companies in general roughly 30/70 while the
|
|
8672
|
+
user's own customers answer yes. A question nearly everyone answers yes to
|
|
8673
|
+
\u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
|
|
8674
|
+
write it as asked: say it would add no signal, and offer the sharper version
|
|
8675
|
+
you would write instead.
|
|
8676
|
+
6. **\u2264120 characters, in the user's language.**
|
|
8677
|
+
|
|
8678
|
+
An org with **zero** questions scores every lead on firmographics alone. That
|
|
8679
|
+
is a finding worth stating, and the fix is a starter set of **3** questions on
|
|
8680
|
+
three different dimensions \u2014 not one. Propose all three at once.
|
|
8681
|
+
|
|
8682
|
+
**5 \u2014 The change is the user's call, not yours.** This holds for the questions,
|
|
8683
|
+
the targeting prompt and the buyer profile alike. Show the exact text you
|
|
8684
|
+
propose and what it will change, then get an explicit yes before calling
|
|
8685
|
+
\`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
|
|
8686
|
+
\`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
|
|
8687
|
+
needs \`confirm:true\`. Do not write an org setting in the same turn the user
|
|
8688
|
+
first stated the rule \u2014 and once they have said yes, actually write it:
|
|
8689
|
+
describing the change is not making it.
|
|
8690
|
+
|
|
8691
|
+
**Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
|
|
8692
|
+
jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
|
|
8693
|
+
ask. Deliver the leads first, then raise the setting.
|
|
8694
|
+
|
|
8695
|
+
|
|
8465
8696
|
WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
8466
8697
|
|
|
8467
8698
|
WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
|
|
@@ -9513,7 +9744,35 @@ WHEN NOT TO USE: the user is just skipping ONE outreach attempt \u2014 that's a
|
|
|
9513
9744
|
|
|
9514
9745
|
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\`.
|
|
9515
9746
|
`;
|
|
9516
|
-
var leadbay_set_qualification_questions =
|
|
9747
|
+
var leadbay_set_qualification_questions = `## WHEN TO USE
|
|
9748
|
+
|
|
9749
|
+
Trigger phrases: "add / remove / replace a qualification question", "update my qualification so Leadbay looks for <trait>", "my best customers are <trait> \u2014 target those", "I don't want <kind of company> at all", "the leads aren't relevant, fix my criteria".
|
|
9750
|
+
|
|
9751
|
+
Do NOT use for: "what are my qualification questions" \u2192 \`leadbay_get_qualification_questions\`; "narrow to <sector> / <headcount> / <territory>" \u2192 \`leadbay_adjust_audience\`; "a qualitative orientation one yes/no can't express" \u2192 \`leadbay_refine_prompt\`; "exclude <named company>" \u2192 \`leadbay_dislike_lead\`; "only leads that already have an email and a phone" \u2192 \`leadbay_enrich_titles\`.
|
|
9752
|
+
|
|
9753
|
+
Prefer when: the user stated a durable company TRAIT a stranger could estimate from that company's public material, and get_qualification_questions shows nothing covering it. Read first, propose the text, write only on a yes.
|
|
9754
|
+
|
|
9755
|
+
Examples that SHOULD invoke this tool:
|
|
9756
|
+
- "Our best customers are cold-storage plants that run their own maintenance crews \u2014 update my qualification for that."
|
|
9757
|
+
- "\xC9carte les soci\xE9t\xE9s en liquidation, je ne veux plus les voir."
|
|
9758
|
+
- "Remove the flooring question and add one about install crews."
|
|
9759
|
+
|
|
9760
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
9761
|
+
- "What qualification questions does Leadbay use?"
|
|
9762
|
+
- "Add fintech to my Joinery lens."
|
|
9763
|
+
- "Exclude Groupe Solidum, we had an unpaid invoice with them."
|
|
9764
|
+
|
|
9765
|
+
## RENDER (quick)
|
|
9766
|
+
|
|
9767
|
+
Before writing: show the exact question text you propose, say what it
|
|
9768
|
+
changes, and ask for a yes (route via ask_user_input_v0). After writing:
|
|
9769
|
+
one confirmation line ("Added 1 question \u2014 you now score leads against 4
|
|
9770
|
+
questions.") then the resulting questions as a numbered list. On a
|
|
9771
|
+
non-changing preview, surface the \`hint\` and ask \u2014 never auto-confirm.
|
|
9772
|
+
|
|
9773
|
+
---
|
|
9774
|
+
|
|
9775
|
+
Modify the organization's **qualification questions** \u2014 the AI-agent questions Leadbay scores every lead against. Use when the user wants to add, remove, or rewrite them \u2014 e.g. "add a question about whether they run install crews", "remove the flooring question", "replace my questions with these three".
|
|
9517
9776
|
|
|
9518
9777
|
The backend stores the list as a whole, so this tool reads the current questions and applies your change:
|
|
9519
9778
|
|
|
@@ -9525,11 +9784,142 @@ Leadbay allows **at most 5** qualification questions. If a change would exceed 5
|
|
|
9525
9784
|
|
|
9526
9785
|
**Dropping any existing question is destructive** \u2014 it changes how every lead is scored. Any change that removes a current question requires \`confirm:true\` \u2014 including a same-count **swap** (remove one + add one) or a \`questions\` replacement that omits a current question, not only when the list gets shorter. Without \`confirm\`, the tool previews what would be removed and applies nothing. Pure additions never need confirm.
|
|
9527
9786
|
|
|
9528
|
-
Returns the resulting \`{qualification_questions, count, previous_count, changed}\`.
|
|
9787
|
+
Returns the resulting \`{qualification_questions, count, previous_count, changed}\`.
|
|
9788
|
+
|
|
9789
|
+
### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
|
|
9790
|
+
|
|
9791
|
+
**Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
|
|
9792
|
+
"I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
|
|
9793
|
+
claim about the user's ACCOUNT, and it is false unless a tool call made it
|
|
9794
|
+
true. The rule lives in the org's settings or it does not exist: your context
|
|
9795
|
+
window ends with this conversation, and the next session, the scheduled run and
|
|
9796
|
+
the user's colleague all read the account, not your memory. If you have not
|
|
9797
|
+
called a tool, do not say the rule is in place.
|
|
9798
|
+
|
|
9799
|
+
When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
|
|
9800
|
+
liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
|
|
9801
|
+
own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
|
|
9802
|
+
describing their account, not just this batch. Filter the batch and they say
|
|
9803
|
+
it again next week; their questions, buyer profile and targeting prompt never
|
|
9804
|
+
move.
|
|
9805
|
+
|
|
9806
|
+
**1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
|
|
9807
|
+
first. It returns the question set PLUS the ideal buyer profile and the
|
|
9808
|
+
targeting prompt those questions sit beside. You cannot judge whether a rule is
|
|
9809
|
+
already covered without seeing them.
|
|
9810
|
+
|
|
9811
|
+
**2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
|
|
9812
|
+
|
|
9813
|
+
| What the user stated | Where it belongs |
|
|
9814
|
+
|---|---|
|
|
9815
|
+
| A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
|
|
9816
|
+
| A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
|
|
9817
|
+
| A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
|
|
9818
|
+
| Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
|
|
9819
|
+
| CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
|
|
9820
|
+
| A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
|
|
9821
|
+
| An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
|
|
9822
|
+
|
|
9823
|
+
**3 \u2014 Decide whether to change anything at all.** Touching a question
|
|
9824
|
+
re-scores every lead in the pipeline and draws on the org's quota, so a change
|
|
9825
|
+
that surfaces the same companies is a pure loss. Four reasons to write NOTHING
|
|
9826
|
+
and say why:
|
|
9827
|
+
|
|
9828
|
+
1. **An existing question already covers the rule.** Quote that question back
|
|
9829
|
+
and stop. **Never reword a question that already means the same thing** \u2014
|
|
9830
|
+
even when the user asks you to "clarify" or "improve" the wording. A reword
|
|
9831
|
+
is a removal plus an addition: it re-scores every lead, spends quota, and
|
|
9832
|
+
surfaces exactly the same companies. Offer instead to find out whether any
|
|
9833
|
+
question is testing the WRONG thing.
|
|
9834
|
+
2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
|
|
9835
|
+
3. **An existing question already tests that dimension.** Two questions on one
|
|
9836
|
+
dimension waste a slot and add no signal.
|
|
9837
|
+
4. **The question the user asked for is not decisive.** See step 4: say so,
|
|
9838
|
+
offer the sharper version, and write only what they then choose. Adding a
|
|
9839
|
+
question you know separates nothing is worse than adding none.
|
|
9840
|
+
|
|
9841
|
+
**The ceiling is 5 questions.** Read the count before you answer an "add a
|
|
9842
|
+
question" request: at 5 the honest answer is not "sure, I'll add it". Say in
|
|
9843
|
+
that same turn that the set is full, list the five, and let the USER name which
|
|
9844
|
+
one goes. Never pre-pick the casualty.
|
|
9845
|
+
|
|
9846
|
+
**4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
|
|
9847
|
+
|
|
9848
|
+
1. **The estimative marker is literal and mandatory.** English questions start
|
|
9849
|
+
\`Is the company likely to \u2026\`. French questions start
|
|
9850
|
+
\`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
|
|
9851
|
+
scorer works from public text it cannot verify, so a verifiable question
|
|
9852
|
+
scores almost everything as no.
|
|
9853
|
+
- \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
|
|
9854
|
+
- \u274C \`Does the company run its own maintenance crew?\`
|
|
9855
|
+
- \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
|
|
9856
|
+
- \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
|
|
9857
|
+
- \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
|
|
9858
|
+
2. **It tests the lead as a BUYER.** Before you write a question, ask: would
|
|
9859
|
+
a company answering yes write a cheque to THIS user? A question that only
|
|
9860
|
+
describes what the lead's own business does \u2014 "Is the company likely to
|
|
9861
|
+
manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
|
|
9862
|
+
category test, not a buying test, and it scores the user's competitors and
|
|
9863
|
+
suppliers as well as their prospects. Test the need the user's offering
|
|
9864
|
+
meets: "Is the company likely to run consumer campaigns that need paid media
|
|
9865
|
+
placement?"
|
|
9866
|
+
3. **Estimable from public material** \u2014 the company's website, its about page,
|
|
9867
|
+
its job postings, its registry entry. Never its budget, its internal plans
|
|
9868
|
+
or its future intentions. When the user's rule is un-observable ("has budget
|
|
9869
|
+
for copywriting"), propose the observable proxy, and tell them you swapped
|
|
9870
|
+
it and why.
|
|
9871
|
+
4. **One dimension each.** \`Is the company likely to operate a cold-storage
|
|
9872
|
+
plant AND run its own maintenance crew?\` is two questions. Split it into
|
|
9873
|
+
two, or pick the one that discriminates better.
|
|
9874
|
+
5. **Decisive.** It should split companies in general roughly 30/70 while the
|
|
9875
|
+
user's own customers answer yes. A question nearly everyone answers yes to
|
|
9876
|
+
\u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
|
|
9877
|
+
write it as asked: say it would add no signal, and offer the sharper version
|
|
9878
|
+
you would write instead.
|
|
9879
|
+
6. **\u2264120 characters, in the user's language.**
|
|
9880
|
+
|
|
9881
|
+
An org with **zero** questions scores every lead on firmographics alone. That
|
|
9882
|
+
is a finding worth stating, and the fix is a starter set of **3** questions on
|
|
9883
|
+
three different dimensions \u2014 not one. Propose all three at once.
|
|
9884
|
+
|
|
9885
|
+
**5 \u2014 The change is the user's call, not yours.** This holds for the questions,
|
|
9886
|
+
the targeting prompt and the buyer profile alike. Show the exact text you
|
|
9887
|
+
propose and what it will change, then get an explicit yes before calling
|
|
9888
|
+
\`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
|
|
9889
|
+
\`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
|
|
9890
|
+
needs \`confirm:true\`. Do not write an org setting in the same turn the user
|
|
9891
|
+
first stated the rule \u2014 and once they have said yes, actually write it:
|
|
9892
|
+
describing the change is not making it.
|
|
9893
|
+
|
|
9894
|
+
**Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
|
|
9895
|
+
jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
|
|
9896
|
+
ask. Deliver the leads first, then raise the setting.
|
|
9897
|
+
|
|
9898
|
+
|
|
9899
|
+
WHEN TO USE: the user stated a durable company trait no existing question covers, and they have agreed to the exact text you proposed.
|
|
9900
|
+
|
|
9901
|
+
WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions), to change a single lead's data, or for any rule the table above sends elsewhere. This is org-level \u2014 it affects scoring for ALL leads.
|
|
9902
|
+
|
|
9903
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
9904
|
+
|
|
9905
|
+
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.
|
|
9529
9906
|
|
|
9530
|
-
|
|
9907
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
9908
|
+
|
|
9909
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
9910
|
+
|---|---|---|
|
|
9911
|
+
| \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
|
|
9912
|
+
| \`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") |
|
|
9913
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
9914
|
+
|
|
9915
|
+
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.
|
|
9916
|
+
|
|
9917
|
+
**Rules:**
|
|
9918
|
+
- 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.
|
|
9919
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
9920
|
+
- 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.
|
|
9921
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
9531
9922
|
|
|
9532
|
-
WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions) or to change a single lead's data. This is org-level \u2014 it affects scoring for ALL leads.
|
|
9533
9923
|
|
|
9534
9924
|
### RENDERING
|
|
9535
9925
|
|
|
@@ -10123,7 +10513,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
|
|
|
10123
10513
|
Prefer when: the user describes a target profile or names a count of NEW companies \u2014 craft the example_lead per the seed rules below BEFORE calling; never pass the user's raw sentence as query.
|
|
10124
10514
|
|
|
10125
10515
|
Examples that SHOULD invoke this tool:
|
|
10126
|
-
- "Find me 10 gyms around Dallas that would buy our flooring, with
|
|
10516
|
+
- "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
|
|
10127
10517
|
- "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
|
|
10128
10518
|
- "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
|
|
10129
10519
|
|
|
@@ -10134,11 +10524,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
10134
10524
|
|
|
10135
10525
|
## RENDER (quick)
|
|
10136
10526
|
|
|
10137
|
-
3-col table of delivered leads in returned order: col 1 = 10-segment fit
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
(matched/examined/delivered/stop reason)
|
|
10141
|
-
delivered. Full algorithm below.
|
|
10527
|
+
3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
|
|
10528
|
+
+ linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
|
|
10529
|
+
contact + found channels. ALWAYS close with the honest funnel line
|
|
10530
|
+
(matched/examined/delivered/stop reason), especially on 0 delivered.
|
|
10142
10531
|
|
|
10143
10532
|
---
|
|
10144
10533
|
|
|
@@ -10147,27 +10536,25 @@ company universe, applies hard filters, skips what the org already knows
|
|
|
10147
10536
|
(\`novelty: org\`), optionally qualifies against the org's own intelligence
|
|
10148
10537
|
(questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
|
|
10149
10538
|
contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
|
|
10150
|
-
\`still_running\` + \`next_poll\` \u2014 hand
|
|
10539
|
+
\`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
|
|
10151
10540
|
\u226430 min, results kept 30 days.
|
|
10152
10541
|
|
|
10153
10542
|
**Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
|
|
10154
10543
|
\`channels: []\`) is FREE: company profile + fit score + cached research +
|
|
10155
10544
|
contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
|
|
10156
10545
|
\`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
|
|
10157
|
-
on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
|
|
10158
|
-
carries \`confirm: true\` \u2014 nothing is submitted and you get
|
|
10546
|
+
on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
|
|
10547
|
+
WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
|
|
10159
10548
|
\`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
|
|
10160
10549
|
\`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
|
|
10161
|
-
\`confirm: false\` vetoes.
|
|
10162
|
-
|
|
10163
|
-
not.
|
|
10550
|
+
\`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
|
|
10551
|
+
is free, exploring it with \`qualify: true\` is not.
|
|
10164
10552
|
|
|
10165
10553
|
**Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
|
|
10166
|
-
exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
\`leadbay_refine_prompt\`.
|
|
10554
|
+
exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
|
|
10555
|
+
Violators survive, use quota and get delivered: post-filter them yourself and
|
|
10556
|
+
say the tier didn't enforce it. Durable enforcement \u2192
|
|
10557
|
+
\`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
|
|
10171
10558
|
|
|
10172
10559
|
### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
|
|
10173
10560
|
|
|
@@ -10187,13 +10574,13 @@ measured:
|
|
|
10187
10574
|
model, what they sell or operate, who they serve, observable scale. Write
|
|
10188
10575
|
it like the first paragraph of their About-Us page.
|
|
10189
10576
|
- STRONG: "Operator of full-service fitness centers offering strength
|
|
10190
|
-
areas, group classes and personal training to members across
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10577
|
+
areas, group classes and personal training to members across clubs."
|
|
10578
|
+
- WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
|
|
10579
|
+
4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
|
|
10580
|
+
"expanding", "just raised" never appear in registry text, so they match
|
|
10581
|
+
nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
|
|
10582
|
+
\`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
|
|
10583
|
+
"B2B software company operating an in-house outbound sales team."
|
|
10197
10584
|
5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
|
|
10198
10585
|
6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
|
|
10199
10586
|
\`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
|
|
@@ -10203,24 +10590,22 @@ measured:
|
|
|
10203
10590
|
never a country: this workspace's own is dropped, any other is refused.
|
|
10204
10591
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
10205
10592
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
10206
|
-
Use \`query\` only for signal an example can't express.
|
|
10207
10593
|
8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
|
|
10208
10594
|
warehouses") needs one search each with its own description and
|
|
10209
|
-
\`request_id\` \u2014 a blended seed lands between the clusters
|
|
10210
|
-
neither.
|
|
10595
|
+
\`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
|
|
10211
10596
|
|
|
10212
10597
|
|
|
10213
10598
|
**Parameter notes**
|
|
10214
10599
|
- \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
|
|
10215
10600
|
ask (same live job, no double launch); NEW for a changed ask. Derive from ask
|
|
10216
10601
|
+ archetype + date: \`gyms-dallas-2026-07-28\`.
|
|
10217
|
-
- Never lower \`min_ai_score\`
|
|
10218
|
-
|
|
10219
|
-
- \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
|
|
10220
|
-
|
|
10602
|
+
- Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
|
|
10603
|
+
the AI just scored as junk.
|
|
10604
|
+
- \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
|
|
10605
|
+
don't hammer).
|
|
10221
10606
|
|
|
10222
10607
|
**Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
|
|
10223
|
-
zero delivered gets a cause and a next move (
|
|
10608
|
+
zero delivered gets a cause and a next move (RENDERING).
|
|
10224
10609
|
|
|
10225
10610
|
---
|
|
10226
10611
|
|
|
@@ -17382,8 +17767,16 @@ function townName(value) {
|
|
|
17382
17767
|
}
|
|
17383
17768
|
function cityHintCore(cityHint) {
|
|
17384
17769
|
const head = cityHint.split(",")[0] ?? "";
|
|
17385
|
-
const
|
|
17386
|
-
|
|
17770
|
+
const segments = cityHint.split(",").map((part) => part.trim()).filter(Boolean);
|
|
17771
|
+
for (let take = segments.length; take > 0; take -= 1) {
|
|
17772
|
+
const candidate = segments.slice(0, take).join(" ");
|
|
17773
|
+
const expanded = expandAlias(candidate);
|
|
17774
|
+
if (expanded !== candidate) {
|
|
17775
|
+
return { name: townName(expanded), isCity: true };
|
|
17776
|
+
}
|
|
17777
|
+
}
|
|
17778
|
+
const prefixed = ADMIN_PREFIX.test(normalizeGeo(head));
|
|
17779
|
+
return { name: townName(head), isCity: prefixed };
|
|
17387
17780
|
}
|
|
17388
17781
|
function stateFieldOf(lead) {
|
|
17389
17782
|
const state = lead?.location?.state;
|
|
@@ -19167,6 +19560,174 @@ var researchLeadByNameFuzzy = {
|
|
|
19167
19560
|
}
|
|
19168
19561
|
};
|
|
19169
19562
|
|
|
19563
|
+
// ../core/dist/composite/set-qualification-questions.js
|
|
19564
|
+
var MAX_QUESTIONS = 5;
|
|
19565
|
+
var ESTIMATIVE_MARKERS = [
|
|
19566
|
+
"is the company likely to",
|
|
19567
|
+
"l'entreprise est-elle susceptible",
|
|
19568
|
+
"l\u2019entreprise est-elle susceptible"
|
|
19569
|
+
];
|
|
19570
|
+
function formWarnings(questions) {
|
|
19571
|
+
const out = [];
|
|
19572
|
+
for (const q of questions) {
|
|
19573
|
+
const low = q.trim().toLowerCase();
|
|
19574
|
+
if (!ESTIMATIVE_MARKERS.some((m) => low.startsWith(m))) {
|
|
19575
|
+
out.push(`"${q}" is not in the estimative form. Leadbay scores from public text it cannot verify, so this will mark most leads no. Rewrite it to start "Is the company likely to ..." / "L'entreprise est-elle susceptible de ...".`);
|
|
19576
|
+
}
|
|
19577
|
+
if (q.length > 120) {
|
|
19578
|
+
out.push(`"${q.slice(0, 60)}\u2026" is ${q.length} chars; keep a question under 120.`);
|
|
19579
|
+
}
|
|
19580
|
+
}
|
|
19581
|
+
return out;
|
|
19582
|
+
}
|
|
19583
|
+
var setQualificationQuestions = {
|
|
19584
|
+
name: "leadbay_set_qualification_questions",
|
|
19585
|
+
annotations: {
|
|
19586
|
+
title: "Modify the org's qualification questions",
|
|
19587
|
+
readOnlyHint: false,
|
|
19588
|
+
destructiveHint: true,
|
|
19589
|
+
idempotentHint: false,
|
|
19590
|
+
openWorldHint: true
|
|
19591
|
+
},
|
|
19592
|
+
description: leadbay_set_qualification_questions,
|
|
19593
|
+
write: true,
|
|
19594
|
+
inputSchema: {
|
|
19595
|
+
type: "object",
|
|
19596
|
+
properties: {
|
|
19597
|
+
questions: {
|
|
19598
|
+
type: "array",
|
|
19599
|
+
items: { type: "string", maxLength: 255 },
|
|
19600
|
+
description: `Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove. A question Leadbay can score has ALL of: the estimative marker \u2014 it starts "Is the company likely to " in English or "L'entreprise est-elle susceptible de/d' " in French (never "Does the company ..." or a bare "L'entreprise est-elle <X> ?", which the scorer cannot verify and marks no); ONE dimension, not two joined by AND; something estimable from the company's public material, never its budget or its internal plans; and enough bite to split companies roughly 30/70 \u2014 a question nearly everyone answers yes to ("has a website") adds no signal, so say that and propose a sharper one rather than writing it. Max 120 chars, in the user's language. Never name a specific company in a question. Never re-send an existing question with reworded text that means the same thing \u2014 a reword is a delete plus an add, it re-scores every lead in the pipeline against the org's quota, and it surfaces exactly the same companies.`
|
|
19601
|
+
},
|
|
19602
|
+
add: {
|
|
19603
|
+
type: "array",
|
|
19604
|
+
items: { type: "string", maxLength: 255 },
|
|
19605
|
+
description: `Questions to append to the current list (deduped). Mutually exclusive with \`questions\`. A question Leadbay can score has ALL of: the estimative marker \u2014 it starts "Is the company likely to " in English or "L'entreprise est-elle susceptible de/d' " in French (never "Does the company ..." or a bare "L'entreprise est-elle <X> ?", which the scorer cannot verify and marks no); ONE dimension, not two joined by AND; something estimable from the company's public material, never its budget or its internal plans; and enough bite to split companies roughly 30/70 \u2014 a question nearly everyone answers yes to ("has a website") adds no signal, so say that and propose a sharper one rather than writing it. Max 120 chars, in the user's language. Never name a specific company in a question. Read leadbay_get_qualification_questions first: skip anything an existing question already tests, and anything the lens already filters by sector, headcount or territory.`
|
|
19606
|
+
},
|
|
19607
|
+
remove: {
|
|
19608
|
+
type: "array",
|
|
19609
|
+
items: { type: "string" },
|
|
19610
|
+
description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
|
|
19611
|
+
},
|
|
19612
|
+
confirm: {
|
|
19613
|
+
type: "boolean",
|
|
19614
|
+
description: "Required whenever the change DROPS ANY existing question \u2014 including a same-count swap or a `questions` replacement that omits a current question, not only when the list gets shorter (removing a question changes how every lead is scored). Without it, such a change is previewed and not applied. Pure additions never need confirm."
|
|
19615
|
+
}
|
|
19616
|
+
},
|
|
19617
|
+
additionalProperties: false
|
|
19618
|
+
},
|
|
19619
|
+
outputSchema: {
|
|
19620
|
+
type: "object",
|
|
19621
|
+
properties: {
|
|
19622
|
+
qualification_questions: {
|
|
19623
|
+
type: "array",
|
|
19624
|
+
description: "The questions AFTER the change. Each: {question}.",
|
|
19625
|
+
items: { type: "object" }
|
|
19626
|
+
},
|
|
19627
|
+
count: { type: "number" },
|
|
19628
|
+
previous_count: { type: "number" },
|
|
19629
|
+
changed: {
|
|
19630
|
+
type: "boolean",
|
|
19631
|
+
description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
|
|
19632
|
+
},
|
|
19633
|
+
region: { type: "string" },
|
|
19634
|
+
form_warnings: {
|
|
19635
|
+
type: "array",
|
|
19636
|
+
items: { type: "string" },
|
|
19637
|
+
description: "Present when a written question will score poorly \u2014 not in the estimative 'Is the company likely to ...' form, or over 120 chars. The change WAS applied; tell the user and offer the rewrite."
|
|
19638
|
+
},
|
|
19639
|
+
hint: {
|
|
19640
|
+
type: "string",
|
|
19641
|
+
description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
|
|
19642
|
+
},
|
|
19643
|
+
_meta: { type: "object" }
|
|
19644
|
+
},
|
|
19645
|
+
required: ["qualification_questions", "count", "changed"]
|
|
19646
|
+
},
|
|
19647
|
+
execute: async (client, params, ctx) => {
|
|
19648
|
+
const hasSet = Array.isArray(params.questions);
|
|
19649
|
+
const hasAdd = Array.isArray(params.add) && params.add.length > 0;
|
|
19650
|
+
const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
|
|
19651
|
+
if (hasSet && (hasAdd || hasRemove)) {
|
|
19652
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_BAD_ARGS", "`questions` (full replace) is mutually exclusive with add/remove", "Pass EITHER `questions` (the full new list) OR `add`/`remove`, not both.", "POST /organizations/{orgId}");
|
|
19653
|
+
}
|
|
19654
|
+
if (!hasSet && !hasAdd && !hasRemove) {
|
|
19655
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_NO_CHANGE", "nothing to change \u2014 pass `questions`, `add`, or `remove`", "Provide a full `questions` list, or `add`/`remove` entries.", "POST /organizations/{orgId}");
|
|
19656
|
+
}
|
|
19657
|
+
const orgId = await client.resolveOrgId();
|
|
19658
|
+
const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
|
|
19659
|
+
const currentQs = (current ?? []).map((q) => q.question);
|
|
19660
|
+
const norm = (s) => s.trim();
|
|
19661
|
+
let next;
|
|
19662
|
+
if (hasSet) {
|
|
19663
|
+
next = params.questions.map(norm).filter((s) => s.length > 0);
|
|
19664
|
+
} else {
|
|
19665
|
+
next = [...currentQs];
|
|
19666
|
+
if (hasRemove) {
|
|
19667
|
+
const drop = new Set(params.remove.map(norm));
|
|
19668
|
+
next = next.filter((q) => !drop.has(norm(q)));
|
|
19669
|
+
}
|
|
19670
|
+
if (hasAdd) {
|
|
19671
|
+
const seen2 = new Set(next.map(norm));
|
|
19672
|
+
for (const q of params.add.map(norm)) {
|
|
19673
|
+
if (q.length > 0 && !seen2.has(q)) {
|
|
19674
|
+
next.push(q);
|
|
19675
|
+
seen2.add(q);
|
|
19676
|
+
}
|
|
19677
|
+
}
|
|
19678
|
+
}
|
|
19679
|
+
}
|
|
19680
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19681
|
+
next = next.filter((q) => {
|
|
19682
|
+
const k = norm(q);
|
|
19683
|
+
if (seen.has(k))
|
|
19684
|
+
return false;
|
|
19685
|
+
seen.add(k);
|
|
19686
|
+
return true;
|
|
19687
|
+
});
|
|
19688
|
+
if (next.length > MAX_QUESTIONS) {
|
|
19689
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_LIMIT", `too many questions: ${next.length} (max ${MAX_QUESTIONS})`, `Leadbay allows at most ${MAX_QUESTIONS} qualification questions. Remove some first (pass fewer in \`questions\`, or use \`remove\`), then add.`, "POST /organizations/{orgId}");
|
|
19690
|
+
}
|
|
19691
|
+
const previousCount = currentQs.length;
|
|
19692
|
+
const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
|
|
19693
|
+
if (noChange) {
|
|
19694
|
+
return {
|
|
19695
|
+
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
19696
|
+
count: currentQs.length,
|
|
19697
|
+
previous_count: previousCount,
|
|
19698
|
+
changed: false,
|
|
19699
|
+
region: client.region,
|
|
19700
|
+
hint: "No change \u2014 the resulting list is identical to the current one. Pass different `add`/`remove` entries, or call leadbay_get_qualification_questions to review the current questions."
|
|
19701
|
+
};
|
|
19702
|
+
}
|
|
19703
|
+
const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
|
|
19704
|
+
if (removed.length > 0 && params.confirm !== true) {
|
|
19705
|
+
return {
|
|
19706
|
+
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
19707
|
+
count: currentQs.length,
|
|
19708
|
+
previous_count: previousCount,
|
|
19709
|
+
changed: false,
|
|
19710
|
+
region: client.region,
|
|
19711
|
+
hint: `Re-call with confirm:true to apply. This would remove ${removed.length} question(s): ${removed.map((q) => `"${q}"`).join(", ")}. Removing a question changes how every lead is scored.`
|
|
19712
|
+
};
|
|
19713
|
+
}
|
|
19714
|
+
await client.requestVoid("POST", `/organizations/${orgId}`, {
|
|
19715
|
+
ai_agent_lead_questions: next
|
|
19716
|
+
});
|
|
19717
|
+
client.invalidateTasteProfile();
|
|
19718
|
+
const written = hasSet ? next : (params.add ?? []).map(norm).filter((q) => next.includes(q));
|
|
19719
|
+
const warnings = formWarnings(written);
|
|
19720
|
+
return {
|
|
19721
|
+
qualification_questions: next.map((q) => ({ question: q })),
|
|
19722
|
+
count: next.length,
|
|
19723
|
+
previous_count: previousCount,
|
|
19724
|
+
changed: true,
|
|
19725
|
+
region: client.region,
|
|
19726
|
+
...warnings.length > 0 ? { form_warnings: warnings } : {}
|
|
19727
|
+
};
|
|
19728
|
+
}
|
|
19729
|
+
};
|
|
19730
|
+
|
|
19170
19731
|
// ../core/dist/composite/get-qualification-questions.js
|
|
19171
19732
|
var getQualificationQuestions = {
|
|
19172
19733
|
name: "leadbay_get_qualification_questions",
|
|
@@ -19195,6 +19756,13 @@ var getQualificationQuestions = {
|
|
|
19195
19756
|
type: "number",
|
|
19196
19757
|
description: "Number of qualification questions configured."
|
|
19197
19758
|
},
|
|
19759
|
+
ideal_buyer_profile: {
|
|
19760
|
+
description: "The org's Ideal Buyer Profile {summary, key_characteristics, anti_patterns}, or null when none is configured. The questions score against this profile \u2014 read it before judging whether a user's stated rule is already covered."
|
|
19761
|
+
},
|
|
19762
|
+
targeting_prompt: {
|
|
19763
|
+
type: ["string", "null"],
|
|
19764
|
+
description: "The org's free-text targeting prompt (user_prompt) the AI agent follows, or null when unset. Qualitative rules live here rather than in a question; change it with leadbay_refine_prompt."
|
|
19765
|
+
},
|
|
19198
19766
|
is_admin: {
|
|
19199
19767
|
type: "boolean",
|
|
19200
19768
|
description: "Whether the current bearer-token holder is an org admin. Admins can modify the questions via leadbay_set_qualification_questions."
|
|
@@ -19213,11 +19781,21 @@ var getQualificationQuestions = {
|
|
|
19213
19781
|
const isAdmin = me?.admin ?? false;
|
|
19214
19782
|
const orgId = me?.organization?.id ?? await client.resolveOrgId();
|
|
19215
19783
|
const questions = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`) ?? [];
|
|
19784
|
+
const [ibpResult, promptResult] = await Promise.allSettled([
|
|
19785
|
+
client.request("GET", `/organizations/${orgId}/ideal_buyer_profile`),
|
|
19786
|
+
client.request("GET", `/organizations/${orgId}/user_prompt`)
|
|
19787
|
+
]);
|
|
19788
|
+
const ibp = ibpResult.status === "fulfilled" ? ibpResult.value ?? null : null;
|
|
19789
|
+
const targetingPrompt = promptResult.status === "fulfilled" ? promptResult.value?.prompt ?? null : null;
|
|
19216
19790
|
let hint;
|
|
19217
|
-
if (questions.length
|
|
19218
|
-
hint =
|
|
19791
|
+
if (questions.length >= MAX_QUESTIONS && isAdmin) {
|
|
19792
|
+
hint = `${questions.length} of ${MAX_QUESTIONS} question slots are used \u2014 the set is FULL. An addition is a SWAP: tell the user the set is full, list these ${questions.length} and let THEM choose which one goes, then call leadbay_set_qualification_questions with confirm:true. Never pre-pick the one to drop.`;
|
|
19793
|
+
} else if (questions.length >= MAX_QUESTIONS) {
|
|
19794
|
+
hint = `${questions.length} of ${MAX_QUESTIONS} question slots are used \u2014 the set is FULL. Changing it means dropping one, and that is an org-admin action. Tell the user which question they would need an admin to swap out.`;
|
|
19795
|
+
} else if (questions.length === 0) {
|
|
19796
|
+
hint = `No qualification questions configured \u2014 every lead is scored on firmographics alone. Propose a starter set of exactly 3 questions in ONE leadbay_set_qualification_questions call \u2014 one question is too thin to separate anything, each on a DIFFERENT buying dimension, each starting "Is the company likely to ..." / "L'entreprise est-elle susceptible de ...", and none restating a sector or size the lens already filters on. Get the user's yes first.`;
|
|
19219
19797
|
} else if (isAdmin) {
|
|
19220
|
-
hint =
|
|
19798
|
+
hint = `You're an org admin \u2014 use leadbay_set_qualification_questions to add, remove, or replace these questions. ${MAX_QUESTIONS - questions.length} of ${MAX_QUESTIONS} slots are still free.`;
|
|
19221
19799
|
}
|
|
19222
19800
|
return {
|
|
19223
19801
|
qualification_questions: questions.map((q) => ({
|
|
@@ -19226,6 +19804,12 @@ var getQualificationQuestions = {
|
|
|
19226
19804
|
lang: q.lang
|
|
19227
19805
|
})),
|
|
19228
19806
|
count: questions.length,
|
|
19807
|
+
ideal_buyer_profile: ibp ? {
|
|
19808
|
+
summary: ibp.summary,
|
|
19809
|
+
key_characteristics: ibp.key_characteristics,
|
|
19810
|
+
anti_patterns: ibp.anti_patterns
|
|
19811
|
+
} : null,
|
|
19812
|
+
targeting_prompt: targetingPrompt,
|
|
19229
19813
|
is_admin: isAdmin,
|
|
19230
19814
|
region: client.region,
|
|
19231
19815
|
...hint ? { hint } : {}
|
|
@@ -19450,148 +20034,6 @@ var gettingStarted = {
|
|
|
19450
20034
|
}
|
|
19451
20035
|
};
|
|
19452
20036
|
|
|
19453
|
-
// ../core/dist/composite/set-qualification-questions.js
|
|
19454
|
-
var setQualificationQuestions = {
|
|
19455
|
-
name: "leadbay_set_qualification_questions",
|
|
19456
|
-
annotations: {
|
|
19457
|
-
title: "Modify the org's qualification questions",
|
|
19458
|
-
readOnlyHint: false,
|
|
19459
|
-
destructiveHint: true,
|
|
19460
|
-
idempotentHint: false,
|
|
19461
|
-
openWorldHint: true
|
|
19462
|
-
},
|
|
19463
|
-
description: leadbay_set_qualification_questions,
|
|
19464
|
-
write: true,
|
|
19465
|
-
inputSchema: {
|
|
19466
|
-
type: "object",
|
|
19467
|
-
properties: {
|
|
19468
|
-
questions: {
|
|
19469
|
-
type: "array",
|
|
19470
|
-
items: { type: "string", maxLength: 255 },
|
|
19471
|
-
description: "Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove."
|
|
19472
|
-
},
|
|
19473
|
-
add: {
|
|
19474
|
-
type: "array",
|
|
19475
|
-
items: { type: "string", maxLength: 255 },
|
|
19476
|
-
description: "Questions to append to the current list (deduped). Mutually exclusive with `questions`."
|
|
19477
|
-
},
|
|
19478
|
-
remove: {
|
|
19479
|
-
type: "array",
|
|
19480
|
-
items: { type: "string" },
|
|
19481
|
-
description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
|
|
19482
|
-
},
|
|
19483
|
-
confirm: {
|
|
19484
|
-
type: "boolean",
|
|
19485
|
-
description: "Required whenever the change DROPS ANY existing question \u2014 including a same-count swap or a `questions` replacement that omits a current question, not only when the list gets shorter (removing a question changes how every lead is scored). Without it, such a change is previewed and not applied. Pure additions never need confirm."
|
|
19486
|
-
}
|
|
19487
|
-
},
|
|
19488
|
-
additionalProperties: false
|
|
19489
|
-
},
|
|
19490
|
-
outputSchema: {
|
|
19491
|
-
type: "object",
|
|
19492
|
-
properties: {
|
|
19493
|
-
qualification_questions: {
|
|
19494
|
-
type: "array",
|
|
19495
|
-
description: "The questions AFTER the change. Each: {question}.",
|
|
19496
|
-
items: { type: "object" }
|
|
19497
|
-
},
|
|
19498
|
-
count: { type: "number" },
|
|
19499
|
-
previous_count: { type: "number" },
|
|
19500
|
-
changed: {
|
|
19501
|
-
type: "boolean",
|
|
19502
|
-
description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
|
|
19503
|
-
},
|
|
19504
|
-
region: { type: "string" },
|
|
19505
|
-
hint: {
|
|
19506
|
-
type: "string",
|
|
19507
|
-
description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
|
|
19508
|
-
},
|
|
19509
|
-
_meta: { type: "object" }
|
|
19510
|
-
},
|
|
19511
|
-
required: ["qualification_questions", "count", "changed"]
|
|
19512
|
-
},
|
|
19513
|
-
execute: async (client, params, ctx) => {
|
|
19514
|
-
const hasSet = Array.isArray(params.questions);
|
|
19515
|
-
const hasAdd = Array.isArray(params.add) && params.add.length > 0;
|
|
19516
|
-
const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
|
|
19517
|
-
if (hasSet && (hasAdd || hasRemove)) {
|
|
19518
|
-
throw client.makeError("QUALIFICATION_QUESTIONS_BAD_ARGS", "`questions` (full replace) is mutually exclusive with add/remove", "Pass EITHER `questions` (the full new list) OR `add`/`remove`, not both.", "POST /organizations/{orgId}");
|
|
19519
|
-
}
|
|
19520
|
-
if (!hasSet && !hasAdd && !hasRemove) {
|
|
19521
|
-
throw client.makeError("QUALIFICATION_QUESTIONS_NO_CHANGE", "nothing to change \u2014 pass `questions`, `add`, or `remove`", "Provide a full `questions` list, or `add`/`remove` entries.", "POST /organizations/{orgId}");
|
|
19522
|
-
}
|
|
19523
|
-
const orgId = await client.resolveOrgId();
|
|
19524
|
-
const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
|
|
19525
|
-
const currentQs = (current ?? []).map((q) => q.question);
|
|
19526
|
-
const norm = (s) => s.trim();
|
|
19527
|
-
let next;
|
|
19528
|
-
if (hasSet) {
|
|
19529
|
-
next = params.questions.map(norm).filter((s) => s.length > 0);
|
|
19530
|
-
} else {
|
|
19531
|
-
next = [...currentQs];
|
|
19532
|
-
if (hasRemove) {
|
|
19533
|
-
const drop = new Set(params.remove.map(norm));
|
|
19534
|
-
next = next.filter((q) => !drop.has(norm(q)));
|
|
19535
|
-
}
|
|
19536
|
-
if (hasAdd) {
|
|
19537
|
-
const seen2 = new Set(next.map(norm));
|
|
19538
|
-
for (const q of params.add.map(norm)) {
|
|
19539
|
-
if (q.length > 0 && !seen2.has(q)) {
|
|
19540
|
-
next.push(q);
|
|
19541
|
-
seen2.add(q);
|
|
19542
|
-
}
|
|
19543
|
-
}
|
|
19544
|
-
}
|
|
19545
|
-
}
|
|
19546
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19547
|
-
next = next.filter((q) => {
|
|
19548
|
-
const k = norm(q);
|
|
19549
|
-
if (seen.has(k))
|
|
19550
|
-
return false;
|
|
19551
|
-
seen.add(k);
|
|
19552
|
-
return true;
|
|
19553
|
-
});
|
|
19554
|
-
const MAX_QUESTIONS = 5;
|
|
19555
|
-
if (next.length > MAX_QUESTIONS) {
|
|
19556
|
-
throw client.makeError("QUALIFICATION_QUESTIONS_LIMIT", `too many questions: ${next.length} (max ${MAX_QUESTIONS})`, `Leadbay allows at most ${MAX_QUESTIONS} qualification questions. Remove some first (pass fewer in \`questions\`, or use \`remove\`), then add.`, "POST /organizations/{orgId}");
|
|
19557
|
-
}
|
|
19558
|
-
const previousCount = currentQs.length;
|
|
19559
|
-
const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
|
|
19560
|
-
if (noChange) {
|
|
19561
|
-
return {
|
|
19562
|
-
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
19563
|
-
count: currentQs.length,
|
|
19564
|
-
previous_count: previousCount,
|
|
19565
|
-
changed: false,
|
|
19566
|
-
region: client.region,
|
|
19567
|
-
hint: "No change \u2014 the resulting list is identical to the current one. Pass different `add`/`remove` entries, or call leadbay_get_qualification_questions to review the current questions."
|
|
19568
|
-
};
|
|
19569
|
-
}
|
|
19570
|
-
const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
|
|
19571
|
-
if (removed.length > 0 && params.confirm !== true) {
|
|
19572
|
-
return {
|
|
19573
|
-
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
19574
|
-
count: currentQs.length,
|
|
19575
|
-
previous_count: previousCount,
|
|
19576
|
-
changed: false,
|
|
19577
|
-
region: client.region,
|
|
19578
|
-
hint: `Re-call with confirm:true to apply. This would remove ${removed.length} question(s): ${removed.map((q) => `"${q}"`).join(", ")}. Removing a question changes how every lead is scored.`
|
|
19579
|
-
};
|
|
19580
|
-
}
|
|
19581
|
-
await client.requestVoid("POST", `/organizations/${orgId}`, {
|
|
19582
|
-
ai_agent_lead_questions: next
|
|
19583
|
-
});
|
|
19584
|
-
client.invalidateTasteProfile();
|
|
19585
|
-
return {
|
|
19586
|
-
qualification_questions: next.map((q) => ({ question: q })),
|
|
19587
|
-
count: next.length,
|
|
19588
|
-
previous_count: previousCount,
|
|
19589
|
-
changed: true,
|
|
19590
|
-
region: client.region
|
|
19591
|
-
};
|
|
19592
|
-
}
|
|
19593
|
-
};
|
|
19594
|
-
|
|
19595
20037
|
// ../core/dist/composite/get-lead-custom-fields.js
|
|
19596
20038
|
var getLeadCustomFields = {
|
|
19597
20039
|
name: "leadbay_get_lead_custom_fields",
|
|
@@ -24546,11 +24988,11 @@ var findNewLeads = {
|
|
|
24546
24988
|
properties: {
|
|
24547
24989
|
query: {
|
|
24548
24990
|
type: "string",
|
|
24549
|
-
description: "Natural-language ICP ask. Matches topic VOCABULARY \u2014 can surface vendors of a product as easily as buyers of it. Prefer example_lead
|
|
24991
|
+
description: "Natural-language ICP ask. Matches topic VOCABULARY \u2014 can surface vendors of a product as easily as buyers of it. Prefer example_lead. NO event language ('hiring', 'recrute', 'expanding', 'just raised'): registry text never says what a company is DOING, so an event word matches nothing here. Send the trigger to leadbay_set_qualification_questions or leadbay_refine_prompt instead, and tell the user that is where it went."
|
|
24550
24992
|
},
|
|
24551
24993
|
example_lead: {
|
|
24552
24994
|
type: "object",
|
|
24553
|
-
description: "A FICTIONAL typical ideal customer used as a look-alike seed \u2014 the highest-leverage input. Put everything in `description` (registry 'About Us' style, what the company IS); leave `name` unset (a distinctive invented name pulls matches toward name-lookalikes).",
|
|
24995
|
+
description: "A FICTIONAL typical ideal customer used as a look-alike seed \u2014 the highest-leverage input. Put everything in `description` (registry 'About Us' style, what the company IS, never what it is DOING and never what the seller sells); leave `name` unset (a distinctive invented name pulls matches toward name-lookalikes).",
|
|
24554
24996
|
properties: {
|
|
24555
24997
|
name: { type: "string" },
|
|
24556
24998
|
description: { type: "string" },
|
|
@@ -27764,6 +28206,7 @@ var FRICTION = `Problem reports: when the user asks you to report a Leadbay prob
|
|
|
27764
28206
|
var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
|
|
27765
28207
|
var QUOTA_REFRESH = `Show the refreshed quota AFTER a quota-using action only when it matters: the user asks where they now stand, the action stopped on an exhausted window, or a top-up the user confirmed landed. Otherwise report the result and say nothing about quota. When you do show it, wait for genuine completion \u2014 leadbay_bulk_enrich_status reports the job done (all_done, OR a plateau you've decided is terminal: overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 then call leadbay_account_status once and render the per-window %/$ gauge (Daily/Weekly/Monthly) it returns. leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
|
|
27766
28208
|
var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.`;
|
|
28209
|
+
var STATED_RULES = `Stated fit rules live in the account, not in your memory: when the user tells you what makes a lead good or bad ("\xE9carte les soci\xE9t\xE9s liquid\xE9es", "I don't want associations", "our best customers run their own maintenance crews", "the leads aren't relevant"), that is a change to their Leadbay settings. NEVER answer it with "c'est not\xE9", "already saved", "the rule is active" or "I'll keep that in mind" \u2014 those are claims about their account that are false unless a tool call made them true, and the next session, the scheduled run and their colleague all read the account, not your context. Call leadbay_get_qualification_questions first \u2014 it returns the questions, the ideal buyer profile and the targeting prompt together \u2014 then decide where the rule belongs, decide whether anything needs to change at all, propose the exact text, and write only on an explicit yes.`;
|
|
27767
28210
|
var TRANSIENT_401 = `Transient 401s (DO NOT narrate as an auth problem): Leadbay bearer tokens do NOT expire on a timer, and the client already auto-retried this call once before surfacing it. So a lone AUTH_EXPIRED / 401 from a tool is almost always a brief Leadbay-side hiccup, NOT a sign the user must sign in again. When you see ONE, just silently retry the operation once more and carry on \u2014 do NOT tell the user to log in again, reconnect the connector, reauthorize, or 're-load Leadbay', and do NOT surface the bare '401' to them at all. Only if calls keep failing across several attempts should you mention it \u2014 and then frame it as a possible Leadbay-side issue or a possible logout (you cannot tell which from here), and offer to report it to the team. Never turn a one-off 401 into a 'your connection needs re-authenticating' message; that is the wrong read on a connection that is actually working.`;
|
|
27768
28211
|
var TRIGGERED_BY = `Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a non-empty \`_triggered_by\` argument \u2014 the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on \u2014 the recalled directive, the schedule's intent, or the original request being retried \u2014 so the value is always a real, auditable trace. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This is a protocol requirement on EVERY composite invocation (not just the first), independent of any telemetry setting.`;
|
|
27769
28212
|
var VERIFICATION = `After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.`;
|
|
@@ -27932,6 +28375,9 @@ function buildServerInstructions(exposed) {
|
|
|
27932
28375
|
}
|
|
27933
28376
|
parts.push(TRIGGERED_BY);
|
|
27934
28377
|
parts.push(MENTAL_MODEL);
|
|
28378
|
+
if (has("leadbay_get_qualification_questions")) {
|
|
28379
|
+
parts.push(STATED_RULES);
|
|
28380
|
+
}
|
|
27935
28381
|
if (has("leadbay_create_topup_link")) {
|
|
27936
28382
|
parts.push(QUOTA_TOPUP);
|
|
27937
28383
|
}
|
|
@@ -28862,7 +29308,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
28862
29308
|
}
|
|
28863
29309
|
|
|
28864
29310
|
// src/http-server.ts
|
|
28865
|
-
var VERSION = true ? "0.39.
|
|
29311
|
+
var VERSION = true ? "0.39.5" : "0.0.0-dev";
|
|
28866
29312
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
28867
29313
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
28868
29314
|
var logger = {
|