feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
2aee6286d6
commit
5e0615f36d
126 files changed
+17436
No files matched your search
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
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")
|
||||
Reference in new issue
Block a user