|
|
""" |
|
|
Database Manager for B2B Sales AI Agent |
|
|
Handles database initialization, migrations, and session management |
|
|
""" |
|
|
from sqlalchemy import create_engine, event |
|
|
from sqlalchemy.orm import sessionmaker, scoped_session |
|
|
from sqlalchemy.pool import StaticPool |
|
|
import os |
|
|
import logging |
|
|
from pathlib import Path |
|
|
from contextlib import contextmanager |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
class DatabaseManager: |
|
|
""" |
|
|
Manages SQLite database connections and sessions |
|
|
""" |
|
|
|
|
|
def __init__(self, db_path: str = None): |
|
|
""" |
|
|
Initialize database manager |
|
|
|
|
|
Args: |
|
|
db_path: Path to SQLite database file |
|
|
""" |
|
|
if db_path is None: |
|
|
|
|
|
|
|
|
default_path = os.getenv('DATABASE_PATH', './data/cx_agent.db') |
|
|
|
|
|
|
|
|
if os.path.exists('/data'): |
|
|
|
|
|
default_path = '/data/cx_agent.db' |
|
|
elif os.path.exists('/tmp'): |
|
|
|
|
|
default_path = '/tmp/cx_agent.db' |
|
|
|
|
|
db_path = default_path |
|
|
|
|
|
self.db_path = db_path |
|
|
self.engine = None |
|
|
self.Session = None |
|
|
|
|
|
def initialize(self): |
|
|
"""Initialize database connection and create tables""" |
|
|
try: |
|
|
print(f"📂 Initializing database at: {self.db_path}") |
|
|
logger.info(f"Initializing database at: {self.db_path}") |
|
|
|
|
|
|
|
|
db_dir = Path(self.db_path).parent |
|
|
db_dir.mkdir(parents=True, exist_ok=True) |
|
|
print(f"📁 Database directory: {db_dir}") |
|
|
logger.info(f"Database directory created/verified: {db_dir}") |
|
|
|
|
|
|
|
|
self.engine = create_engine( |
|
|
f'sqlite:///{self.db_path}', |
|
|
connect_args={'check_same_thread': False}, |
|
|
poolclass=StaticPool, |
|
|
echo=False |
|
|
) |
|
|
|
|
|
|
|
|
@event.listens_for(self.engine, "connect") |
|
|
def set_sqlite_pragma(dbapi_conn, connection_record): |
|
|
cursor = dbapi_conn.cursor() |
|
|
cursor.execute("PRAGMA foreign_keys=ON") |
|
|
cursor.close() |
|
|
|
|
|
|
|
|
|
|
|
session_factory = sessionmaker(bind=self.engine, expire_on_commit=False) |
|
|
self.Session = scoped_session(session_factory) |
|
|
|
|
|
|
|
|
try: |
|
|
from models.database import Base as EnterpriseBase |
|
|
EnterpriseBase.metadata.create_all(self.engine) |
|
|
print("✅ Enterprise tables created") |
|
|
logger.info("Enterprise tables created") |
|
|
except ImportError as e: |
|
|
print(f"⚠️ Could not import enterprise models: {e}") |
|
|
logger.warning(f"Could not import enterprise models: {e}") |
|
|
|
|
|
logger.info(f"Database initialized at {self.db_path}") |
|
|
|
|
|
|
|
|
self._initialize_default_data() |
|
|
|
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to initialize database: {str(e)}") |
|
|
raise |
|
|
|
|
|
def _initialize_default_data(self): |
|
|
"""Insert default data for new databases""" |
|
|
try: |
|
|
from models.database import Setting, Sequence, SequenceEmail, Template |
|
|
|
|
|
session = self.Session() |
|
|
|
|
|
|
|
|
existing_settings = session.query(Setting).first() |
|
|
if existing_settings: |
|
|
session.close() |
|
|
return |
|
|
|
|
|
|
|
|
default_settings = [ |
|
|
Setting(key='company_name', value='Your Company', description='Company name for email footers'), |
|
|
Setting(key='company_address', value='123 Main St, City, State 12345', description='Physical address for CAN-SPAM compliance'), |
|
|
Setting(key='sender_name', value='Sales Team', description='Default sender name'), |
|
|
Setting(key='sender_email', value='hello@example.com', description='Default sender email'), |
|
|
Setting(key='daily_email_limit', value='1000', description='Max emails per day'), |
|
|
Setting(key='enable_tracking', value='1', description='Enable email tracking'), |
|
|
] |
|
|
session.add_all(default_settings) |
|
|
|
|
|
|
|
|
cold_outreach = Sequence( |
|
|
name='Cold Outreach - 3 Touch', |
|
|
description='Standard 3-email cold outreach sequence', |
|
|
category='outbound', |
|
|
is_template=True |
|
|
) |
|
|
session.add(cold_outreach) |
|
|
session.flush() |
|
|
|
|
|
sequence_emails = [ |
|
|
SequenceEmail( |
|
|
sequence_id=cold_outreach.id, |
|
|
step_number=1, |
|
|
wait_days=0, |
|
|
subject='Quick question about {{company_name}}', |
|
|
body='''Hi {{first_name}}, |
|
|
|
|
|
I noticed {{company_name}} is in the {{industry}} space with {{company_size}} employees. |
|
|
|
|
|
Companies like yours often face challenges with {{pain_points}}. |
|
|
|
|
|
We've helped similar companies reduce support costs by 35% and improve customer satisfaction significantly. |
|
|
|
|
|
Would you be open to a brief 15-minute call to explore if we might be able to help? |
|
|
|
|
|
Best regards, |
|
|
{{sender_name}}''' |
|
|
), |
|
|
SequenceEmail( |
|
|
sequence_id=cold_outreach.id, |
|
|
step_number=2, |
|
|
wait_days=3, |
|
|
subject='Re: Quick question about {{company_name}}', |
|
|
body='''Hi {{first_name}}, |
|
|
|
|
|
I wanted to follow up on my previous email. I understand you're busy, so I'll keep this brief. |
|
|
|
|
|
We recently helped a company similar to {{company_name}} achieve: |
|
|
• 40% reduction in support ticket volume |
|
|
• 25% improvement in customer satisfaction scores |
|
|
• 30% faster response times |
|
|
|
|
|
I'd love to share how we did it. Are you available for a quick call this week? |
|
|
|
|
|
Best, |
|
|
{{sender_name}}''' |
|
|
), |
|
|
SequenceEmail( |
|
|
sequence_id=cold_outreach.id, |
|
|
step_number=3, |
|
|
wait_days=7, |
|
|
subject='Last attempt - {{company_name}}', |
|
|
body='''Hi {{first_name}}, |
|
|
|
|
|
This is my last attempt to reach you. I completely understand if now isn't the right time. |
|
|
|
|
|
If you're interested in learning how we can help {{company_name}} improve customer experience, I'm happy to send over some quick resources. |
|
|
|
|
|
Otherwise, I'll assume this isn't a priority right now and won't bother you again. |
|
|
|
|
|
Thanks for your time, |
|
|
{{sender_name}} |
|
|
|
|
|
P.S. If you'd prefer to be removed from my list, just reply "Not interested" and I'll make sure you don't hear from me again.''' |
|
|
), |
|
|
] |
|
|
session.add_all(sequence_emails) |
|
|
|
|
|
|
|
|
templates = [ |
|
|
Template( |
|
|
name='Meeting Request', |
|
|
category='meeting_request', |
|
|
subject='Meeting invitation - {{company_name}}', |
|
|
body='''Hi {{first_name}}, |
|
|
|
|
|
Thank you for your interest! I'd love to schedule a call to discuss how we can help {{company_name}}. |
|
|
|
|
|
Here are a few time slots that work for me: |
|
|
• {{time_slot_1}} |
|
|
• {{time_slot_2}} |
|
|
• {{time_slot_3}} |
|
|
|
|
|
Let me know which works best for you, or feel free to suggest another time. |
|
|
|
|
|
Looking forward to speaking with you! |
|
|
|
|
|
Best, |
|
|
{{sender_name}}''', |
|
|
variables='["first_name", "company_name", "time_slot_1", "time_slot_2", "time_slot_3", "sender_name"]' |
|
|
), |
|
|
Template( |
|
|
name='Follow-up After Meeting', |
|
|
category='follow_up', |
|
|
subject='Great speaking with you, {{first_name}}', |
|
|
body='''Hi {{first_name}}, |
|
|
|
|
|
Thanks for taking the time to speak with me today about {{company_name}}'s customer experience goals. |
|
|
|
|
|
As discussed, here are the next steps: |
|
|
• {{next_step_1}} |
|
|
• {{next_step_2}} |
|
|
|
|
|
I'll follow up on {{follow_up_date}} as we agreed. |
|
|
|
|
|
Please don't hesitate to reach out if you have any questions in the meantime. |
|
|
|
|
|
Best regards, |
|
|
{{sender_name}}''', |
|
|
variables='["first_name", "company_name", "next_step_1", "next_step_2", "follow_up_date", "sender_name"]' |
|
|
), |
|
|
] |
|
|
session.add_all(templates) |
|
|
|
|
|
session.commit() |
|
|
session.close() |
|
|
|
|
|
logger.info("Default data initialized successfully") |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to initialize default data: {str(e)}") |
|
|
if session: |
|
|
session.rollback() |
|
|
session.close() |
|
|
|
|
|
|
|
|
@contextmanager |
|
|
def get_session(self): |
|
|
""" |
|
|
Context manager for database sessions |
|
|
|
|
|
Usage: |
|
|
with db_manager.get_session() as session: |
|
|
session.query(Contact).all() |
|
|
""" |
|
|
session = self.Session() |
|
|
try: |
|
|
yield session |
|
|
session.commit() |
|
|
except Exception: |
|
|
session.rollback() |
|
|
raise |
|
|
finally: |
|
|
session.close() |
|
|
|
|
|
def close(self): |
|
|
"""Close database connection""" |
|
|
if self.Session: |
|
|
self.Session.remove() |
|
|
if self.engine: |
|
|
self.engine.dispose() |
|
|
logger.info("Database connection closed") |
|
|
|
|
|
|
|
|
|
|
|
_db_manager = None |
|
|
|
|
|
|
|
|
def get_db_manager() -> DatabaseManager: |
|
|
"""Get or create global database manager instance""" |
|
|
global _db_manager |
|
|
if _db_manager is None: |
|
|
_db_manager = DatabaseManager() |
|
|
_db_manager.initialize() |
|
|
return _db_manager |
|
|
|
|
|
|
|
|
def init_database(db_path: str = None): |
|
|
"""Initialize database with custom path""" |
|
|
global _db_manager |
|
|
_db_manager = DatabaseManager(db_path) |
|
|
_db_manager.initialize() |
|
|
return _db_manager |
|
|
|