Files
replypier/backend/app/models/lead_capture.py
T

118 lines
5.2 KiB
Python
Raw Normal View History

"""
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from sqlalchemy import (
Column, Integer, String, Boolean, DateTime, ForeignKey,
Enum as SQLEnum, JSON, Text, func, false, true,
)
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
import enum
import uuid
from app.database import Base
class LeadAssignmentMode(str, enum.Enum):
"""Where a qualified lead is routed. Phase 1: stored only, not acted on."""
NONE = "none"
SALES_TEAM = "sales_team"
SPECIFIC_PERSON = "specific_person"
ROUND_ROBIN = "round_robin"
class CrmSyncTarget(str, enum.Enum):
"""Which CRM a qualified lead syncs to.
Stored as a plain string column (not a DB enum) so adding a CRM never
requires a schema migration — mirrors ChannelType. str() is the value so
in-memory enum instances and refreshed-from-DB strings render identically.
"""
NONE = "none"
HUBSPOT = "hubspot"
PIPEDRIVE = "pipedrive"
SALESFORCE = "salesforce"
def __str__(self) -> str: # pragma: no cover - trivial
return self.value
class LeadCaptureConfig(Base):
"""Per-agent lead-capture configuration (1:1 with Agent).
Prompt-driven, like transfer_to_human: a simple on/off toggle. When enabled, the
agent collects the configured fields conversationally (no form) and emits them as
structured output plus an AI-written lead summary.
"""
__tablename__ = "lead_capture_configs"
id = Column(Integer, primary_key=True, index=True)
agent_id = Column(
UUID(as_uuid=True),
ForeignKey("agents.id", ondelete="CASCADE"),
unique=True,
nullable=False,
)
# Master toggle (mirrors Agent.transfer_to_human).
enabled = Column(Boolean, default=False, server_default=false(), nullable=False)
# GDPR: require the visitor's explicit "yes" before a lead is recorded (default on).
require_consent = Column(Boolean, default=True, server_default=true(), nullable=False)
# Free-text steering appended to the agent's system prompt (e.g. "prioritise
# pricing-page visitors, be proactive after hours"). The agent decides *when* to ask.
guidance = Column(Text, nullable=True)
# Which details to collect — feeds the prompt and defines the expected lead_data keys:
# {"key": "email", "standard": true, "required": true, "enabled": true}
# {"key": "custom_1", "standard": false, "label": "Team size", "enabled": true}
fields = Column(JSON, nullable=True)
# Routing config — persisted so the shape is ready for real integration later,
# but never acted on in phase 1 (no CRM/Slack/assignment side effects).
assignment_mode = Column(SQLEnum(LeadAssignmentMode), default=LeadAssignmentMode.NONE, server_default="NONE", nullable=False)
assignment_target_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
crm_sync_target = Column(String(32), default=CrmSyncTarget.NONE.value, server_default="none", nullable=False)
slack_notify_enabled = Column(Boolean, default=False, server_default=false(), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
agent = relationship("Agent", back_populates="lead_capture_config", uselist=False)
class LeadCaptureResponse(Base):
"""One row per captured lead — the fields the agent extracted conversationally,
plus an AI-written qualification summary. Kept separate from Customer.meta_data
(integrator-supplied) so AI-captured leads have a clean home."""
__tablename__ = "lead_capture_responses"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False)
agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True)
customer_id = Column(UUID(as_uuid=True), ForeignKey("customers.id", ondelete="CASCADE"), nullable=False)
session_id = Column(UUID(as_uuid=True), ForeignKey("session_to_agents.session_id"), nullable=True)
field_values = Column(JSON, nullable=True) # {"email": ..., "company": ..., "custom_1": ...}
# AI-written qualification narrative (e.g. "40-person fintech replacing Intercom, wants a demo").
summary = Column(Text, nullable=True)
# Whether the visitor gave explicit consent to be contacted (GDPR).
consent = Column(Boolean, default=False, nullable=False)
qualified = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
customer = relationship("Customer", back_populates="lead_capture_responses")
agent = relationship("Agent")