from datetime import datetime, timedelta from bson import ObjectId import database from database import users_collection, agents_collection, agent_usage_collection import auth from auth import hash_password, verify_password async def get_user_by_email(email: str): return await users_collection.find_one({"email": email}) async def get_user_by_azure_ad_id(azure_ad_id: str): """Get user by Azure AD Object ID""" return await users_collection.find_one({"azure_ad_id": azure_ad_id}) async def create_user(email: str, password: str = None, full_name: str = None, is_admin: bool = False, auth_provider: str = "local"): now = datetime.utcnow() user_doc = { "email": email, "full_name": full_name, "is_active": True, "is_admin": is_admin, "auth_provider": auth_provider, "created_at": now, "updated_at": now, } # Only add hashed_password for local auth users if auth_provider == "local" and password: user_doc["hashed_password"] = hash_password(password) result = await users_collection.insert_one(user_doc) user_doc["_id"] = result.inserted_id return user_doc async def create_or_update_azure_user(azure_profile: dict): """ Create or update user from Azure AD profile Returns the user document """ azure_ad_id = azure_profile.get("azure_ad_id") email = azure_profile.get("email") if not azure_ad_id or not email: raise ValueError("Azure AD profile missing required fields") now = datetime.utcnow() # Check if user exists by Azure AD ID first, then by email existing_user = await get_user_by_azure_ad_id(azure_ad_id) if not existing_user: existing_user = await get_user_by_email(email) if existing_user: # Update existing user with Azure AD info update_data = { "azure_ad_id": azure_ad_id, "email": email, "full_name": azure_profile.get("full_name") or existing_user.get("full_name"), "auth_provider": "azure_ad", "updated_at": now, } await users_collection.update_one( {"_id": existing_user["_id"]}, {"$set": update_data} ) # Return updated user return await users_collection.find_one({"_id": existing_user["_id"]}) else: # Create new user from Azure AD profile user_doc = { "azure_ad_id": azure_ad_id, "email": email, "full_name": azure_profile.get("full_name"), "is_active": True, "is_admin": False, # New users are not admin by default "auth_provider": "azure_ad", "created_at": now, "updated_at": now, } result = await users_collection.insert_one(user_doc) user_doc["_id"] = result.inserted_id return user_doc async def authenticate_user(email: str, password: str): """Authenticate user with local credentials (fallback method)""" print(f"🔐 CRUD: Authenticating user {email}") user = await get_user_by_email(email) if not user: print(f"🔐 CRUD: User {email} not found in database") return None print(f"🔐 CRUD: User found - auth_provider: {user.get('auth_provider')}") print(f"🔐 CRUD: Has hashed_password: {'hashed_password' in user}") # Only authenticate local users with password if user.get("auth_provider") != "local": print(f"🔐 CRUD: User is not local auth provider: {user.get('auth_provider')}") return None if not user.get("hashed_password"): print("🔐 CRUD: User has no hashed_password field") return None print("🔐 CRUD: Verifying password...") password_valid = verify_password(password, user["hashed_password"]) print(f"🔐 CRUD: Password verification result: {password_valid}") if not password_valid: return None print("🔐 CRUD: Authentication successful!") return user # Agent CRUD operations async def create_agent(agent_data: dict, user_id: str): # Check for duplicate agent names from the same user created recently (within 5 minutes) from datetime import timedelta five_minutes_ago = datetime.utcnow() - timedelta(minutes=5) existing_agent = await agents_collection.find_one({ "agent_name": agent_data.get("agent_name"), "created_by": user_id, "created_at": {"$gte": five_minutes_ago} }) if existing_agent: raise ValueError(f"Agent with name '{agent_data.get('agent_name')}' was already created recently. Please wait before creating another agent with the same name.") now = datetime.utcnow() agent_doc = { **agent_data, "created_by": user_id, "created_at": now, "updated_at": now, } result = await agents_collection.insert_one(agent_doc) agent_doc["_id"] = result.inserted_id return agent_doc async def get_agent_by_id(agent_id: str): try: return await agents_collection.find_one({"_id": ObjectId(agent_id)}) except: return None async def get_agents_by_user(user_id: str, status_filter: str = None, limit: int = None): query = {"created_by": user_id} if status_filter: query["agent_status"] = status_filter cursor = agents_collection.find(query).sort("created_at", -1) if limit: cursor = cursor.limit(limit) return await cursor.to_list(length=None) async def get_all_agents(status_filter: str = None, limit: int = None): query = {} if status_filter: query["agent_status"] = status_filter cursor = agents_collection.find(query).sort("created_at", -1) if limit: cursor = cursor.limit(limit) return await cursor.to_list(length=None) async def search_agents(search_term: str, user_id: str = None): """Search agents by name, description, or tags""" query = { "$or": [ {"agent_name": {"$regex": search_term, "$options": "i"}}, {"agent_description": {"$regex": search_term, "$options": "i"}}, {"agent_tags": {"$regex": search_term, "$options": "i"}}, {"agent_department": {"$regex": search_term, "$options": "i"}} ] } if user_id: query["created_by"] = user_id return await agents_collection.find(query).sort("created_at", -1).to_list(length=None) async def get_agent_stats(): """Get agent statistics""" pipeline = [ { "$group": { "_id": "$agent_status", "count": {"$sum": 1} } } ] stats = await agents_collection.aggregate(pipeline).to_list(length=None) return {stat["_id"]: stat["count"] for stat in stats} async def update_agent(agent_id: str, update_data: dict, user_id: str = None): try: filter_query = {"_id": ObjectId(agent_id)} if user_id: filter_query["created_by"] = user_id update_data["updated_at"] = datetime.utcnow() result = await agents_collection.update_one( filter_query, {"$set": update_data} ) if result.modified_count: return await get_agent_by_id(agent_id) return None except: return None async def delete_agent(agent_id: str, user_id: str = None): try: print(f"🗑️ CRUD: Deleting agent {agent_id} with user_id filter: {user_id}") filter_query = {"_id": ObjectId(agent_id)} if user_id: filter_query["created_by"] = user_id print(f"🗑️ CRUD: Filter query: {filter_query}") result = await agents_collection.delete_one(filter_query) print(f"🗑️ CRUD: Delete result - deleted_count: {result.deleted_count}") return result.deleted_count > 0 except Exception as e: print(f"🗑️ CRUD: Exception during delete: {e}") return False async def get_user_by_id(user_id: str): try: return await users_collection.find_one({"_id": ObjectId(user_id)}) except: return None async def get_all_users(): return await users_collection.find({}).to_list(length=None) async def update_user(user_id: str, update_data: dict): try: update_data["updated_at"] = datetime.utcnow() result = await users_collection.update_one( {"_id": ObjectId(user_id)}, {"$set": update_data} ) if result.modified_count: return await get_user_by_id(user_id) return None except: return None async def delete_user(user_id: str): try: result = await users_collection.delete_one({"_id": ObjectId(user_id)}) return result.deleted_count > 0 except: return False async def get_agent_by_name(agent_name: str): """Get agent by exact name match globally""" return await agents_collection.find_one({"agent_name": agent_name}) def _agent_data_differs(existing_agent: dict, new_agent_data: dict) -> bool: """Compare agent data excluding name and metadata fields to detect differences""" comparable_fields = [ "agent_description", "agent_purpose", "agent_version", "agent_status", "agent_location", "agent_department", "agent_contact_person", "agent_tags", "agent_userbase", "agent_capabilities", "agent_metadata" ] for field in comparable_fields: existing_value = existing_agent.get(field) new_value = new_agent_data.get(field) if existing_value != new_value: return True return False async def create_agent_usage_record(agent_name: str, agent_data: dict): """Create usage record for existing agent and update main record if data differs""" now = datetime.utcnow() usage_doc = { "agent_name": agent_name, "agent_data": agent_data, "timestamp": now, "created_at": now } try: result = await agent_usage_collection.insert_one(usage_doc) existing_agent = await get_agent_by_name(agent_name) if existing_agent and _agent_data_differs(existing_agent, agent_data): update_data = {k: v for k, v in agent_data.items() if k != "agent_name"} update_data["updated_at"] = now await agents_collection.update_one( {"agent_name": agent_name}, {"$set": update_data} ) return result.inserted_id except Exception as e: raise Exception(f"Failed to store agent usage data: {str(e)}") async def get_agent_usage_stats(agent_name: str, start_date: datetime = None, end_date: datetime = None): """Get usage statistics for an agent within date range""" query = {"agent_name": agent_name} if start_date or end_date: date_filter = {} if start_date: date_filter["$gte"] = start_date if end_date: date_filter["$lte"] = end_date query["timestamp"] = date_filter # Get total count total_count = await agent_usage_collection.count_documents(query) # Get first and last usage first_usage = await agent_usage_collection.find_one(query, sort=[("timestamp", 1)]) last_usage = await agent_usage_collection.find_one(query, sort=[("timestamp", -1)]) return { "total_usage_count": total_count, "first_usage": first_usage["timestamp"] if first_usage else None, "last_usage": last_usage["timestamp"] if last_usage else None } async def get_agent_usage_by_period(agent_name: str, period: str = "daily", start_date: datetime = None, end_date: datetime = None): """Get usage data grouped by time period (daily, weekly, monthly)""" query = {"agent_name": agent_name} if start_date or end_date: date_filter = {} if start_date: date_filter["$gte"] = start_date if end_date: date_filter["$lte"] = end_date query["timestamp"] = date_filter # Define grouping format based on period if period == "daily": date_format = "%Y-%m-%d" elif period == "weekly": date_format = "%Y-W%U" elif period == "monthly": date_format = "%Y-%m" else: date_format = "%Y-%m-%d" pipeline = [ {"$match": query}, { "$group": { "_id": {"$dateToString": {"format": date_format, "date": "$timestamp"}}, "count": {"$sum": 1} } }, {"$sort": {"_id": 1}} ] results = await agent_usage_collection.aggregate(pipeline).to_list(length=None) return {result["_id"]: result["count"] for result in results} async def create_agent_from_collector(agent_data: dict): """Create agent from agent collector API data (no user ownership)""" now = datetime.utcnow() # Set automatic timestamps if not provided if not agent_data.get("agent_created_at"): agent_data["agent_created_at"] = now.isoformat() if not agent_data.get("agent_updated_at"): agent_data["agent_updated_at"] = now.isoformat() agent_doc = { **agent_data, "created_by": "agent_collector_api", # Special marker for agent collector created agents "created_at": now, "updated_at": now, } try: result = await agents_collection.insert_one(agent_doc) agent_doc["_id"] = result.inserted_id return agent_doc except Exception as e: raise Exception(f"Failed to store agent data: {str(e)}")