aspira-modern / database.py
arnavam's picture
updated the groq_key provider & graph in frontend
b2e554a
Raw
History Blame Contribute Delete
11.4 kB
import os
import json
import time
import asyncio
from logger_config import get_logger
from datetime import datetime
from dotenv import load_dotenv
from pymongo import AsyncMongoClient, UpdateOne
from pymongo.errors import DuplicateKeyError, ServerSelectionTimeoutError, ConnectionFailure
# Load environment variables
load_dotenv()
logger = get_logger(__name__)
class Database:
def __init__(self, uri=None, db_name="aspira_db"):
if uri is None:
uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
# Configure client to handle idle drops and timeouts properly
self.client = AsyncMongoClient(
uri,
# Proactively close connections before load balancers (usually ~4 mins) drop them
maxIdleTimeMS=210000,
serverSelectionTimeoutMS=10000 # Wait 10s per attempt to find a server
)
self.db = self.client[db_name]
async def initialize(self, retries=5, delay=5):
"""Tries to connect and initialize indexes, waiting if the Atlas cluster is paused."""
for attempt in range(1, retries + 1):
try:
# Ping the database to force connection and check availability
await self.client.admin.command('ping')
await self._init_indexes()
logger.info(
"Successfully connected to MongoDB and initialized indexes.")
return
except (ServerSelectionTimeoutError, ConnectionFailure) as e:
logger.warning(f"MongoDB connection failed on attempt {attempt}/{retries}. Cluster might be waking up. Retrying in {delay}s...")
if attempt == retries:
logger.error(
"Failed to connect to MongoDB after multiple retries. The backend might fail.")
raise e
await asyncio.sleep(delay)
async def _init_indexes(self):
"""Create indexes for performance and uniqueness"""
# User uniqueness
await self.db.users.create_index("username", unique=True)
# Meta: user_id + key unique
await self.db.meta.create_index([("user_id", 1), ("key", 1)], unique=True)
# Keywords: user_id + conversation_id + keyword unique
await self.db.keywords.create_index(
[("user_id", 1), ("conversation_id", 1), ("keyword", 1)], unique=True)
# QA Cache: user_id + question unique
await self.db.qa_cache.create_index(
[("user_id", 1), ("question", 1)], unique=True)
# Conversation: index on user_id and conversation_id for fast retrieval
await self.db.conversation.create_index(
[("user_id", 1), ("conversation_id", 1), ("id", 1)])
# Scores: user_id unique
await self.db.scores.create_index("user_id", unique=True)
# --- USER MANAGEMENT ---
async def create_user(self, username, password_hash, groq_api_key_encrypted=""):
try:
result = await self.db.users.insert_one({
"username": username,
"password_hash": password_hash,
"groq_api_key": groq_api_key_encrypted,
"created_at": datetime.utcnow()
})
return str(result.inserted_id)
except DuplicateKeyError:
return None
async def get_user(self, username):
return await self.db.users.find_one({"username": username})
async def get_user_by_id(self, user_id: str):
from bson.objectid import ObjectId
try:
return await self.db.users.find_one({"_id": ObjectId(user_id)})
except Exception:
return None
# --- SESSION_COUNTER ---
async def get_session_counter(self, user_id: str) -> int:
doc = await self.db.meta.find_one(
{"user_id": user_id, "key": "session_counter"})
return int(doc["value"]) if doc else 0
async def increment_session_counter(self, user_id: str) -> int:
# Atomic increment
result = await self.db.meta.find_one_and_update(
{"user_id": user_id, "key": "session_counter"},
{"$inc": {"value": 1}},
upsert=True,
return_document=True
)
return result["value"]
# --- RESUME ---
async def save_resume(self, user_id: str, text: str):
await self.db.meta.update_one(
{"user_id": user_id, "key": "resume"},
{"$set": {"value": text, "updated_at": datetime.utcnow()}},
upsert=True
)
async def get_resume(self, user_id: str) -> str:
doc = await self.db.meta.find_one({"user_id": user_id, "key": "resume"})
return doc["value"] if doc else None
# --- KEYWORDS (KW) ---
async def get_keywords(self, user_id: str, conversation_id: str = "default") -> dict:
cursor = self.db.keywords.find({"user_id": user_id})
kw = {}
async for doc in cursor:
# Default to 1 if it's missing
version = doc.get("schema_version", 1)
# If it's the old state, pretend it belongs to the "default" conversation
if version == 1:
if conversation_id == "default":
kw[doc["keyword"]] = doc["scores"]
# If it's the new state, strictly match the conversation_id
elif version == 2:
if doc.get("conversation_id") == conversation_id:
kw[doc["keyword"]] = doc["scores"]
return kw
async def update_keywords(self, user_id: str, kw_dict: dict, conversation_id: str = "default"):
"""Replaces keywords for the user."""
try:
await self.db.keywords.delete_many(
{"user_id": user_id, "conversation_id": conversation_id})
if kw_dict:
await self.db.keywords.insert_many([
{"user_id": user_id, "conversation_id": conversation_id, "keyword": k, "scores": v,
"updated_at": datetime.utcnow(), "schema_version": 2}
for k, v in kw_dict.items()
])
except Exception as e:
logger.error(f"Error updating keywords: {e}")
# --- QA ---
async def update_qa(self, user_id: str, qa_dict: dict):
ops = []
for q, a in qa_dict.items():
ops.append(UpdateOne(
{"user_id": user_id, "question": q},
{"$set": {"answer_chunk": a, "created_at": datetime.utcnow()}},
upsert=True
))
if ops:
try:
await self.db.qa_cache.bulk_write(ops)
except Exception as e:
logger.error(f"Error updating QA: {e}")
async def get_qa(self, user_id: str) -> dict:
cursor = self.db.qa_cache.find({"user_id": user_id})
return {doc["question"]: doc["answer_chunk"] async for doc in cursor}
# --- CONVERSATION HISTORY ---
async def add_conversation_message(self, user_id: str, message: str, conversation_id: str = "default"):
role = "user" if message.startswith("User:") else "interviewer"
await self.db.conversation.insert_one({
"user_id": user_id,
"conversation_id": conversation_id,
"message": message,
"role": role,
"timestamp": datetime.utcnow(),
"schema_version": 2
})
async def get_conversation_history(self, user_id: str, conversation_id: str = "default") -> list:
cursor = self.db.conversation.find(
{"user_id": user_id}).sort("timestamp", 1)
history = []
async for doc in cursor:
version = doc.get("schema_version", 1) # Default to 1 if missing
# Legacy records (version 1) default to "default" conversation
if version == 1 and conversation_id == "default":
history.append(doc["message"])
# New records (version 2) check the explicit conversation_id
elif version == 2 and doc.get("conversation_id") == conversation_id:
history.append(doc["message"])
return history
async def get_conversations(self, user_id: str) -> list:
"""Returns a list of unique conversation IDs for the user."""
return await self.db.conversation.distinct("conversation_id", {"user_id": user_id})
# --- SCORES ---
async def get_score(self, user_id: str) -> dict:
doc = await self.db.scores.find_one({"user_id": user_id})
return doc.get("data", {}) if doc else {}
async def update_score(self, user_id: str, score_data: dict):
"""Update user's score data (accumulated interview performance)."""
try:
await self.db.scores.update_one(
{"user_id": user_id},
{"$set": {"data": score_data, "updated_at": datetime.utcnow()}},
upsert=True
)
except Exception as e:
logger.error(f"Error updating score: {e}")
# --- METADATA ---
async def get_interview_metadata(self, user_id: str, conversation_id: str) -> dict:
doc = await self.db.metadata.find_one({"user_id": user_id, "conversation_id": conversation_id})
return doc.get("metadata", {}) if doc else {}
async def save_interview_metadata(self, user_id: str, conversation_id: str, metadata: dict):
try:
await self.db.metadata.update_one(
{"user_id": user_id, "conversation_id": conversation_id},
{"$set": {"metadata": metadata, "updated_at": datetime.utcnow()}},
upsert=True
)
except Exception as e:
logger.error(f"Error updating interview metadata: {e}")
# --- EVALUATION ---
async def get_evaluation(self, user_id: str, conversation_id: str) -> dict:
doc = await self.db.evaluations.find_one({"user_id": user_id, "conversation_id": conversation_id})
return doc.get("evaluation", {}) if doc else {}
async def save_evaluation(self, user_id: str, conversation_id: str, evaluation: dict):
try:
await self.db.evaluations.update_one(
{"user_id": user_id, "conversation_id": conversation_id},
{"$set": {"evaluation": evaluation, "updated_at": datetime.utcnow()}},
upsert=True
)
except Exception as e:
logger.error(f"Error updating evaluation: {e}")
# --- KNOWLEDGE GRAPH ---
async def get_knowledge_graph(self, user_id: str, conversation_id: str) -> dict:
"""Retrieve the knowledge graph for a specific conversation."""
doc = await self.db.knowledge_graphs.find_one(
{"user_id": user_id, "conversation_id": conversation_id}
)
return doc.get("graph_data", {}) if doc else {}
async def save_knowledge_graph(self, user_id: str, conversation_id: str, graph_data: dict):
"""Upsert the knowledge graph for a specific conversation."""
try:
await self.db.knowledge_graphs.update_one(
{"user_id": user_id, "conversation_id": conversation_id},
{"$set": {"graph_data": graph_data, "updated_at": datetime.utcnow()}},
upsert=True
)
except Exception as e:
logger.error(f"Error saving knowledge graph: {e}")