Source code for sherpa_ai.agents.user_agent_manager

"""User-specific agent management module for Sherpa AI.

This module provides user-specific agent management capabilities, including
personalized agent creation, user preferences, and multi-user support.
"""

import json
from datetime import datetime
from typing import Any, Dict, List, Optional, Set

from loguru import logger
from pydantic import BaseModel, Field

from sherpa_ai.agents.base import BaseAgent
from sherpa_ai.agents.persistent_agent_pool import PersistentAgentPool, AgentMetadata


[docs] class UserPreferences(BaseModel): """User preferences for agent management. Attributes: user_id (str): Unique user identifier. default_agent_type (str): Default agent type for new agents. max_agents (int): Maximum number of agents per user. auto_save (bool): Whether to automatically save agent state. preferred_llm (str): Preferred LLM model. notification_settings (Dict[str, Any]): Notification preferences. created_at (datetime): When preferences were created. updated_at (datetime): When preferences were last updated. """ user_id: str = Field(..., description="Unique user identifier") default_agent_type: str = Field(default="QAAgent", description="Default agent type for new agents") max_agents: int = Field(default=10, description="Maximum number of agents per user") auto_save: bool = Field(default=True, description="Whether to automatically save agent state") preferred_llm: str = Field(default="gpt-3.5-turbo", description="Preferred LLM model") notification_settings: Dict[str, Any] = Field(default_factory=dict, description="Notification preferences") created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow)
[docs] class UserAgentSession(BaseModel): """User agent session information. Attributes: session_id (str): Unique session identifier. user_id (str): User who owns this session. agent_id (str): Agent being used in this session. started_at (datetime): When the session started. last_activity (datetime): Last activity timestamp. is_active (bool): Whether the session is currently active. session_data (Dict[str, Any]): Additional session data. """ session_id: str = Field(..., description="Unique session identifier") user_id: str = Field(..., description="User who owns this session") agent_id: str = Field(..., description="Agent being used in this session") started_at: datetime = Field(default_factory=datetime.utcnow) last_activity: datetime = Field(default_factory=datetime.utcnow) is_active: bool = Field(default=True) session_data: Dict[str, Any] = Field(default_factory=dict)
[docs] class UserAgentManager: """Manages user-specific agents and their interactions. This class provides comprehensive user-specific agent management, including personalized agent creation, session management, and user preference handling. Attributes: agent_pool (PersistentAgentPool): The persistent agent pool. user_preferences (Dict[str, UserPreferences]): User preferences cache. active_sessions (Dict[str, UserAgentSession]): Active user sessions. """ def __init__(self, db_path: str = "user_agents.db"): """Initialize the user agent manager. Args: db_path (str): Path to the database file. Example: >>> manager = UserAgentManager("my_user_agents.db") >>> print(manager.agent_pool.storage_path) my_user_agents.db """ self.agent_pool = PersistentAgentPool(db_path, "sqlite") self.user_preferences: Dict[str, UserPreferences] = {} self.active_sessions: Dict[str, UserAgentSession] = {} # Initialize user preferences table self._init_user_preferences_table() def _init_user_preferences_table(self): """Initialize the user preferences table in the database.""" try: import sqlite3 with sqlite3.connect(self.agent_pool.storage_path) as conn: cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS user_preferences ( user_id TEXT PRIMARY KEY, default_agent_type TEXT NOT NULL DEFAULT 'QAAgent', max_agents INTEGER NOT NULL DEFAULT 10, auto_save BOOLEAN NOT NULL DEFAULT 1, preferred_llm TEXT NOT NULL DEFAULT 'gpt-3.5-turbo', notification_settings TEXT, -- JSON created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL ) """) conn.commit() except Exception as e: logger.error(f"Failed to initialize user preferences table: {e}")
[docs] def create_user(self, user_id: str, preferences: Optional[UserPreferences] = None, **kwargs) -> UserPreferences: """Create a new user with default or custom preferences. Args: user_id (str): Unique user identifier. preferences (Optional[UserPreferences]): Custom user preferences. **kwargs: Additional preferences to override defaults. Returns: UserPreferences: The created user preferences. Example: >>> manager = UserAgentManager() >>> prefs = manager.create_user("user123", max_agents=5) >>> print(prefs.max_agents) 5 """ if preferences is None: # Create preferences with any additional kwargs preferences = UserPreferences(user_id=user_id, **kwargs) # Save to database self._save_user_preferences(preferences) # Cache preferences self.user_preferences[user_id] = preferences logger.info(f"Created user: {user_id}") return preferences
[docs] def get_user_preferences(self, user_id: str) -> Optional[UserPreferences]: """Get user preferences. Args: user_id (str): User identifier. Returns: Optional[UserPreferences]: User preferences or None if not found. Example: >>> manager = UserAgentManager() >>> prefs = manager.get_user_preferences("user123") >>> print(prefs.max_agents if prefs else "User not found") 10 """ # Check cache first if user_id in self.user_preferences: return self.user_preferences[user_id] # Load from database try: import sqlite3 with sqlite3.connect(self.agent_pool.storage_path) as conn: cursor = conn.cursor() cursor.execute(""" SELECT default_agent_type, max_agents, auto_save, preferred_llm, notification_settings, created_at, updated_at FROM user_preferences WHERE user_id = ? """, (user_id,)) row = cursor.fetchone() if row: default_agent_type, max_agents, auto_save, preferred_llm, notification_settings, created_at, updated_at = row prefs = UserPreferences( user_id=user_id, default_agent_type=default_agent_type, max_agents=max_agents, auto_save=bool(auto_save), preferred_llm=preferred_llm, notification_settings=json.loads(notification_settings) if notification_settings else {}, created_at=datetime.fromisoformat(created_at), updated_at=datetime.fromisoformat(updated_at) ) # Cache preferences self.user_preferences[user_id] = prefs return prefs except Exception as e: logger.error(f"Failed to get user preferences for {user_id}: {e}") return None
[docs] def update_user_preferences(self, user_id: str, **kwargs) -> bool: """Update user preferences. Args: user_id (str): User identifier. **kwargs: Preference fields to update. Returns: bool: True if successful, False otherwise. Example: >>> manager = UserAgentManager() >>> success = manager.update_user_preferences("user123", max_agents=20) >>> print("Updated" if success else "Failed") Updated """ try: prefs = self.get_user_preferences(user_id) if not prefs: return False # Update fields for key, value in kwargs.items(): if hasattr(prefs, key): setattr(prefs, key, value) prefs.updated_at = datetime.utcnow() # Save to database self._save_user_preferences(prefs) # Update cache self.user_preferences[user_id] = prefs logger.info(f"Updated preferences for user: {user_id}") return True except Exception as e: logger.error(f"Failed to update user preferences for {user_id}: {e}") return False
def _save_user_preferences(self, preferences: UserPreferences): """Save user preferences to database. Args: preferences (UserPreferences): Preferences to save. """ try: import sqlite3 with sqlite3.connect(self.agent_pool.storage_path) as conn: cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO user_preferences ( user_id, default_agent_type, max_agents, auto_save, preferred_llm, notification_settings, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( preferences.user_id, preferences.default_agent_type, preferences.max_agents, preferences.auto_save, preferences.preferred_llm, json.dumps(preferences.notification_settings), preferences.created_at.isoformat(), preferences.updated_at.isoformat() )) conn.commit() except Exception as e: logger.error(f"Failed to save user preferences: {e}") raise
[docs] def create_agent_for_user(self, user_id: str, agent_name: str, agent_type: Optional[str] = None, **kwargs) -> Optional[str]: """Create a new agent for a specific user. Args: user_id (str): User identifier. agent_name (str): Name for the new agent. agent_type (Optional[str]): Type of agent to create. **kwargs: Additional agent configuration. Returns: Optional[str]: Agent ID if successful, None otherwise. Example: >>> manager = UserAgentManager() >>> agent_id = manager.create_agent_for_user("user123", "My Assistant", "QAAgent") >>> print(agent_id if agent_id else "Failed") My Assistant_140123456789 """ try: # Get user preferences prefs = self.get_user_preferences(user_id) if not prefs: # Create user with default preferences prefs = self.create_user(user_id) # Check agent limit current_count = self.agent_pool.get_agent_count(user_id) if current_count >= prefs.max_agents: logger.warning(f"User {user_id} has reached maximum agent limit ({prefs.max_agents})") return None # Determine agent type if agent_type is None: agent_type = prefs.default_agent_type # Create agent instance agent = self._create_agent_instance(agent_type, agent_name, prefs, **kwargs) if not agent: return None # Save agent agent_id = self.agent_pool.save_agent(agent, user_id=user_id) logger.info(f"Created agent '{agent_name}' for user '{user_id}'") return agent_id except Exception as e: logger.error(f"Failed to create agent for user {user_id}: {e}") return None
def _create_agent_instance(self, agent_type: str, agent_name: str, preferences: UserPreferences, **kwargs) -> Optional[BaseAgent]: """Create an agent instance of the specified type. Args: agent_type (str): Type of agent to create. agent_name (str): Name for the agent. preferences (UserPreferences): User preferences. **kwargs: Additional configuration. Returns: Optional[BaseAgent]: Created agent instance or None if failed. """ try: # Import agent classes dynamically if agent_type == "QAAgent": from sherpa_ai.agents.qa_agent import QAAgent return QAAgent(name=agent_name, **kwargs) elif agent_type == "MLEngineer": from sherpa_ai.agents.ml_engineer import MLEngineer return MLEngineer(name=agent_name, **kwargs) elif agent_type == "Critic": from sherpa_ai.agents.critic import Critic return Critic(name=agent_name, **kwargs) elif agent_type == "UserAgent": from sherpa_ai.agents.user import UserAgent return UserAgent(name=agent_name, **kwargs) else: logger.error(f"Unknown agent type: {agent_type}") return None except ImportError as e: logger.error(f"Failed to import agent type {agent_type}: {e}") return None except Exception as e: logger.error(f"Failed to create agent instance: {e}") return None
[docs] def get_user_agents(self, user_id: str, active_only: bool = True) -> List[AgentMetadata]: """Get all agents for a specific user. Args: user_id (str): User identifier. active_only (bool): Only return active agents. Returns: List[AgentMetadata]: List of agent metadata. Example: >>> manager = UserAgentManager() >>> agents = manager.get_user_agents("user123") >>> for agent in agents: ... print(f"{agent.agent_name} ({agent.agent_type})") My Assistant (QAAgent) """ return self.agent_pool.list_agents(user_id=user_id, active_only=active_only)
[docs] def get_agent_for_user(self, user_id: str, agent_name: str) -> Optional[BaseAgent]: """Get a specific agent for a user. Args: user_id (str): User identifier. agent_name (str): Agent name. Returns: Optional[BaseAgent]: The agent if found, None otherwise. Example: >>> manager = UserAgentManager() >>> agent = manager.get_agent_for_user("user123", "My Assistant") >>> print(agent.name if agent else "Not found") My Assistant """ return self.agent_pool.get_agent_by_name(agent_name, user_id)
[docs] def start_user_session(self, user_id: str, agent_name: str) -> Optional[str]: """Start a new user session with an agent. Args: user_id (str): User identifier. agent_name (str): Agent name to use. Returns: Optional[str]: Session ID if successful, None otherwise. Example: >>> manager = UserAgentManager() >>> session_id = manager.start_user_session("user123", "My Assistant") >>> print(session_id if session_id else "Failed") session_140123456789 """ try: # Get agent agent = self.get_agent_for_user(user_id, agent_name) if not agent: logger.error(f"Agent '{agent_name}' not found for user '{user_id}'") return None # Create session session_id = f"session_{int(datetime.utcnow().timestamp())}" session = UserAgentSession( session_id=session_id, user_id=user_id, agent_id=agent.name, session_data={"agent_type": agent.__class__.__name__} ) # Store session self.active_sessions[session_id] = session logger.info(f"Started session {session_id} for user {user_id} with agent {agent_name}") return session_id except Exception as e: logger.error(f"Failed to start session for user {user_id}: {e}") return None
[docs] def end_user_session(self, session_id: str) -> bool: """End a user session. Args: session_id (str): Session identifier. Returns: bool: True if successful, False otherwise. Example: >>> manager = UserAgentManager() >>> success = manager.end_user_session("session_140123456789") >>> print("Ended" if success else "Failed") Ended """ try: if session_id in self.active_sessions: session = self.active_sessions[session_id] session.is_active = False session.last_activity = datetime.utcnow() # Remove from active sessions del self.active_sessions[session_id] logger.info(f"Ended session {session_id}") return True else: logger.warning(f"Session {session_id} not found") return False except Exception as e: logger.error(f"Failed to end session {session_id}: {e}") return False
[docs] def get_user_session(self, session_id: str) -> Optional[UserAgentSession]: """Get user session information. Args: session_id (str): Session identifier. Returns: Optional[UserAgentSession]: Session information or None if not found. Example: >>> manager = UserAgentManager() >>> session = manager.get_user_session("session_140123456789") >>> print(session.user_id if session else "Not found") user123 """ return self.active_sessions.get(session_id)
[docs] def get_user_active_sessions(self, user_id: str) -> List[UserAgentSession]: """Get all active sessions for a user. Args: user_id (str): User identifier. Returns: List[UserAgentSession]: List of active sessions. Example: >>> manager = UserAgentManager() >>> sessions = manager.get_user_active_sessions("user123") >>> print(f"User has {len(sessions)} active sessions") User has 2 active sessions """ return [session for session in self.active_sessions.values() if session.user_id == user_id and session.is_active]
[docs] def auto_save_agent_state(self, user_id: str, agent: BaseAgent) -> bool: """Automatically save agent state if user has auto-save enabled. Args: user_id (str): User identifier. agent (BaseAgent): Agent to save. Returns: bool: True if saved or auto-save disabled, False if failed. Example: >>> manager = UserAgentManager() >>> success = manager.auto_save_agent_state("user123", agent) >>> print("Saved" if success else "Failed") Saved """ try: prefs = self.get_user_preferences(user_id) if not prefs or not prefs.auto_save: return True # Auto-save disabled, consider it successful # Update agent in pool agent_id = f"{agent.name}_{id(agent)}" return self.agent_pool.update_agent(agent_id, agent) except Exception as e: logger.error(f"Failed to auto-save agent state for user {user_id}: {e}") return False
[docs] def get_user_statistics(self, user_id: str) -> Dict[str, Any]: """Get statistics for a user. Args: user_id (str): User identifier. Returns: Dict[str, Any]: User statistics. Example: >>> manager = UserAgentManager() >>> stats = manager.get_user_statistics("user123") >>> print(f"User has {stats['total_agents']} agents") User has 5 agents """ try: agents = self.get_user_agents(user_id) active_sessions = self.get_user_active_sessions(user_id) # Count agents by type agent_types = {} for agent in agents: agent_type = agent.agent_type agent_types[agent_type] = agent_types.get(agent_type, 0) + 1 return { "user_id": user_id, "total_agents": len(agents), "active_agents": len([a for a in agents if a.is_active]), "agent_types": agent_types, "active_sessions": len(active_sessions), "max_agents": self.get_user_preferences(user_id).max_agents if self.get_user_preferences(user_id) else 10, "last_updated": datetime.utcnow().isoformat() } except Exception as e: logger.error(f"Failed to get user statistics for {user_id}: {e}") return {"error": str(e)}
[docs] def cleanup_inactive_sessions(self, max_age_hours: int = 24) -> int: """Clean up inactive sessions older than specified hours. Args: max_age_hours (int): Maximum age of sessions in hours. Returns: int: Number of sessions cleaned up. Example: >>> manager = UserAgentManager() >>> cleaned = manager.cleanup_inactive_sessions(12) >>> print(f"Cleaned up {cleaned} sessions") Cleaned up 3 sessions """ try: from datetime import timedelta cutoff_time = datetime.utcnow() - timedelta(hours=max_age_hours) sessions_to_remove = [] for session_id, session in self.active_sessions.items(): if session.last_activity < cutoff_time: sessions_to_remove.append(session_id) # Remove old sessions for session_id in sessions_to_remove: del self.active_sessions[session_id] logger.info(f"Cleaned up {len(sessions_to_remove)} inactive sessions") return len(sessions_to_remove) except Exception as e: logger.error(f"Failed to cleanup inactive sessions: {e}") return 0