cryogenic22 commited on
Commit
1956fa7
·
verified ·
1 Parent(s): 29232b5

Update src/core/services/database_service.py

Browse files
Files changed (1) hide show
  1. 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 complete database schema"""
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
- -- Opportunity tracking
129
- CREATE TABLE IF NOT EXISTS opportunities (
130
- id TEXT PRIMARY KEY,
131
- name TEXT NOT NULL,
132
- account_id TEXT NOT NULL,
133
- primary_contact_id TEXT,
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
- async def generate_synthetic_data(self):
200
- """Generate realistic synthetic data using LLM"""
201
- if not self.llm:
202
- logger.warning("No LLM service provided, using basic synthetic data")
203
- self._generate_basic_synthetic_data()
204
- return
205
-
 
 
 
206
  try:
207
  with self.get_db() as conn:
208
- c = conn.cursor()
209
-
210
- # Generate base scenario with LLM
211
- scenario = await self._generate_business_scenario()
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- # Generate users first
214
- users = await self._generate_users(scenario)
215
- c.executemany('''
216
- INSERT INTO users VALUES (
217
- :id, :email, :name, :role, :department, :title, :region,
218
- :quota, :created_at, :last_login
219
- )
220
- ''', users)
 
 
 
 
 
221
 
222
- # Additional generation logic for accounts, contacts, etc.
223
- conn.commit()
224
  except Exception as e:
225
- logger.error(f"Error generating synthetic data: {str(e)}")
226
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- if __name__ == "__main__":
229
- logging.basicConfig(level=logging.INFO)
230
- db_service = DatabaseService(db_path="./robata.db")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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