Spaces:
Sleeping
Sleeping
Update src/core/services/database_service.py
Browse files- src/core/services/database_service.py +585 -100
src/core/services/database_service.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Enhanced database service with LLM-powered synthetic data generation
|
| 3 |
-
"""
|
| 4 |
import sqlite3
|
| 5 |
from contextlib import contextmanager
|
| 6 |
from pathlib import Path
|
|
@@ -54,11 +52,10 @@ class DatabaseService:
|
|
| 54 |
conn.close()
|
| 55 |
|
| 56 |
def setup_database(self):
|
| 57 |
-
"""Set up
|
| 58 |
with self.get_db() as conn:
|
| 59 |
c = conn.cursor()
|
| 60 |
c.executescript('''
|
| 61 |
-
-- Core tables
|
| 62 |
CREATE TABLE IF NOT EXISTS users (
|
| 63 |
id TEXT PRIMARY KEY,
|
| 64 |
email TEXT UNIQUE,
|
|
@@ -125,106 +122,594 @@ class DatabaseService:
|
|
| 125 |
FOREIGN KEY (owner_id) REFERENCES users (id)
|
| 126 |
);
|
| 127 |
|
| 128 |
-
--
|
| 129 |
-
CREATE
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
type TEXT, -- new, upsell, cross-sell
|
| 135 |
-
stage TEXT, -- prospect, qualified, proposal, negotiation, closed-won, closed-lost
|
| 136 |
-
value REAL,
|
| 137 |
-
close_date TEXT,
|
| 138 |
-
probability INTEGER,
|
| 139 |
-
source_interaction_id TEXT,
|
| 140 |
-
created_at TEXT,
|
| 141 |
-
updated_at TEXT,
|
| 142 |
-
last_contact_date TEXT,
|
| 143 |
-
next_step TEXT,
|
| 144 |
-
next_step_date TEXT,
|
| 145 |
-
notes TEXT,
|
| 146 |
-
FOREIGN KEY (account_id) REFERENCES accounts (id),
|
| 147 |
-
FOREIGN KEY (primary_contact_id) REFERENCES contacts (id),
|
| 148 |
-
FOREIGN KEY (source_interaction_id) REFERENCES interactions (id)
|
| 149 |
-
);
|
| 150 |
-
|
| 151 |
-
CREATE TABLE IF NOT EXISTS opportunity_stages (
|
| 152 |
-
id TEXT PRIMARY KEY,
|
| 153 |
-
opportunity_id TEXT NOT NULL,
|
| 154 |
-
stage TEXT NOT NULL,
|
| 155 |
-
changed_at TEXT NOT NULL,
|
| 156 |
-
changed_by TEXT NOT NULL,
|
| 157 |
-
notes TEXT,
|
| 158 |
-
FOREIGN KEY (opportunity_id) REFERENCES opportunities (id),
|
| 159 |
-
FOREIGN KEY (changed_by) REFERENCES users (id)
|
| 160 |
-
);
|
| 161 |
-
|
| 162 |
-
-- Follow-up tracking
|
| 163 |
-
CREATE TABLE IF NOT EXISTS follow_ups (
|
| 164 |
-
id TEXT PRIMARY KEY,
|
| 165 |
-
interaction_id TEXT,
|
| 166 |
-
opportunity_id TEXT,
|
| 167 |
-
type TEXT NOT NULL,
|
| 168 |
-
title TEXT NOT NULL,
|
| 169 |
-
description TEXT,
|
| 170 |
-
due_date TEXT NOT NULL,
|
| 171 |
-
status TEXT DEFAULT 'pending',
|
| 172 |
-
assignee_id TEXT,
|
| 173 |
-
created_at TEXT,
|
| 174 |
-
calendar_event_id TEXT,
|
| 175 |
-
reminder_sent BOOLEAN DEFAULT FALSE,
|
| 176 |
-
completed_at TEXT,
|
| 177 |
-
FOREIGN KEY (interaction_id) REFERENCES interactions (id),
|
| 178 |
-
FOREIGN KEY (opportunity_id) REFERENCES opportunities (id),
|
| 179 |
-
FOREIGN KEY (assignee_id) REFERENCES users (id)
|
| 180 |
-
);
|
| 181 |
-
|
| 182 |
-
-- Performance indexes
|
| 183 |
-
CREATE INDEX IF NOT EXISTS idx_opportunities_account
|
| 184 |
-
ON opportunities(account_id);
|
| 185 |
-
CREATE INDEX IF NOT EXISTS idx_opportunities_contact
|
| 186 |
-
ON opportunities(primary_contact_id);
|
| 187 |
-
CREATE INDEX IF NOT EXISTS idx_opportunity_stages_opp
|
| 188 |
-
ON opportunity_stages(opportunity_id);
|
| 189 |
-
CREATE INDEX IF NOT EXISTS idx_follow_ups_interaction
|
| 190 |
-
ON follow_ups(interaction_id);
|
| 191 |
-
CREATE INDEX IF NOT EXISTS idx_follow_ups_opportunity
|
| 192 |
-
ON follow_ups(opportunity_id);
|
| 193 |
-
CREATE INDEX IF NOT EXISTS idx_interactions_account
|
| 194 |
-
ON interactions(account_id);
|
| 195 |
-
CREATE INDEX IF NOT EXISTS idx_contacts_account
|
| 196 |
-
ON contacts(account_id);
|
| 197 |
''')
|
| 198 |
|
| 199 |
-
|
| 200 |
-
"""
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
| 206 |
try:
|
| 207 |
with self.get_db() as conn:
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
-
# Additional generation logic for accounts, contacts, etc.
|
| 223 |
-
conn.commit()
|
| 224 |
except Exception as e:
|
| 225 |
-
logger.error(f"Error
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Enhanced database service with complete functionality"""
|
|
|
|
|
|
|
| 2 |
import sqlite3
|
| 3 |
from contextlib import contextmanager
|
| 4 |
from pathlib import Path
|
|
|
|
| 52 |
conn.close()
|
| 53 |
|
| 54 |
def setup_database(self):
|
| 55 |
+
"""Set up database schema"""
|
| 56 |
with self.get_db() as conn:
|
| 57 |
c = conn.cursor()
|
| 58 |
c.executescript('''
|
|
|
|
| 59 |
CREATE TABLE IF NOT EXISTS users (
|
| 60 |
id TEXT PRIMARY KEY,
|
| 61 |
email TEXT UNIQUE,
|
|
|
|
| 122 |
FOREIGN KEY (owner_id) REFERENCES users (id)
|
| 123 |
);
|
| 124 |
|
| 125 |
+
-- Create indexes for better query performance
|
| 126 |
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
| 127 |
+
CREATE INDEX IF NOT EXISTS idx_accounts_owner ON accounts(account_owner_id);
|
| 128 |
+
CREATE INDEX IF NOT EXISTS idx_contacts_account ON contacts(account_id);
|
| 129 |
+
CREATE INDEX IF NOT EXISTS idx_interactions_account ON interactions(account_id);
|
| 130 |
+
CREATE INDEX IF NOT EXISTS idx_interactions_owner ON interactions(owner_id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
''')
|
| 132 |
|
| 133 |
+
def get_user_by_email(self, email: str) -> Optional[Dict]:
|
| 134 |
+
"""
|
| 135 |
+
Get user details by email address
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
email: User's email address
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
Dict containing user details if found, None otherwise
|
| 142 |
+
"""
|
| 143 |
try:
|
| 144 |
with self.get_db() as conn:
|
| 145 |
+
cursor = conn.execute("""
|
| 146 |
+
SELECT
|
| 147 |
+
id,
|
| 148 |
+
email,
|
| 149 |
+
name,
|
| 150 |
+
role,
|
| 151 |
+
department,
|
| 152 |
+
title,
|
| 153 |
+
region,
|
| 154 |
+
quota,
|
| 155 |
+
created_at,
|
| 156 |
+
last_login
|
| 157 |
+
FROM users
|
| 158 |
+
WHERE email = ?
|
| 159 |
+
""", (email,))
|
| 160 |
|
| 161 |
+
row = cursor.fetchone()
|
| 162 |
+
if row:
|
| 163 |
+
# Update last login
|
| 164 |
+
conn.execute("""
|
| 165 |
+
UPDATE users
|
| 166 |
+
SET last_login = ?
|
| 167 |
+
WHERE id = ?
|
| 168 |
+
""", (datetime.now().isoformat(), row['id']))
|
| 169 |
+
conn.commit()
|
| 170 |
+
|
| 171 |
+
# Convert row to dict
|
| 172 |
+
return dict(row)
|
| 173 |
+
return None
|
| 174 |
|
|
|
|
|
|
|
| 175 |
except Exception as e:
|
| 176 |
+
logger.error(f"Error getting user by email: {str(e)}")
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
def generate_synthetic_data(self):
|
| 180 |
+
"""Generate synthetic test data"""
|
| 181 |
+
with self.get_db() as conn:
|
| 182 |
+
c = conn.cursor()
|
| 183 |
+
|
| 184 |
+
# Generate Users
|
| 185 |
+
users = []
|
| 186 |
+
user_ids = [] # Keep track of user IDs for account assignment
|
| 187 |
+
|
| 188 |
+
# Predefined test user
|
| 189 |
+
test_user_id = str(uuid.uuid4())
|
| 190 |
+
users.append({
|
| 191 |
+
'id': test_user_id,
|
| 192 |
+
'email': 'test@example.com', # Default test login
|
| 193 |
+
'name': 'Test User',
|
| 194 |
+
'role': 'sales_rep',
|
| 195 |
+
'department': 'Sales',
|
| 196 |
+
'title': 'Senior Sales Representative',
|
| 197 |
+
'region': 'North',
|
| 198 |
+
'quota': 1000000.0,
|
| 199 |
+
'created_at': datetime.now().isoformat(),
|
| 200 |
+
'last_login': datetime.now().isoformat()
|
| 201 |
+
})
|
| 202 |
+
user_ids.append(test_user_id)
|
| 203 |
+
|
| 204 |
+
# Generate additional users
|
| 205 |
+
for _ in range(10):
|
| 206 |
+
user_id = str(uuid.uuid4())
|
| 207 |
+
user_ids.append(user_id)
|
| 208 |
+
users.append({
|
| 209 |
+
'id': user_id,
|
| 210 |
+
'email': self.fake.company_email(),
|
| 211 |
+
'name': self.fake.name(),
|
| 212 |
+
'role': random.choice(['sales_rep', 'regional_lead', 'head_of_sales']),
|
| 213 |
+
'department': random.choice(['Sales', 'Consulting', 'Technology']),
|
| 214 |
+
'title': 'Senior Sales Representative',
|
| 215 |
+
'region': random.choice(['North', 'South', 'East', 'West']),
|
| 216 |
+
'quota': random.uniform(500000, 2000000),
|
| 217 |
+
'created_at': datetime.now().isoformat(),
|
| 218 |
+
'last_login': datetime.now().isoformat()
|
| 219 |
+
})
|
| 220 |
+
|
| 221 |
+
# Insert users
|
| 222 |
+
c.executemany('''
|
| 223 |
+
INSERT OR REPLACE INTO users VALUES (
|
| 224 |
+
:id, :email, :name, :role, :department, :title, :region,
|
| 225 |
+
:quota, :created_at, :last_login
|
| 226 |
+
)
|
| 227 |
+
''', users)
|
| 228 |
+
|
| 229 |
+
# Generate Accounts
|
| 230 |
+
accounts = []
|
| 231 |
+
industries = ['Technology', 'Healthcare', 'Financial Services', 'Manufacturing', 'Retail']
|
| 232 |
+
|
| 233 |
+
# Ensure test user has accounts
|
| 234 |
+
for _ in range(3):
|
| 235 |
+
accounts.append({
|
| 236 |
+
'id': str(uuid.uuid4()),
|
| 237 |
+
'name': self.fake.company(),
|
| 238 |
+
'parent_account_id': None,
|
| 239 |
+
'industry': random.choice(industries),
|
| 240 |
+
'status': 'active',
|
| 241 |
+
'website': self.fake.url(),
|
| 242 |
+
'annual_revenue': random.uniform(1000000, 100000000),
|
| 243 |
+
'employee_count': random.randint(50, 10000),
|
| 244 |
+
'technology_stack': json.dumps(['Python', 'React', 'AWS']),
|
| 245 |
+
'region': 'North',
|
| 246 |
+
'address': self.fake.address(),
|
| 247 |
+
'account_owner_id': test_user_id, # Assign to test user
|
| 248 |
+
'engagement_score': random.uniform(0, 100),
|
| 249 |
+
'created_at': datetime.now().isoformat(),
|
| 250 |
+
'last_activity_at': datetime.now().isoformat()
|
| 251 |
+
})
|
| 252 |
+
|
| 253 |
+
# Generate additional accounts
|
| 254 |
+
for user_id in user_ids:
|
| 255 |
+
for _ in range(random.randint(2, 5)):
|
| 256 |
+
accounts.append({
|
| 257 |
+
'id': str(uuid.uuid4()),
|
| 258 |
+
'name': self.fake.company(),
|
| 259 |
+
'parent_account_id': None,
|
| 260 |
+
'industry': random.choice(industries),
|
| 261 |
+
'status': 'active',
|
| 262 |
+
'website': self.fake.url(),
|
| 263 |
+
'annual_revenue': random.uniform(1000000, 100000000),
|
| 264 |
+
'employee_count': random.randint(50, 10000),
|
| 265 |
+
'technology_stack': json.dumps(['Python', 'React', 'AWS']),
|
| 266 |
+
'region': random.choice(['North', 'South', 'East', 'West']),
|
| 267 |
+
'address': self.fake.address(),
|
| 268 |
+
'account_owner_id': user_id,
|
| 269 |
+
'engagement_score': random.uniform(0, 100),
|
| 270 |
+
'created_at': datetime.now().isoformat(),
|
| 271 |
+
'last_activity_at': datetime.now().isoformat()
|
| 272 |
+
})
|
| 273 |
+
|
| 274 |
+
# Insert accounts
|
| 275 |
+
c.executemany('''
|
| 276 |
+
INSERT OR REPLACE INTO accounts VALUES (
|
| 277 |
+
:id, :name, :parent_account_id, :industry, :status, :website,
|
| 278 |
+
:annual_revenue, :employee_count, :technology_stack, :region,
|
| 279 |
+
:address, :account_owner_id, :engagement_score, :created_at,
|
| 280 |
+
:last_activity_at
|
| 281 |
+
)
|
| 282 |
+
''', accounts)
|
| 283 |
+
|
| 284 |
+
# Generate contacts for each account
|
| 285 |
+
contacts = []
|
| 286 |
+
for account in accounts:
|
| 287 |
+
for _ in range(random.randint(3, 8)): # 3-8 contacts per account
|
| 288 |
+
contacts.append({
|
| 289 |
+
'id': str(uuid.uuid4()),
|
| 290 |
+
'account_id': account['id'],
|
| 291 |
+
'first_name': self.fake.first_name(),
|
| 292 |
+
'last_name': self.fake.last_name(),
|
| 293 |
+
'email': self.fake.email(),
|
| 294 |
+
'phone': self.fake.phone_number(),
|
| 295 |
+
'title': random.choice(['CEO', 'CTO', 'CFO', 'VP Sales', 'Director']),
|
| 296 |
+
'department': random.choice(['Executive', 'Sales', 'IT', 'Finance']),
|
| 297 |
+
'reports_to_id': None,
|
| 298 |
+
'influence_level': random.choice(['High', 'Medium', 'Low']),
|
| 299 |
+
'engagement_score': random.uniform(0, 100),
|
| 300 |
+
'preferences': json.dumps({}),
|
| 301 |
+
'created_at': datetime.now().isoformat(),
|
| 302 |
+
'last_contacted': datetime.now().isoformat()
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
c.executemany('''
|
| 306 |
+
INSERT OR REPLACE INTO contacts VALUES (
|
| 307 |
+
:id, :account_id, :first_name, :last_name, :email, :phone,
|
| 308 |
+
:title, :department, :reports_to_id, :influence_level,
|
| 309 |
+
:engagement_score, :preferences, :created_at, :last_contacted
|
| 310 |
+
)
|
| 311 |
+
''', contacts)
|
| 312 |
+
|
| 313 |
+
# Generate interactions for each account
|
| 314 |
+
interactions = []
|
| 315 |
+
for account in accounts:
|
| 316 |
+
for _ in range(random.randint(5, 12)): # 5-12 interactions per account
|
| 317 |
+
interactions.append({
|
| 318 |
+
'id': str(uuid.uuid4()),
|
| 319 |
+
'type': random.choice(['call', 'meeting', 'email', 'presentation']),
|
| 320 |
+
'account_id': account['id'],
|
| 321 |
+
'owner_id': account['account_owner_id'],
|
| 322 |
+
'transcript': self.fake.paragraph(),
|
| 323 |
+
'summary': self.fake.sentence(),
|
| 324 |
+
'sentiment_score': random.uniform(0, 1),
|
| 325 |
+
'metadata': json.dumps({
|
| 326 |
+
'duration': random.randint(15, 120),
|
| 327 |
+
'location': random.choice(['virtual', 'in-person']),
|
| 328 |
+
'attendees': random.randint(1, 5),
|
| 329 |
+
'key_points': [
|
| 330 |
+
self.fake.sentence() for _ in range(random.randint(2, 5))
|
| 331 |
+
],
|
| 332 |
+
'action_items': [
|
| 333 |
+
{'description': self.fake.sentence(), 'owner': self.fake.name()}
|
| 334 |
+
for _ in range(random.randint(1, 3))
|
| 335 |
+
]
|
| 336 |
+
}),
|
| 337 |
+
'created_at': datetime.now().isoformat()
|
| 338 |
+
})
|
| 339 |
+
|
| 340 |
+
c.executemany('''
|
| 341 |
+
INSERT OR REPLACE INTO interactions VALUES (
|
| 342 |
+
:id, :type, :account_id, :owner_id, :transcript, :summary,
|
| 343 |
+
:sentiment_score, :metadata, :created_at
|
| 344 |
+
)
|
| 345 |
+
''', interactions)
|
| 346 |
+
|
| 347 |
+
conn.commit()
|
| 348 |
+
|
| 349 |
+
def get_user_accounts(self, user_id: str) -> List[Dict]:
|
| 350 |
+
"""Get accounts associated with user"""
|
| 351 |
+
with self.get_db() as conn:
|
| 352 |
+
cursor = conn.execute("""
|
| 353 |
+
SELECT * FROM accounts
|
| 354 |
+
WHERE account_owner_id = ?
|
| 355 |
+
ORDER BY name
|
| 356 |
+
""", (user_id,))
|
| 357 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 358 |
+
|
| 359 |
+
def get_account_metrics(self, account_id: str) -> Dict:
|
| 360 |
+
"""Get metrics for a specific account"""
|
| 361 |
+
with self.get_db() as conn:
|
| 362 |
+
# Get contact count
|
| 363 |
+
cursor = conn.execute("""
|
| 364 |
+
SELECT COUNT(*) as contact_count
|
| 365 |
+
FROM contacts
|
| 366 |
+
WHERE account_id = ?
|
| 367 |
+
""", (account_id,))
|
| 368 |
+
contact_count = cursor.fetchone()['contact_count']
|
| 369 |
+
|
| 370 |
+
# Get interaction count and average sentiment
|
| 371 |
+
cursor = conn.execute("""
|
| 372 |
+
SELECT
|
| 373 |
+
COUNT(*) as interaction_count,
|
| 374 |
+
AVG(sentiment_score) as avg_sentiment
|
| 375 |
+
FROM interactions
|
| 376 |
+
WHERE account_id = ?
|
| 377 |
+
""", (account_id,))
|
| 378 |
+
interaction_stats = cursor.fetchone()
|
| 379 |
+
|
| 380 |
+
return {
|
| 381 |
+
'contact_count': contact_count,
|
| 382 |
+
'interaction_count': interaction_stats['interaction_count'],
|
| 383 |
+
'avg_sentiment': interaction_stats['avg_sentiment'] or 0.0
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
def get_recent_interactions(self, user_id: str = None, limit: int = 10) -> List[Dict]:
|
| 387 |
+
"""Get recent interactions with account and user details"""
|
| 388 |
+
with self.get_db() as conn:
|
| 389 |
+
query = """
|
| 390 |
+
SELECT
|
| 391 |
+
i.*,
|
| 392 |
+
a.name as account_name,
|
| 393 |
+
u.name as owner_name,
|
| 394 |
+
a.industry as account_industry
|
| 395 |
+
FROM interactions i
|
| 396 |
+
JOIN accounts a ON i.account_id = a.id
|
| 397 |
+
JOIN users u ON i.owner_id = u.id
|
| 398 |
+
"""
|
| 399 |
+
params = []
|
| 400 |
+
|
| 401 |
+
if user_id:
|
| 402 |
+
query += " WHERE i.owner_id = ?"
|
| 403 |
+
params.append(user_id)
|
| 404 |
+
|
| 405 |
+
query += " ORDER BY i.created_at DESC LIMIT ?"
|
| 406 |
+
params.append(limit)
|
| 407 |
+
|
| 408 |
+
cursor = conn.execute(query, params)
|
| 409 |
+
interactions = []
|
| 410 |
+
|
| 411 |
+
for row in cursor:
|
| 412 |
+
interaction = dict(row)
|
| 413 |
+
# Parse JSON fields
|
| 414 |
+
try:
|
| 415 |
+
if interaction.get('metadata'):
|
| 416 |
+
interaction['metadata'] = json.loads(interaction['metadata'])
|
| 417 |
+
else:
|
| 418 |
+
interaction['metadata'] = {}
|
| 419 |
+
except json.JSONDecodeError:
|
| 420 |
+
interaction['metadata'] = {}
|
| 421 |
+
|
| 422 |
+
interactions.append(interaction)
|
| 423 |
+
|
| 424 |
+
return interactions
|
| 425 |
+
|
| 426 |
+
def get_contacts(self, account_id: str) -> List[Dict]:
|
| 427 |
+
"""Get contacts for an account with their relationships"""
|
| 428 |
+
with self.get_db() as conn:
|
| 429 |
+
cursor = conn.execute("""
|
| 430 |
+
SELECT
|
| 431 |
+
c.*,
|
| 432 |
+
c2.first_name as reports_to_first_name,
|
| 433 |
+
c2.last_name as reports_to_last_name
|
| 434 |
+
FROM contacts c
|
| 435 |
+
LEFT JOIN contacts c2 ON c.reports_to_id = c2.id
|
| 436 |
+
WHERE c.account_id = ?
|
| 437 |
+
ORDER BY c.influence_level DESC, c.first_name, c.last_name
|
| 438 |
+
""", (account_id,))
|
| 439 |
+
|
| 440 |
+
contacts = []
|
| 441 |
+
for row in cursor:
|
| 442 |
+
contact = dict(row)
|
| 443 |
+
# Parse JSON fields
|
| 444 |
+
try:
|
| 445 |
+
if contact.get('preferences'):
|
| 446 |
+
contact['preferences'] = json.loads(contact['preferences'])
|
| 447 |
+
else:
|
| 448 |
+
contact['preferences'] = {}
|
| 449 |
+
except json.JSONDecodeError:
|
| 450 |
+
contact['preferences'] = {}
|
| 451 |
+
|
| 452 |
+
contacts.append(contact)
|
| 453 |
+
|
| 454 |
+
return contacts
|
| 455 |
+
|
| 456 |
+
def get_dashboard_data(self) -> Tuple[Dict, pd.DataFrame, pd.DataFrame]:
|
| 457 |
+
"""Get aggregated data for dashboard"""
|
| 458 |
+
with self.get_db() as conn:
|
| 459 |
+
# Get counts
|
| 460 |
+
counts = {
|
| 461 |
+
'accounts': conn.execute('SELECT COUNT(*) FROM accounts').fetchone()[0],
|
| 462 |
+
'contacts': conn.execute('SELECT COUNT(*) FROM contacts').fetchone()[0],
|
| 463 |
+
'interactions': conn.execute('SELECT COUNT(*) FROM interactions').fetchone()[0]
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
# Get recent interactions
|
| 467 |
+
recent_interactions = pd.read_sql("""
|
| 468 |
+
SELECT
|
| 469 |
+
i.created_at, i.type,
|
| 470 |
+
a.name as account_name,
|
| 471 |
+
u.name as owner_name,
|
| 472 |
+
i.sentiment_score
|
| 473 |
+
FROM interactions i
|
| 474 |
+
JOIN accounts a ON i.account_id = a.id
|
| 475 |
+
JOIN users u ON i.owner_id = u.id
|
| 476 |
+
ORDER BY i.created_at DESC
|
| 477 |
+
LIMIT 10
|
| 478 |
+
""", conn)
|
| 479 |
+
|
| 480 |
+
# Get account distribution
|
| 481 |
+
account_distribution = pd.read_sql("""
|
| 482 |
+
SELECT industry, COUNT(*) as count
|
| 483 |
+
FROM accounts
|
| 484 |
+
GROUP BY industry
|
| 485 |
+
""", conn)
|
| 486 |
+
|
| 487 |
+
return counts, recent_interactions, account_distribution
|
| 488 |
+
|
| 489 |
+
def save_interaction(self, interaction_data: Dict[str, Any]) -> str:
|
| 490 |
+
"""Save a new interaction to the database"""
|
| 491 |
+
with self.get_db() as conn:
|
| 492 |
+
cursor = conn.cursor()
|
| 493 |
+
|
| 494 |
+
# Ensure required fields
|
| 495 |
+
required_fields = ['id', 'type', 'account_id', 'owner_id', 'created_at']
|
| 496 |
+
for field in required_fields:
|
| 497 |
+
if field not in interaction_data:
|
| 498 |
+
raise ValueError(f"Missing required field: {field}")
|
| 499 |
+
|
| 500 |
+
# Convert any dict/list fields to JSON
|
| 501 |
+
if 'metadata' in interaction_data and isinstance(interaction_data['metadata'], (dict, list)):
|
| 502 |
+
interaction_data['metadata'] = json.dumps(interaction_data['metadata'])
|
| 503 |
+
|
| 504 |
+
# Build query dynamically based on provided fields
|
| 505 |
+
fields = interaction_data.keys()
|
| 506 |
+
placeholders = ','.join(['?' for _ in fields])
|
| 507 |
+
query = f"INSERT INTO interactions ({','.join(fields)}) VALUES ({placeholders})"
|
| 508 |
+
|
| 509 |
+
cursor.execute(query, list(interaction_data.values()))
|
| 510 |
+
conn.commit()
|
| 511 |
+
|
| 512 |
+
return interaction_data['id']
|
| 513 |
|
| 514 |
+
def add_account(self, account_data: Dict[str, Any]) -> str:
|
| 515 |
+
"""Add a new account to the database"""
|
| 516 |
+
account_id = str(uuid.uuid4())
|
| 517 |
+
account_data['id'] = account_id
|
| 518 |
+
account_data['created_at'] = datetime.now().isoformat()
|
| 519 |
+
account_data['last_activity_at'] = datetime.now().isoformat()
|
| 520 |
+
|
| 521 |
+
with self.get_db() as conn:
|
| 522 |
+
c = conn.cursor()
|
| 523 |
+
placeholders = ', '.join(['?' for _ in account_data])
|
| 524 |
+
columns = ', '.join(account_data.keys())
|
| 525 |
+
sql = f'INSERT INTO accounts ({columns}) VALUES ({placeholders})'
|
| 526 |
+
c.execute(sql, list(account_data.values()))
|
| 527 |
+
conn.commit()
|
| 528 |
+
|
| 529 |
+
return account_id
|
| 530 |
+
|
| 531 |
+
def add_contact(self, contact_data: Dict[str, Any]) -> str:
|
| 532 |
+
"""Add a new contact to the database"""
|
| 533 |
+
contact_id = str(uuid.uuid4())
|
| 534 |
+
contact_data['id'] = contact_id
|
| 535 |
+
contact_data['created_at'] = datetime.now().isoformat()
|
| 536 |
+
contact_data['last_contacted'] = datetime.now().isoformat()
|
| 537 |
+
|
| 538 |
+
with self.get_db() as conn:
|
| 539 |
+
c = conn.cursor()
|
| 540 |
+
placeholders = ', '.join(['?' for _ in contact_data])
|
| 541 |
+
columns = ', '.join(contact_data.keys())
|
| 542 |
+
sql = f'INSERT INTO contacts ({columns}) VALUES ({placeholders})'
|
| 543 |
+
c.execute(sql, list(contact_data.values()))
|
| 544 |
+
conn.commit()
|
| 545 |
+
|
| 546 |
+
return contact_id
|
| 547 |
+
|
| 548 |
+
def update_account(self, account_id: str, update_data: Dict[str, Any]) -> bool:
|
| 549 |
+
"""Update an existing account"""
|
| 550 |
+
update_data['last_activity_at'] = datetime.now().isoformat()
|
| 551 |
+
|
| 552 |
+
with self.get_db() as conn:
|
| 553 |
+
c = conn.cursor()
|
| 554 |
+
set_clause = ', '.join([f"{k} = ?" for k in update_data.keys()])
|
| 555 |
+
sql = f'UPDATE accounts SET {set_clause} WHERE id = ?'
|
| 556 |
+
values = list(update_data.values()) + [account_id]
|
| 557 |
+
c.execute(sql, values)
|
| 558 |
+
conn.commit()
|
| 559 |
+
return c.rowcount > 0
|
| 560 |
+
|
| 561 |
+
def update_contact(self, contact_id: str, update_data: Dict[str, Any]) -> bool:
|
| 562 |
+
"""Update an existing contact"""
|
| 563 |
+
update_data['last_contacted'] = datetime.now().isoformat()
|
| 564 |
+
|
| 565 |
+
with self.get_db() as conn:
|
| 566 |
+
c = conn.cursor()
|
| 567 |
+
set_clause = ', '.join([f"{k} = ?" for k in update_data.keys()])
|
| 568 |
+
sql = f'UPDATE contacts SET {set_clause} WHERE id = ?'
|
| 569 |
+
values = list(update_data.values()) + [contact_id]
|
| 570 |
+
c.execute(sql, values)
|
| 571 |
+
conn.commit()
|
| 572 |
+
return c.rowcount > 0
|
| 573 |
+
|
| 574 |
+
def get_account_timeline(self, account_id: str, days: int = 90) -> List[Dict]:
|
| 575 |
+
"""Get timeline of account activities"""
|
| 576 |
+
cutoff_date = (datetime.now() - timedelta(days=days)).isoformat()
|
| 577 |
+
|
| 578 |
+
with self.get_db() as conn:
|
| 579 |
+
cursor = conn.execute("""
|
| 580 |
+
SELECT
|
| 581 |
+
'interaction' as event_type,
|
| 582 |
+
i.id,
|
| 583 |
+
i.type as subtype,
|
| 584 |
+
i.created_at,
|
| 585 |
+
i.summary as description,
|
| 586 |
+
i.sentiment_score,
|
| 587 |
+
u.name as actor_name
|
| 588 |
+
FROM interactions i
|
| 589 |
+
JOIN users u ON i.owner_id = u.id
|
| 590 |
+
WHERE i.account_id = ? AND i.created_at > ?
|
| 591 |
+
ORDER BY i.created_at DESC
|
| 592 |
+
""", (account_id, cutoff_date))
|
| 593 |
+
|
| 594 |
+
timeline = []
|
| 595 |
+
for row in cursor:
|
| 596 |
+
event = dict(row)
|
| 597 |
+
timeline.append(event)
|
| 598 |
+
|
| 599 |
+
return timeline
|
| 600 |
+
|
| 601 |
+
def get_account_details(self, account_id: str) -> Optional[Dict]:
|
| 602 |
+
"""Get detailed account information"""
|
| 603 |
+
with self.get_db() as conn:
|
| 604 |
+
cursor = conn.execute("""
|
| 605 |
+
SELECT
|
| 606 |
+
a.*,
|
| 607 |
+
u.name as owner_name,
|
| 608 |
+
COUNT(DISTINCT c.id) as contact_count,
|
| 609 |
+
COUNT(DISTINCT i.id) as interaction_count,
|
| 610 |
+
AVG(i.sentiment_score) as avg_sentiment
|
| 611 |
+
FROM accounts a
|
| 612 |
+
LEFT JOIN users u ON a.account_owner_id = u.id
|
| 613 |
+
LEFT JOIN contacts c ON a.id = c.account_id
|
| 614 |
+
LEFT JOIN interactions i ON a.id = i.account_id
|
| 615 |
+
WHERE a.id = ?
|
| 616 |
+
GROUP BY a.id
|
| 617 |
+
""", (account_id,))
|
| 618 |
+
|
| 619 |
+
row = cursor.fetchone()
|
| 620 |
+
if row:
|
| 621 |
+
account = dict(row)
|
| 622 |
+
try:
|
| 623 |
+
account['technology_stack'] = json.loads(account['technology_stack'])
|
| 624 |
+
except (json.JSONDecodeError, TypeError):
|
| 625 |
+
account['technology_stack'] = []
|
| 626 |
+
return account
|
| 627 |
+
return None
|
| 628 |
+
|
| 629 |
+
def search_accounts(self, query: str, limit: int = 10) -> List[Dict]:
|
| 630 |
+
"""Search accounts by name or industry"""
|
| 631 |
+
search_term = f"%{query}%"
|
| 632 |
+
|
| 633 |
+
with self.get_db() as conn:
|
| 634 |
+
cursor = conn.execute("""
|
| 635 |
+
SELECT
|
| 636 |
+
a.*,
|
| 637 |
+
u.name as owner_name
|
| 638 |
+
FROM accounts a
|
| 639 |
+
LEFT JOIN users u ON a.account_owner_id = u.id
|
| 640 |
+
WHERE
|
| 641 |
+
a.name LIKE ? OR
|
| 642 |
+
a.industry LIKE ? OR
|
| 643 |
+
a.website LIKE ?
|
| 644 |
+
LIMIT ?
|
| 645 |
+
""", (search_term, search_term, search_term, limit))
|
| 646 |
+
|
| 647 |
+
return [dict(row) for row in cursor]
|
| 648 |
+
|
| 649 |
+
def search_contacts(self, query: str, account_id: Optional[str] = None, limit: int = 10) -> List[Dict]:
|
| 650 |
+
"""Search contacts by name or email"""
|
| 651 |
+
search_term = f"%{query}%"
|
| 652 |
+
|
| 653 |
+
with self.get_db() as conn:
|
| 654 |
+
sql = """
|
| 655 |
+
SELECT
|
| 656 |
+
c.*,
|
| 657 |
+
a.name as account_name
|
| 658 |
+
FROM contacts c
|
| 659 |
+
JOIN accounts a ON c.account_id = a.id
|
| 660 |
+
WHERE
|
| 661 |
+
(c.first_name LIKE ? OR
|
| 662 |
+
c.last_name LIKE ? OR
|
| 663 |
+
c.email LIKE ?)
|
| 664 |
+
"""
|
| 665 |
+
params = [search_term, search_term, search_term]
|
| 666 |
+
|
| 667 |
+
if account_id:
|
| 668 |
+
sql += " AND c.account_id = ?"
|
| 669 |
+
params.append(account_id)
|
| 670 |
+
|
| 671 |
+
sql += " LIMIT ?"
|
| 672 |
+
params.append(limit)
|
| 673 |
+
|
| 674 |
+
cursor = conn.execute(sql, params)
|
| 675 |
+
return [dict(row) for row in cursor]
|
| 676 |
+
|
| 677 |
+
def search_interactions(self, query: str, user_id: Optional[str] = None, limit: int = 10) -> List[Dict]:
|
| 678 |
+
"""Search interactions by content"""
|
| 679 |
+
search_term = f"%{query}%"
|
| 680 |
+
|
| 681 |
+
with self.get_db() as conn:
|
| 682 |
+
sql = """
|
| 683 |
+
SELECT
|
| 684 |
+
i.*,
|
| 685 |
+
a.name as account_name,
|
| 686 |
+
u.name as owner_name
|
| 687 |
+
FROM interactions i
|
| 688 |
+
JOIN accounts a ON i.account_id = a.id
|
| 689 |
+
JOIN users u ON i.owner_id = u.id
|
| 690 |
+
WHERE
|
| 691 |
+
(i.transcript LIKE ? OR
|
| 692 |
+
i.summary LIKE ?)
|
| 693 |
+
"""
|
| 694 |
+
params = [search_term, search_term]
|
| 695 |
+
|
| 696 |
+
if user_id:
|
| 697 |
+
sql += " AND i.owner_id = ?"
|
| 698 |
+
params.append(user_id)
|
| 699 |
+
|
| 700 |
+
sql += " ORDER BY i.created_at DESC LIMIT ?"
|
| 701 |
+
params.append(limit)
|
| 702 |
+
|
| 703 |
+
cursor = conn.execute(sql, params)
|
| 704 |
+
interactions = []
|
| 705 |
+
|
| 706 |
+
for row in cursor:
|
| 707 |
+
interaction = dict(row)
|
| 708 |
+
try:
|
| 709 |
+
if interaction.get('metadata'):
|
| 710 |
+
interaction['metadata'] = json.loads(interaction['metadata'])
|
| 711 |
+
except json.JSONDecodeError:
|
| 712 |
+
interaction['metadata'] = {}
|
| 713 |
+
interactions.append(interaction)
|
| 714 |
+
|
| 715 |
+
return interactions
|