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>
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…
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>
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>