Spaces:
Sleeping
Sleeping
Update src/core/services/database_service.py
Browse files- src/core/services/database_service.py +136 -107
src/core/services/database_service.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Enhanced database service with
|
| 2 |
import sqlite3
|
| 3 |
from contextlib import contextmanager
|
| 4 |
from pathlib import Path
|
|
@@ -8,15 +8,28 @@ from datetime import datetime, timedelta
|
|
| 8 |
import random
|
| 9 |
from faker import Faker
|
| 10 |
import uuid
|
| 11 |
-
|
| 12 |
|
| 13 |
class DatabaseService:
|
| 14 |
-
def __init__(self, db_path: str = "data/robata.db"):
|
| 15 |
-
"""Initialize database service"""
|
|
|
|
| 16 |
self.db_path = db_path
|
| 17 |
self.fake = Faker()
|
| 18 |
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
| 19 |
self.setup_database()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
@contextmanager
|
| 22 |
def get_db(self):
|
|
@@ -29,6 +42,7 @@ class DatabaseService:
|
|
| 29 |
conn.close()
|
| 30 |
|
| 31 |
def setup_database(self):
|
|
|
|
| 32 |
with self.get_db() as conn:
|
| 33 |
c = conn.cursor()
|
| 34 |
c.executescript('''
|
|
@@ -64,133 +78,148 @@ class DatabaseService:
|
|
| 64 |
FOREIGN KEY (parent_account_id) REFERENCES accounts (id),
|
| 65 |
FOREIGN KEY (account_owner_id) REFERENCES users (id)
|
| 66 |
);
|
| 67 |
-
|
| 68 |
-
CREATE TABLE IF NOT EXISTS contacts (
|
| 69 |
-
id TEXT PRIMARY KEY,
|
| 70 |
-
account_id TEXT,
|
| 71 |
-
first_name TEXT,
|
| 72 |
-
last_name TEXT,
|
| 73 |
-
email TEXT,
|
| 74 |
-
phone TEXT,
|
| 75 |
-
title TEXT,
|
| 76 |
-
department TEXT,
|
| 77 |
-
reports_to_id TEXT,
|
| 78 |
-
influence_level TEXT,
|
| 79 |
-
engagement_score REAL,
|
| 80 |
-
preferences TEXT,
|
| 81 |
-
created_at TEXT,
|
| 82 |
-
last_contacted TEXT,
|
| 83 |
-
FOREIGN KEY (account_id) REFERENCES accounts (id),
|
| 84 |
-
FOREIGN KEY (reports_to_id) REFERENCES contacts (id)
|
| 85 |
-
);
|
| 86 |
-
CREATE TABLE IF NOT EXISTS interactions (
|
| 87 |
-
id TEXT PRIMARY KEY,
|
| 88 |
-
type TEXT,
|
| 89 |
-
account_id TEXT,
|
| 90 |
-
owner_id TEXT,
|
| 91 |
-
transcript TEXT,
|
| 92 |
-
summary TEXT,
|
| 93 |
-
sentiment_score REAL,
|
| 94 |
-
metadata TEXT,
|
| 95 |
-
created_at TEXT,
|
| 96 |
-
FOREIGN KEY (account_id) REFERENCES accounts (id),
|
| 97 |
-
FOREIGN KEY (owner_id) REFERENCES users (id)
|
| 98 |
-
);
|
| 99 |
''')
|
| 100 |
|
| 101 |
def generate_synthetic_data(self):
|
| 102 |
-
"""Generate
|
| 103 |
with self.get_db() as conn:
|
| 104 |
c = conn.cursor()
|
| 105 |
-
|
| 106 |
-
# Configuration
|
| 107 |
-
NUM_USERS = 50
|
| 108 |
-
NUM_ACCOUNTS = 50
|
| 109 |
-
NUM_CONTACTS = 300
|
| 110 |
-
|
| 111 |
-
# Helper function to generate timestamp within last year
|
| 112 |
-
def random_date():
|
| 113 |
-
days = random.randint(0, 365)
|
| 114 |
-
return (datetime.now() - timedelta(days=days)).isoformat()
|
| 115 |
-
|
| 116 |
# Generate Users
|
| 117 |
-
roles = ['sales_rep'] * 35 + ['regional_lead'] * 10 + ['head_of_sales'] * 5
|
| 118 |
-
departments = ['Sales', 'Consulting', 'Technology', 'Operations']
|
| 119 |
-
regions = ['North', 'South', 'East', 'West', 'Central']
|
| 120 |
-
|
| 121 |
users = []
|
| 122 |
-
for
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
users.append({
|
| 126 |
-
'id':
|
| 127 |
'email': self.fake.company_email(),
|
| 128 |
'name': self.fake.name(),
|
| 129 |
-
'role':
|
| 130 |
-
'department':
|
| 131 |
-
'title':
|
| 132 |
-
'region': random.choice(
|
| 133 |
'quota': random.uniform(500000, 2000000),
|
| 134 |
-
'created_at':
|
| 135 |
-
'last_login':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
def get_user_by_email(self, email: str) -> Optional[Dict]:
|
| 144 |
"""Get user by email address"""
|
| 145 |
with self.get_db() as conn:
|
| 146 |
-
# Print all users for debugging
|
| 147 |
-
all_users = conn.execute("SELECT email FROM users").fetchall()
|
| 148 |
-
print(f"Available users: {[u[0] for u in all_users]}")
|
| 149 |
-
|
| 150 |
cursor = conn.execute(
|
| 151 |
"SELECT * FROM users WHERE email = ?",
|
| 152 |
(email,)
|
| 153 |
)
|
| 154 |
row = cursor.fetchone()
|
| 155 |
-
if row
|
| 156 |
-
|
| 157 |
-
print(f"No user found for email: {email}")
|
| 158 |
-
return None
|
| 159 |
-
|
| 160 |
def get_user_accounts(self, user_id: str) -> List[Dict]:
|
| 161 |
"""Get accounts associated with user"""
|
| 162 |
with self.get_db() as conn:
|
| 163 |
cursor = conn.execute("""
|
| 164 |
-
SELECT
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
ORDER BY a.name
|
| 168 |
""", (user_id,))
|
| 169 |
-
return [dict(row) for row in cursor.fetchall()]
|
| 170 |
-
|
| 171 |
-
def get_account_metrics(self, account_id: str) -> Dict:
|
| 172 |
-
"""Get metrics for a specific account"""
|
| 173 |
-
with self.get_db() as conn:
|
| 174 |
-
contacts_count = conn.execute(
|
| 175 |
-
"SELECT COUNT(*) FROM contacts WHERE account_id = ?",
|
| 176 |
-
(account_id,)
|
| 177 |
-
).fetchone()[0]
|
| 178 |
-
|
| 179 |
-
return {
|
| 180 |
-
'contact_count': contacts_count,
|
| 181 |
-
'interaction_count': 0, # Default for now
|
| 182 |
-
'avg_sentiment': 0 # Default for now
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
def get_recent_interactions(self, user_id: str, limit: int = 10) -> List[Dict]:
|
| 186 |
-
"""Get recent interactions for a user"""
|
| 187 |
-
with self.get_db() as conn:
|
| 188 |
-
cursor = conn.execute("""
|
| 189 |
-
SELECT i.*, a.name as account_name
|
| 190 |
-
FROM interactions i
|
| 191 |
-
JOIN accounts a ON i.account_id = a.id
|
| 192 |
-
WHERE i.owner_id = ?
|
| 193 |
-
ORDER BY i.created_at DESC
|
| 194 |
-
LIMIT ?
|
| 195 |
-
""", (user_id, limit))
|
| 196 |
-
return [dict(row) for row in cursor.fetchall()]
|
|
|
|
| 1 |
+
"""Enhanced database service with persistent storage and account management"""
|
| 2 |
import sqlite3
|
| 3 |
from contextlib import contextmanager
|
| 4 |
from pathlib import Path
|
|
|
|
| 8 |
import random
|
| 9 |
from faker import Faker
|
| 10 |
import uuid
|
| 11 |
+
import os
|
| 12 |
|
| 13 |
class DatabaseService:
|
| 14 |
+
def __init__(self, db_path: str = "/data/robata.db"):
|
| 15 |
+
"""Initialize database service with persistent storage"""
|
| 16 |
+
# Use /data directory for persistence in Hugging Face Spaces
|
| 17 |
self.db_path = db_path
|
| 18 |
self.fake = Faker()
|
| 19 |
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
| 20 |
self.setup_database()
|
| 21 |
+
|
| 22 |
+
# Only generate data if database is empty
|
| 23 |
+
if not self._has_data():
|
| 24 |
+
self.generate_synthetic_data()
|
| 25 |
+
|
| 26 |
+
def _has_data(self) -> bool:
|
| 27 |
+
"""Check if database has existing data"""
|
| 28 |
+
with self.get_db() as conn:
|
| 29 |
+
cursor = conn.cursor()
|
| 30 |
+
cursor.execute("SELECT COUNT(*) FROM users")
|
| 31 |
+
user_count = cursor.fetchone()[0]
|
| 32 |
+
return user_count > 0
|
| 33 |
|
| 34 |
@contextmanager
|
| 35 |
def get_db(self):
|
|
|
|
| 42 |
conn.close()
|
| 43 |
|
| 44 |
def setup_database(self):
|
| 45 |
+
"""Set up database schema"""
|
| 46 |
with self.get_db() as conn:
|
| 47 |
c = conn.cursor()
|
| 48 |
c.executescript('''
|
|
|
|
| 78 |
FOREIGN KEY (parent_account_id) REFERENCES accounts (id),
|
| 79 |
FOREIGN KEY (account_owner_id) REFERENCES users (id)
|
| 80 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
''')
|
| 82 |
|
| 83 |
def generate_synthetic_data(self):
|
| 84 |
+
"""Generate synthetic users and accounts with proper linkage"""
|
| 85 |
with self.get_db() as conn:
|
| 86 |
c = conn.cursor()
|
| 87 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
# Generate Users
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
users = []
|
| 90 |
+
user_ids = [] # Keep track of user IDs for account assignment
|
| 91 |
+
|
| 92 |
+
# Predefined test user
|
| 93 |
+
test_user_id = str(uuid.uuid4())
|
| 94 |
+
users.append({
|
| 95 |
+
'id': test_user_id,
|
| 96 |
+
'email': 'test@example.com', # Default test login
|
| 97 |
+
'name': 'Test User',
|
| 98 |
+
'role': 'sales_rep',
|
| 99 |
+
'department': 'Sales',
|
| 100 |
+
'title': 'Senior Sales Representative',
|
| 101 |
+
'region': 'North',
|
| 102 |
+
'quota': 1000000.0,
|
| 103 |
+
'created_at': datetime.now().isoformat(),
|
| 104 |
+
'last_login': datetime.now().isoformat()
|
| 105 |
+
})
|
| 106 |
+
user_ids.append(test_user_id)
|
| 107 |
+
|
| 108 |
+
# Generate additional users
|
| 109 |
+
for _ in range(10):
|
| 110 |
+
user_id = str(uuid.uuid4())
|
| 111 |
+
user_ids.append(user_id)
|
| 112 |
users.append({
|
| 113 |
+
'id': user_id,
|
| 114 |
'email': self.fake.company_email(),
|
| 115 |
'name': self.fake.name(),
|
| 116 |
+
'role': random.choice(['sales_rep', 'regional_lead', 'head_of_sales']),
|
| 117 |
+
'department': random.choice(['Sales', 'Consulting', 'Technology']),
|
| 118 |
+
'title': 'Senior Sales Representative',
|
| 119 |
+
'region': random.choice(['North', 'South', 'East', 'West']),
|
| 120 |
'quota': random.uniform(500000, 2000000),
|
| 121 |
+
'created_at': datetime.now().isoformat(),
|
| 122 |
+
'last_login': datetime.now().isoformat()
|
| 123 |
+
})
|
| 124 |
+
|
| 125 |
+
# Insert users
|
| 126 |
+
c.executemany('''
|
| 127 |
+
INSERT OR REPLACE INTO users VALUES (
|
| 128 |
+
:id, :email, :name, :role, :department, :title, :region,
|
| 129 |
+
:quota, :created_at, :last_login
|
| 130 |
+
)
|
| 131 |
+
''', users)
|
| 132 |
+
|
| 133 |
+
# Generate Accounts
|
| 134 |
+
accounts = []
|
| 135 |
+
industries = ['Technology', 'Healthcare', 'Financial Services', 'Manufacturing', 'Retail']
|
| 136 |
+
|
| 137 |
+
# Ensure test user has accounts
|
| 138 |
+
for _ in range(3):
|
| 139 |
+
accounts.append({
|
| 140 |
+
'id': str(uuid.uuid4()),
|
| 141 |
+
'name': self.fake.company(),
|
| 142 |
+
'parent_account_id': None,
|
| 143 |
+
'industry': random.choice(industries),
|
| 144 |
+
'status': 'active',
|
| 145 |
+
'website': self.fake.url(),
|
| 146 |
+
'annual_revenue': random.uniform(1000000, 100000000),
|
| 147 |
+
'employee_count': random.randint(50, 10000),
|
| 148 |
+
'technology_stack': json.dumps(['Python', 'React', 'AWS']),
|
| 149 |
+
'region': 'North',
|
| 150 |
+
'address': self.fake.address(),
|
| 151 |
+
'account_owner_id': test_user_id, # Assign to test user
|
| 152 |
+
'engagement_score': random.uniform(0, 100),
|
| 153 |
+
'created_at': datetime.now().isoformat(),
|
| 154 |
+
'last_activity_at': datetime.now().isoformat()
|
| 155 |
})
|
| 156 |
+
|
| 157 |
+
# Generate additional accounts
|
| 158 |
+
for user_id in user_ids:
|
| 159 |
+
for _ in range(random.randint(2, 5)):
|
| 160 |
+
accounts.append({
|
| 161 |
+
'id': str(uuid.uuid4()),
|
| 162 |
+
'name': self.fake.company(),
|
| 163 |
+
'parent_account_id': None,
|
| 164 |
+
'industry': random.choice(industries),
|
| 165 |
+
'status': 'active',
|
| 166 |
+
'website': self.fake.url(),
|
| 167 |
+
'annual_revenue': random.uniform(1000000, 100000000),
|
| 168 |
+
'employee_count': random.randint(50, 10000),
|
| 169 |
+
'technology_stack': json.dumps(['Python', 'React', 'AWS']),
|
| 170 |
+
'region': random.choice(['North', 'South', 'East', 'West']),
|
| 171 |
+
'address': self.fake.address(),
|
| 172 |
+
'account_owner_id': user_id,
|
| 173 |
+
'engagement_score': random.uniform(0, 100),
|
| 174 |
+
'created_at': datetime.now().isoformat(),
|
| 175 |
+
'last_activity_at': datetime.now().isoformat()
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
# Insert accounts
|
| 179 |
+
c.executemany('''
|
| 180 |
+
INSERT OR REPLACE INTO accounts VALUES (
|
| 181 |
+
:id, :name, :parent_account_id, :industry, :status, :website,
|
| 182 |
+
:annual_revenue, :employee_count, :technology_stack, :region,
|
| 183 |
+
:address, :account_owner_id, :engagement_score, :created_at,
|
| 184 |
+
:last_activity_at
|
| 185 |
+
)
|
| 186 |
+
''', accounts)
|
| 187 |
+
|
| 188 |
+
conn.commit()
|
| 189 |
|
| 190 |
+
def add_account(self, account_data: Dict[str, Any]) -> str:
|
| 191 |
+
"""Add a new account to the database"""
|
| 192 |
+
account_id = str(uuid.uuid4())
|
| 193 |
+
account_data['id'] = account_id
|
| 194 |
+
account_data['created_at'] = datetime.now().isoformat()
|
| 195 |
+
account_data['last_activity_at'] = datetime.now().isoformat()
|
| 196 |
+
|
| 197 |
+
with self.get_db() as conn:
|
| 198 |
+
c = conn.cursor()
|
| 199 |
+
placeholders = ', '.join(['?' for _ in account_data])
|
| 200 |
+
columns = ', '.join(account_data.keys())
|
| 201 |
+
sql = f'INSERT INTO accounts ({columns}) VALUES ({placeholders})'
|
| 202 |
+
c.execute(sql, list(account_data.values()))
|
| 203 |
+
conn.commit()
|
| 204 |
+
|
| 205 |
+
return account_id
|
| 206 |
|
| 207 |
def get_user_by_email(self, email: str) -> Optional[Dict]:
|
| 208 |
"""Get user by email address"""
|
| 209 |
with self.get_db() as conn:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
cursor = conn.execute(
|
| 211 |
"SELECT * FROM users WHERE email = ?",
|
| 212 |
(email,)
|
| 213 |
)
|
| 214 |
row = cursor.fetchone()
|
| 215 |
+
return dict(row) if row else None
|
| 216 |
+
|
|
|
|
|
|
|
|
|
|
| 217 |
def get_user_accounts(self, user_id: str) -> List[Dict]:
|
| 218 |
"""Get accounts associated with user"""
|
| 219 |
with self.get_db() as conn:
|
| 220 |
cursor = conn.execute("""
|
| 221 |
+
SELECT * FROM accounts
|
| 222 |
+
WHERE account_owner_id = ?
|
| 223 |
+
ORDER BY name
|
|
|
|
| 224 |
""", (user_id,))
|
| 225 |
+
return [dict(row) for row in cursor.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|