Spaces:
Running
Running
| """ | |
| database.py β SQLite CRM Schema + All DB Operations | |
| Free Tier: SQLite (100% free, no signup needed) | |
| """ | |
| import sqlite3 | |
| import os | |
| from datetime import datetime | |
| DB_PATH = os.path.join(os.path.dirname(__file__), "crm.db") | |
| def get_connection(): | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row # lets you access columns by name | |
| return conn | |
| def init_db(): | |
| """Create all tables if they don't exist.""" | |
| conn = get_connection() | |
| c = conn.cursor() | |
| # ββ LEADS TABLE ββββββββββββββββββββββββββββββββββββββββββββββ | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS leads ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| business_name TEXT NOT NULL, | |
| owner_name TEXT, | |
| business_type TEXT, -- restaurant / startup / agency / creator / shop | |
| city TEXT, | |
| website TEXT, | |
| whatsapp TEXT, | |
| email TEXT, | |
| instagram TEXT, | |
| linkedin TEXT, | |
| google_reviews INTEGER DEFAULT 0, | |
| ig_followers INTEGER DEFAULT 0, | |
| score INTEGER DEFAULT 0, | |
| status TEXT DEFAULT 'new', -- new / contacted / replied / qualified / closed | |
| preferred_channel TEXT, | |
| notes TEXT, | |
| created_at TEXT DEFAULT (datetime('now')), | |
| updated_at TEXT DEFAULT (datetime('now')) | |
| ) | |
| """) | |
| # ββ MESSAGES TABLE ββββββββββββββββββββββββββββββββββββββββββββ | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS messages ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| lead_id INTEGER NOT NULL, | |
| channel TEXT NOT NULL, -- whatsapp / email / instagram / linkedin | |
| direction TEXT NOT NULL, -- outbound / inbound | |
| content TEXT NOT NULL, | |
| status TEXT DEFAULT 'draft', -- draft / sent / delivered / read / replied | |
| sent_at TEXT, | |
| created_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (lead_id) REFERENCES leads(id) | |
| ) | |
| """) | |
| # ββ FOLLOW-UPS TABLE ββββββββββββββββββββββββββββββββββββββββββ | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS followups ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| lead_id INTEGER NOT NULL, | |
| scheduled TEXT NOT NULL, -- datetime string | |
| channel TEXT NOT NULL, | |
| message TEXT NOT NULL, | |
| done INTEGER DEFAULT 0, -- 0=pending, 1=done | |
| FOREIGN KEY (lead_id) REFERENCES leads(id) | |
| ) | |
| """) | |
| # ββ SCORES TABLE βββββββββββββββββββββββββββββββββββββββββββββ | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS scores ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| lead_id INTEGER NOT NULL UNIQUE, | |
| website_score INTEGER DEFAULT 0, | |
| social_score INTEGER DEFAULT 0, | |
| channel_score INTEGER DEFAULT 0, | |
| potential_score INTEGER DEFAULT 0, | |
| total INTEGER DEFAULT 0, | |
| updated_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (lead_id) REFERENCES leads(id) | |
| ) | |
| """) | |
| # ββ AGENT LOGS TABLE ββββββββββββββββββββββββββββββββββββββββββ | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS agent_logs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| timestamp TEXT DEFAULT (datetime('now')), | |
| level TEXT DEFAULT 'info', -- info / success / warning / error | |
| message TEXT NOT NULL | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| print("Database ready -> crm.db") | |
| # ββ LEAD CRUD βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def add_lead(data: dict) -> int: | |
| conn = get_connection() | |
| c = conn.cursor() | |
| c.execute(""" | |
| INSERT INTO leads | |
| (business_name, owner_name, business_type, city, | |
| website, whatsapp, email, instagram, linkedin, | |
| google_reviews, ig_followers, notes) | |
| VALUES | |
| (:business_name, :owner_name, :business_type, :city, | |
| :website, :whatsapp, :email, :instagram, :linkedin, | |
| :google_reviews, :ig_followers, :notes) | |
| """, data) | |
| lead_id = c.lastrowid | |
| conn.commit() | |
| conn.close() | |
| return lead_id | |
| def get_lead(lead_id: int) -> dict: | |
| conn = get_connection() | |
| c = conn.cursor() | |
| row = c.execute("SELECT * FROM leads WHERE id=?", (lead_id,)).fetchone() | |
| conn.close() | |
| return dict(row) if row else None | |
| def get_all_leads(status: str = None) -> list: | |
| conn = get_connection() | |
| c = conn.cursor() | |
| if status: | |
| rows = c.execute("SELECT * FROM leads WHERE status=? ORDER BY score DESC", (status,)).fetchall() | |
| else: | |
| rows = c.execute("SELECT * FROM leads ORDER BY score DESC").fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| def update_lead(lead_id: int, fields: dict): | |
| fields["updated_at"] = datetime.now().isoformat() | |
| fields["id"] = lead_id | |
| set_clause = ", ".join(f"{k}=:{k}" for k in fields if k != "id") | |
| conn = get_connection() | |
| conn.execute(f"UPDATE leads SET {set_clause} WHERE id=:id", fields) | |
| conn.commit() | |
| conn.close() | |
| def delete_lead(lead_id: int): | |
| conn = get_connection() | |
| conn.execute("DELETE FROM leads WHERE id=?", (lead_id,)) | |
| conn.execute("DELETE FROM messages WHERE lead_id=?", (lead_id,)) | |
| conn.execute("DELETE FROM followups WHERE lead_id=?", (lead_id,)) | |
| conn.execute("DELETE FROM scores WHERE lead_id=?", (lead_id,)) | |
| conn.commit() | |
| conn.close() | |
| # ββ MESSAGE CRUD ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_message(lead_id: int, channel: str, direction: str, content: str, status="draft") -> int: | |
| conn = get_connection() | |
| c = conn.cursor() | |
| c.execute(""" | |
| INSERT INTO messages (lead_id, channel, direction, content, status) | |
| VALUES (?, ?, ?, ?, ?) | |
| """, (lead_id, channel, direction, content, status)) | |
| msg_id = c.lastrowid | |
| conn.commit() | |
| conn.close() | |
| return msg_id | |
| def mark_message_sent(msg_id: int): | |
| conn = get_connection() | |
| conn.execute( | |
| "UPDATE messages SET status='sent', sent_at=datetime('now') WHERE id=?", | |
| (msg_id,) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def get_messages(lead_id: int) -> list: | |
| conn = get_connection() | |
| rows = conn.execute( | |
| "SELECT * FROM messages WHERE lead_id=? ORDER BY created_at", (lead_id,) | |
| ).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| # ββ SCORE UPSERT ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_score(lead_id: int, breakdown: dict): | |
| conn = get_connection() | |
| conn.execute(""" | |
| INSERT INTO scores (lead_id, website_score, social_score, channel_score, potential_score, total) | |
| VALUES (:lead_id, :website_score, :social_score, :channel_score, :potential_score, :total) | |
| ON CONFLICT(lead_id) DO UPDATE SET | |
| website_score=excluded.website_score, | |
| social_score=excluded.social_score, | |
| channel_score=excluded.channel_score, | |
| potential_score=excluded.potential_score, | |
| total=excluded.total, | |
| updated_at=datetime('now') | |
| """, {"lead_id": lead_id, **breakdown}) | |
| conn.execute("UPDATE leads SET score=? WHERE id=?", (breakdown["total"], lead_id)) | |
| conn.commit() | |
| conn.close() | |
| # ββ FOLLOWUP CRUD βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def schedule_followup(lead_id: int, scheduled: str, channel: str, message: str): | |
| conn = get_connection() | |
| conn.execute(""" | |
| INSERT INTO followups (lead_id, scheduled, channel, message) | |
| VALUES (?, ?, ?, ?) | |
| """, (lead_id, scheduled, channel, message)) | |
| conn.commit() | |
| conn.close() | |
| def get_pending_followups() -> list: | |
| conn = get_connection() | |
| now = datetime.now().isoformat() | |
| rows = conn.execute(""" | |
| SELECT f.*, l.business_name FROM followups f | |
| JOIN leads l ON f.lead_id = l.id | |
| WHERE f.done=0 AND f.scheduled <= ? | |
| ORDER BY f.scheduled | |
| """, (now,)).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| def mark_followup_done(followup_id: int): | |
| conn = get_connection() | |
| conn.execute("UPDATE followups SET done=1 WHERE id=?", (followup_id,)) | |
| conn.commit() | |
| conn.close() | |
| # ββ AGENT LOGS CRUD βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def log_agent_activity(message: str, level: str = "info"): | |
| """Log an activity message from the autonomous agent to the database.""" | |
| conn = get_connection() | |
| conn.execute( | |
| "INSERT INTO agent_logs (message, level) VALUES (?, ?)", | |
| (message, level) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def get_agent_logs(limit: int = 100) -> list: | |
| """Retrieve the latest autonomous agent logs.""" | |
| conn = get_connection() | |
| rows = conn.execute( | |
| "SELECT id, timestamp, level, message FROM agent_logs ORDER BY id DESC LIMIT ?", | |
| (limit,) | |
| ).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| def clear_agent_logs(): | |
| """Clear all agent logs from the database.""" | |
| conn = get_connection() | |
| conn.execute("DELETE FROM agent_logs") | |
| conn.commit() | |
| conn.close() | |
| def get_score_breakdown(lead_id: int) -> dict: | |
| """Retrieve the score breakdown for a lead.""" | |
| conn = get_connection() | |
| c = conn.cursor() | |
| row = c.execute("SELECT * FROM scores WHERE lead_id=?", (lead_id,)).fetchone() | |
| conn.close() | |
| if row: | |
| return dict(row) | |
| else: | |
| return { | |
| "website_score": 0, | |
| "social_score": 0, | |
| "channel_score": 0, | |
| "potential_score": 0, | |
| "total": 0 | |
| } | |