Show full chat transcript on leads, fix Chat Agent settings losing place on Drive connect
Seth+1: Claude Sonnet 5
41f3768
Chat-agent leads only showed their latest message in the lead detail
panel ("Their reply"), truncating the actual conversation. Added a
chat-agent-thread endpoint (reads the transcript already stored on
raw_webhook.messages) and load it automatically for these leads instead
of requiring a click that previously hit the Smartlead-specific thread
route and failed.
The Chat Agent settings page kicked back to the main Settings list after
connecting Google Drive. The OAuth callback always redirects to the bare
/settings URL, but which settings sub-panel was open only lived in local
component state, not the URL — so landing back on /settings forgot Chat
Agent was open. Settings.jsx now checks for the Drive-callback query
params on load and reopens straight into the Chat Agent panel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix chat agent web search, greeting resume, and lead capture gaps
Seth+1: Claude Sonnet 5
b5da8dd
Competitor comparison tables defaulted straight to "Requires confirmation"
because the agentic prompt never told the model to actually use its
search_the_web tool for the comparison — only that the tool existed.
Now the model is instructed to search before falling back, and to label
web-sourced cells as unverified public info rather than a settled claim.
The widget's opening greeting silently disappeared on any repeat open
within the same browser (existing localStorage session skipped the
greeting fetch entirely, with no history shown either). Added a session-
resume endpoint so reopening replays prior messages, or shows the
greeting if nothing was exchanged yet.
Chat-agent-sourced leads were linked into Contacts the same way as
Smartlead reply-imports, which the enrichment "Fetch" gate explicitly
excludes — and no extraction logic existed at all for company name, only
visitor name. Chat-agent leads now get form-style (enrichable) contacts,
and company name is extracted from the conv…
Skip email generation for no-email contacts, make Stop generation more robust
Seth+1: Claude Sonnet 5
2d73657
Contacts with no email address (LinkedIn-only prospects) were being run
through generate_email_sequence anyway, which always raised and got
reported as a "failed" contact needing retry — even though retrying can
never succeed without an email on file. Now skipped cleanly with no
error, leaving LinkedIn generation for them unaffected.
Stop generation's cancel request had no failure handling, so a dropped
or errored POST left the button stuck on "Stopping…" with generation
still running server-side. Added a retry + surfaced error, plus a
"Finishing current contact…" label after a few seconds so a genuinely
in-progress stop (bounded by the current contact's in-flight GPT call)
doesn't read as frozen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Turn campaign timezone field into a searchable city/country picker
Seth+1: Claude Sonnet 5
0d1ff1d
Free-text timezone entry let users type non-IANA names (e.g. "India")
that silently failed open and disabled the business-hours restriction
with no feedback. Replaced it with a type-ahead combobox backed by a
curated city/country list plus the full IANA zone list as fallback, so
only a real timezone id can be committed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LinkedIn connects/DMs are now throttled per connected account against
conservative daily/weekly caps (rolling windows, randomized jitter) enforced
by a new background scheduler so delivery keeps progressing without a
browser tab open. Email sending gets per-campaign timezone/business-hours/
interval controls set during campaign setup. Content generation can now be
stopped mid-run and resumed later without restarting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add dashboard activity/lead/deal stat boxes and backfill Contacts/Companies ownership scoping
Seth+1: Claude Sonnet 5
de29580
- Add Calls, Visits, New leads, Deals, and Closings stat boxes to the
Sales Dashboard, respecting the existing team/date-range filters.
- Add a tenant-wide GET /api/activities endpoint (previously only
available per-contact) and bump /api/leads' page-size cap to match
Deals, so the new dashboard counts aren't silently truncated.
- Give Contact and Company an owner_user_id column and apply the same
team-manager visibility scoping already used by Leads/Deals to their
list, detail, and patch routes — these had no ownership concept at
all before, so every tenant member could see everyone's contacts and
companies regardless of team.
- Add an Owner avatar + reassignment dropdown to Contacts.jsx and
Companies.jsx (factored into a shared OwnerCell component), matching
the admin-can-assign-anyone / member-can-only-self-claim rules the
backend now enforces.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Allow LinkedIn generation for contacts with no email but a LinkedIn URL
Seth+1: Claude Sonnet 5
56eda2f
Apollo CSV exports frequently can't verify an email but still include a
valid LinkedIn profile URL — generate_linkedin_sequence() was hard-
requiring an email and failing these contacts entirely, even though
LinkedIn sending never uses email (it already resolves the profile from
the LinkedIn URL at send time). Also fixes contact field extraction
(both email and LinkedIn generation) to recognize Apollo's actual
column names like "Company Name" instead of only "Company", which was
silently leaving company blank for every Apollo-sourced contact.
- gpt_service.py: case-insensitive field lookup with the same alias
list main.py already uses elsewhere; LinkedIn generation now only
requires a name, email, or LinkedIn URL — not specifically an email.
Email generation still requires an email (needed to actually send).
- Give generate_email_sequence the same typed generation_failed marker
treatment as LinkedIn instead of silently returning fake fallback
email content on error.
- main.p…
Fix LinkedIn campaign generation reliability, quality, and persistence
Seth+1: Claude Sonnet 5
810c33d
- Stop silently swallowing generation errors as fake generic fallback
content; retry once on transient OpenAI failures, then surface a
typed failure instead of writing fabricated sequence rows.
- Fix step-order/message-numbering bugs so LinkedIn steps keep the
wizard's configured order and messages aren't mislabeled by list
position.
- Enforce the LinkedIn connection-note character cap server-side and
tighten the prompt to require concrete personalization and ban
generic filler openers.
- Give campaigns a real backend record (UploadedFile campaign_name/
status/updated_at + list/detail/meta-patch/delete routes) instead of
browser localStorage, which had a bug where a parse failure would
silently repopulate demo data over real campaigns.
- Reconcile the dashboard from the backend list, auto-save campaign
identity from the wizard on every step, and re-fetch the saved
sequence plan/prompts when reopening a campaign so content and
configuration survive closing and re…
Add "Add a contact" form to Leads and Deals detail panels
Seth+1: Claude Sonnet 5
1d7a01a
Mirrors the Companies page's existing "Add a contact" box via a new shared
AddContactForm component, using the generic POST /api/contacts route (rather
than the Companies-specific one) so it works even before a Lead/Deal's
company has synced into a Company row. Refreshes the associated-contacts
list automatically after a save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add sales teams: manager role, team assignment, and team-scoped data visibility
Seth+1: Claude Sonnet 5
67330bb
Managers head a team (Team.manager_user_id, TenantMembership.team_id) and see
their own plus their team's leads, deals, and Call/Visit activity; other
members see only their own. A shared _visible_owner_user_ids() helper gates
list_leads, list_deals, quarterly_recurring_metrics (which the Dashboard
reads from), and both activity-timeline endpoints. Leads gains an
owner_user_id column plus assignment UI (mirroring Deals, which already had
per-user ownership) since team-scoping needs something to scope by. Admins
manage teams and member-team assignment from a new Teams card in Settings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Call/Visit activity logging and timeline for contacts and companies
Seth+1: Claude Sonnet 5
41c19e3
New ContactActivity table tracks calls/visits with notes and author,
surfaced via Call/Visit icon buttons (with an inline note textarea)
everywhere a contact appears — Contacts table rows, associated-contacts
lists on Companies/Leads/Deals, and the contact identity header in each
detail panel. Adds a per-contact activity timeline and a company-level
rollup that aggregates activity across every contact sharing that company.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Type field (Existing/New) to Contacts/Companies/Leads/Deals, synced like Category
Seth+1: Claude Sonnet 5
fe7e466
Company gets a new type column, wired through the existing generic
COMPANY_DETAIL_PATCH_FIELDS propagation so it syncs bidirectionally across
Company/Contact/Lead/Deal exactly like Category. Companies page also gets a
Type filter, table column, and inline-add-row selector matching Category's
existing footprint there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add contact number field to Contacts/Companies, show associated contacts on Leads/Deals
Seth+1: Claude Sonnet 5
c40de6e
Adds a personal phone column on Contact/CrmLead and a synced company-level
phone on Company (using the existing company_phone/company_name_for_emails
style patch-body disambiguation), surfaced in the shared identity/company
editors everywhere they're used. Leads and Deals detail panels now also show
an "Associated contacts" list for the linked company, matching the Companies
page, via a new shared AssociatedContactsList component and a
GET /api/contacts/by-company endpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Category revert bug on Contacts page, generalize sync to all company fields
Seth+1: Claude Sonnet 5
4b1d42b
- Contacts.jsx built its own local companyDetails object for the
editor instead of passing raw_data straight through — that object
was missing a Category key entirely, so the field always read as
empty and every autosave round-trip re-synced the form from that
same Category-less object, snapping the dropdown back to "-".
- Generalized the previous Category-only propagation into full-field
propagation: editing industry, employees, revenue, website, city,
state, country, or category anywhere (Contact/Lead/Company/Deal)
now pushes to the Company row and every Contact/Lead sharing that
company name, not just Category.
- _sync_companies_from_contacts now also tops up any blank field on
an already-existing Company row from the richest matching contact,
fixing Company rows that got stuck sparse after an early sync —
never overwrites a field that already has a value, synced or
manually edited.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sync Category across Company/Contact/Lead, link contacts, add company-level lead conversion
Seth+1: Claude Sonnet 5
6a71a79
- Category is now a true company-level attribute: editing it from the
Company page, a Contact, a Lead, or a Deal linked to a contact
propagates to the Company row and every Contact/Lead sharing that
company name (case-insensitive), so it never drifts between the
four places it's editable.
- Fixed a gap where converting a contact to a lead didn't carry its
Category into the lead's company snapshot.
- Associated contacts in the Company detail panel are now clickable —
navigates to Contacts and opens that exact contact's detail panel
(via a short-lived ?contact= deep link, cleaned from the URL after
use).
- New "Convert to Leads" action on the Company detail panel, mirroring
the existing per-contact "Copy to Leads," but scoped to every
contact associated with that company in one action.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Companies page with Category field and contact sync
Seth+1: Claude Sonnet 5
c253255
- New Company entity (name, category A/B/C, industry, employees,
annual revenue, website, city/state/country) with routes to list,
create, patch, bulk-delete, and re-sync.
- Companies are associated with Contacts by case-insensitive name
match rather than a foreign key, so this is fully additive and
never touches the existing Contact/CrmLead/CrmDeal schema or
behavior. A Company row is auto-created for any distinct
Contact.company value that doesn't already have one, seeded from
the richest matching contact; manual edits are never overwritten
by later syncs.
- New Companies page (sidebar, between Contacts and Leads) built
from the same AppShell/MainTableWorkspace/SlideOverPanel
components as Leads, with a detail panel listing every associated
contact plus an inline "add a contact" form scoped to that company.
- Category dropdown (A/B/C) added to the shared CompanyDetailsEditor
used by Contacts, Leads, and Deals, so it's consistently available
wherever company …
Chat Agent: proactive website CTAs and per-conversation token usage tracking
Seth+1: Claude Sonnet 5
9889053
- Strengthen the Website Link Promotion skill section (and the code-level
CITATIONS instructions that produce the actual link) so the agent actively
surfaces a relevant indexed page as a call-to-action whenever one exists,
instead of only citing it as a passive afterthought.
- Track total LLM tokens spent on each reply (summed across every round of
the agentic tool-calling loop, or from Gemini's usage metadata on that
provider) and surface it in the admin Conversations tab: per message, per
conversation, and as an overall total across every chat for the tenant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chat Agent: render markdown tables properly, show web-search usage to admins, fix pricing leak
Seth+1: Claude Sonnet 5
f6dedbf
- Render GFM pipe tables as real <table> markup (with horizontal scroll) in the
Test Chat panel, Conversations transcript, and the embeddable widget, instead
of dumping raw "| a | b |" text into a narrow chat bubble.
- Track whether a reply actually used the search_the_web tool and surface a
"Used live web search" badge to admins (Test Chat + Conversations), so a
live-web-sourced answer is never mistaken for a knowledge-base answer.
- Fix a real pricing leak: a visitor comparison question ("are you cheaper than
X") caused the agent to recite actual EZOFIS pricing pulled from the tenant's
own indexed content, because the comparison-table instruction pushed the
model to fill every cell from retrieval, overriding the separate "never
disclose pricing" rule. Added a hard, code-level backstop that strips
dollar-amount/discount patterns out of retrieved chunk text before it ever
reaches a prompt, plus reinforced the comparison-table instruction itself.
- Make the Gemini fa…
Chat Agent: opt-in live web search (Gemini) with skill-editable guardrails + AI disclaimer
Seth+1: Claude Sonnet 5
9d44313
Generalizes the agent's web-lookup tool from competitor-only research into a
general search_the_web tool the agentic loop can reach for whenever the
knowledge base isn't enough, gated behind enable_web_search + a configured
Gemini key. Hard safety boundaries (never our own pricing/confidential info,
always framed as unverified) stay in code; "when it's worth searching"
guidance is now editable via the default skill's instructions. Also adds a
persistent "AI can make mistakes" disclaimer to both the embeddable widget
and the admin Test Chat panel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chat Agent: comprehensive EZOFIS default skill, ZIP export, Gemini fast indexing
Seth+1: Claude Sonnet 5
b5da702
- Replace the 3 small guardrail skills with one comprehensive default skill
authored from EZOFIS's real website-agent governance document: condensed
non-negotiable rules always injected via instructions, 10 topic references
searched on demand (keeps every live turn fast), 6 canned-response
templates, a real runnable compliance-checker script, and 8 eval cases
lifted from the document's own test scenarios. A migration auto-
consolidates any tenant whose skills are exactly the old 3 unmodified
defaults; tenants with custom skills are left untouched.
- Fix a real bug found while stress-testing the larger content: the eval
parser's regex was missing re.MULTILINE, so only the first eval of any
multi-eval skill ever parsed back on import.
- Elaborate each Skills section (References/Assets/Templates/Scripts/Evals)
in the Cards UI with its purpose and a worked example.
- Add a ZIP export (GET /api/chat-agent/skills/export-zip + Download ZIP
button) producing the exact ski…
Four upgrades to the Skills anatomy, aimed at making this a faithful demo of
Anthropic's Agent Skills format:
- Real YAML frontmatter (pyyaml) instead of a hand-rolled regex parser, which
broke on descriptions containing a colon/quote/comma — now round-trips
correctly via safe_dump/safe_load.
- New templates/ folder, a full sibling to assets/ throughout: schema,
migration, SKILL.md compile/parse, knowledge-base indexing/search, CRUD
routes, and a frontend section.
- A virtual file-browser view (Cards / File browser toggle in the Skills tab)
that renders the DB-backed skill anatomy as a real-looking
skills/<name>/{SKILL.md,references/,assets/,templates/,scripts/,evals/}
tree with a live preview pane, since there's no actual filesystem to browse
on this hosting.
- Admin-only sandboxed script execution: a real "Run" button that executes
Python in an isolated subprocess (secrets stripped from its environment,
timeout-bound, output capped). This is never reachable from …
Chat Agent: full Agent Skills library (SKILL.md anatomy)
Seth+1: Claude Sonnet 5
0bbe01f
Rebuilds the flat guardrail-string Skills feature into Anthropic's real
Agent Skills format: name + description + instructions (always injected
into the prompt), plus references/assets (indexed into the same
knowledge base the agentic search tool already searches), scripts
(stored/exported for documentation only — never executed, see
SKILLS-SECURITY.md), and evals (test cases graded by substring match or
an LLM judge). Skills export/import as real, portable SKILL.md files.
Also fixes a resilience gap found while testing live in the browser: an
indexing failure (e.g. the embeddings API being down) was crashing the
whole skill save with a 500 and losing the admin's text edits. The
skill's own fields now commit first, indexing runs in its own
try/except, and a failed index attempt rolls back cleanly so the next
save retries instead of getting stuck thinking it's already indexed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three concrete bugs found from a real transcript ("DMS vs Laserfiche"
question):
1. Hallucination: the model said "I couldn't find that in the knowledge
base, however based on general knowledge..." and then invented specific
competitor pricing. Closed the loophole explicitly in both the agentic
(OpenAI) and classic (Gemini) system prompts: an "I don't know" answer
must be the model's entire response, with no fabricated continuation --
it has no reliable general knowledge about this company's pricing,
specs, or competitors. Also nudged it to try more than one search angle
(own product + competitor name) before giving up on a comparison
question, rather than quitting after one narrow miss.
2. Repetition: the lead-capture "ask once, early" instruction relied on the
model remembering not to repeat itself, and it didn't. Now enforced in
code via already_asked_for_contact -- once any prior assistant turn
exists in history, the instruction is dropped from th…
Chat Agent: genuinely agentic RAG for OpenAI (tool-calling loop, not single-shot)
Seth+1: Claude Sonnet 5
eeb98fa
Replaces the old "always pre-retrieve top-6 chunks, then answer once" flow
(for the default OpenAI provider) with a real agentic loop: the model gets
a search_knowledge_base tool and decides per-turn whether to search at all,
can chain a second refined search for hard multi-part questions, and can
hint which source (Drive vs. website) is more likely to have the answer
(now a real source_filter on retrieval, not cosmetic).
Kept fast on purpose: runs with stream=True through the whole loop, so a
question that needs no search streams live starting on the very first
call (no added latency vs. the old design); tool-call rounds are silent
internally (local, in-process retrieval, no network) and only the round
that actually produces the answer is forwarded to the client. Bounded to
MAX_SEARCH_ROUNDS=2 (3 LLM calls worst case) so latency stays predictable
-- verified the cap is real by confirming `tools` is absent from the API
call on the forced final round, not just coincidentally unused.
S…
Fix potential indefinite hang fetching robots.txt (no timeout in stdlib)
Seth+1: Claude Sonnet 5
aafc74d
RobotFileParser.read() calls urllib.request.urlopen() with no timeout at
all. A server that accepts the connection but never responds (some WAFs
tarpit suspicious clients instead of actively rejecting them) would hang
this call forever, silently stalling the whole reindex_website background
task with no error ever surfaced -- website_status would just stay
"running" indefinitely.
_load_robots now fetches robots.txt through our own timeout+retry-protected
_get_with_retry (already used for every other request in this module) and
feeds the text to RobotFileParser.parse() instead of .read(), removing the
unbounded network call entirely.
Verified: robots.txt Allow/Disallow rules still parse correctly, and
urllib.request.urlopen is confirmed to never be called (patched to raise if
invoked).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Chat Agent panel fires config/skills/status/conversations in parallel
on first load (separate fetches in ChatAgentPanel.jsx). Each independently
calls get_or_create_config, which did a plain SELECT-then-INSERT with no
protection against concurrent callers: two requests could both see "no row
yet" and race to insert, and the loser crashed with a raw
sqlite3.IntegrityError (UNIQUE constraint failed: chat_agent_configs.
tenant_id) surfaced as a 500 -- exactly what showed up in the HF logs on a
fresh config (first load after a restart/tenant creation).
get_or_create_config now wraps the insert in a SAVEPOINT (db.begin_nested())
and falls back to re-reading the row on IntegrityError, so a lost race
returns the winner's row instead of crashing -- and since the loser no
longer calls ensure_default_skills again, the 3 default skills aren't
duplicated either.
Verified by reproducing the exact scenario: 4 real threads calling
get_or_create_config concurrently for a brand-new tenant now pro…
Surface real website-crawl failures instead of silently reporting fake success
Seth+1: Claude Sonnet 5
e35d2a3
Same bug class as the earlier Drive fix: if the homepage itself couldn't be
reached or came back as a bot-challenge/CAPTCHA interstitial (which we
already confirmed ezofis.com's SiteGround WAF does to automated clients),
crawl_website silently returned an empty list, and reindex_website reported
"success, 0 pages crawled" with no indication anything was wrong.
crawl_website now fetches the homepage first, outside the tolerant BFS loop,
and raises a clear RuntimeError if it's unreachable or is a bot-challenge
page. Individual sub-page failures deeper in the crawl are still tolerated
(a normal, acceptable partial result) — only a broken homepage fetch is
treated as a real failure, surfaced via reindex_website's existing error
handling into the Chat Agent status log.
Verified: a simulated WAF-block (the exact SiteGround captcha page observed
from ezofis.com) now marks the log status "error" with an actionable
message; a normal successful crawl still indexes correctly (regression
check).…
Add in-panel Test Chat tab for the Chat Agent
Seth+1: Claude Sonnet 5
63e197a
Adds a "Test Chat" tab to Settings -> Chat Agent so the admin can try the
chat experience without embedding the widget anywhere first. It talks to
the exact same public session/message endpoints the embedded widget uses
(not a separate/authenticated test path), so knowledge base retrieval,
skills, streaming, citation links, and soft lead capture all behave exactly
as they would for a real visitor -- including creating a real CRM lead if a
name/email is shared during the test.
Verified end-to-end in-browser with a real seeded session: tab renders,
message send/receive round-trips through the streaming endpoint, citation
links render safely, and the resulting session/lead show up correctly in
the Conversations tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Surface real Drive API errors instead of silently reporting fake success
Seth+1: Claude Sonnet 5
a12eba7
list_folder_files_recursive logged a warning and returned an empty list on
any files.list failure (e.g. Drive API not enabled, revoked access), which
made reindex_drive report "success, 0 files" — indistinguishable from an
actually-empty folder. It now raises with Google's own error message (often
directly actionable, e.g. a link to enable the API), which reindex_drive's
existing error handling surfaces as an "error" status with that message in
the Chat Agent status log.
Found via a real 403 on ezofis.com's Space (Drive API not yet enabled on
the Google Cloud project); verified by reproducing that exact response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Unify Google OAuth callback: Drive-connect reuses the login redirect_uri
Seth+1: Claude Sonnet 5
c7a1227
Drive-connect previously used its own /api/chat-agent/drive/callback,
requiring a second entry in Google Cloud Console's Authorized redirect URIs
(and produced redirect_uri_mismatch until added). Both flows now funnel
through the single already-registered /api/auth/google/callback, and
auth_routes.google_callback tells them apart by checking whether the
incoming `state` matches the Drive-connect flow's pending session state
before falling through to normal login handling. Token exchange is shared
via oauth_utils.exchange_code_for_tokens.
Verified with FastAPI TestClient: Drive-connect's authorize URL now carries
the same redirect_uri as login, the shared callback correctly routes each
flow and persists the Drive refresh token, ordinary login is unaffected,
and a bogus/unmatched state is still rejected with 400.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Website crawler now seeds from sitemap.xml (incl. WordPress-style nested
sitemap indexes) instead of relying solely on nav-link BFS, raises the page
cap 150->400, retries transient 403/429/5xx with backoff, switches to a
standard browser User-Agent (a self-identifying bot UA was tripping generic
WAF bot-blocklists on real WordPress sites), and skips pages that look like
a bot-challenge/captcha interstitial rather than indexing them as content.
Chat replies now cite the specific website pages they drew on as a
"Sources:" line with markdown links (deduped, website-only, max 3), and the
embed widget renders those as real clickable links via a small safe
markdown-lite renderer (DOM text nodes only, http(s)-only hrefs) instead of
plain text.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Chat Agent module: RAG website widget with Drive/site knowledge base and CRM lead capture
Seth+1: Claude Sonnet 5
991c079
Adds an embeddable WordPress chat widget backed by a Google Drive folder and a
site crawl, with admin-editable skills/guardrails, streaming replies, and
automatic CrmLead creation (with live transcript) for every chat that shares
a name/email.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>