diff --git a/examples/preference_agent_demo.py b/examples/preference_agent_demo.py new file mode 100644 index 00000000..ed040f27 --- /dev/null +++ b/examples/preference_agent_demo.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +""" +PreferenceAgent Demo - Enhanced User Profile Analysis + +This example demonstrates the new PreferenceAgent class that provides AI-powered +user behavior analysis and structured profile generation for social media simulation. + +The PreferenceAgent analyzes user historical data and generates comprehensive profiles +including interests, behavioral preferences, interaction styles, and community roles. +""" + +import asyncio +import json +from typing import Dict, Any + +from oasis.user_profile_agent.agent import PreferenceAgent +from oasis.user_profile_agent.types import UserPreferenceProfile +from camel.models import ModelFactory +from camel.types import ModelType, ModelPlatformType + + +def create_sample_user_data(): + """Create sample user data to demonstrate PreferenceAgent analysis.""" + + # Sample user historical behavior data + user_history = { + "user_id": 1, + "actions": { + "create_post": [ + "Just discovered a fascinating new ML algorithm for natural language processing! The attention mechanism improvements are incredible. #MachineLearning #AI", + "Sharing my latest tutorial on Python decorators. Hope this helps fellow developers! 🐍 #Programming #Tutorial", + "The future of AI in software development is here. Excited to see how GitHub Copilot evolves! #AI #SoftwareDevelopment", + "Working on a new deep learning project. The transformer architecture never ceases to amaze me. #DeepLearning #Research" + ], + "create_comment": [ + "Great question! Here's how you can optimize that algorithm for better performance...", + "I've had similar experiences with this framework. The key is to understand the underlying architecture.", + "Excellent point about model efficiency. Have you considered using quantization techniques?", + "This is exactly what the community needs - more practical tutorials like this!" + ], + "like_post": [], + "sign_up": [], + "refresh": [], + "do_nothing": [] + }, + "time_distribution": { + "morning": 2, + "afternoon": 5, + "evening": 8, + "night": 3 + } + } + + # Sample existing user profile data (could be empty for new users) + existing_profile = { + "basic_info": { + "name": + "TechExpert_Alice", + "bio": + "Senior software engineer passionate about AI and machine learning" + } + } + + # Sample agent profile information + agent_profile = { + "agent_id": 1, + "name": "TechExpert_Alice", + "bio": + "Senior software engineer passionate about AI and machine learning. Loves sharing technical insights and helping others learn.", + "personality_traits": ["analytical", "helpful", "detail-oriented"], + "background": "Computer Science PhD, 8 years industry experience" + } + + return user_history, existing_profile, agent_profile + + +async def demonstrate_preference_agent(): + """Demonstrate the PreferenceAgent's profile analysis capabilities.""" + + print("🤖 PreferenceAgent Demo - AI-Powered User Profile Analysis") + print("=" * 60) + + # Initialize the model for PreferenceAgent + print("🔧 Initializing AI model for profile analysis...") + try: + # Use Qwen model with Aliyun endpoint for better regional compatibility + model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.QWEN_MAX, + url="https://dashscope.aliyuncs.com/compatible-mode/v1", + ) + print("✅ Model initialized successfully (using Qwen via Aliyun)") + except Exception as e: + print(f"❌ Failed to initialize model: {e}") + print("💡 Make sure you have set your API keys and model configuration") + return + + # Create PreferenceAgent instance + print("\n🧠 Creating PreferenceAgent...") + preference_agent = PreferenceAgent(model=model) + print("✅ PreferenceAgent created successfully") + + # Prepare sample user data + print("\n📊 Preparing sample user data...") + user_history, existing_profile, agent_profile = create_sample_user_data() + + print("📝 Sample user data prepared:") + print(f" • Posts created: {len(user_history['actions']['create_post'])}") + print( + f" • Comments made: {len(user_history['actions']['create_comment'])}" + ) + print( + f" • Most active period: evening ({user_history['time_distribution']['evening']} actions)" + ) + print(f" • User bio: {agent_profile['bio']}") + + # Demonstrate profile analysis + await demonstrate_profile_analysis(preference_agent, user_history, + existing_profile, agent_profile) + + # Show different scenarios + await demonstrate_new_user_scenario(preference_agent) + + print("\n🎯 PreferenceAgent Demo Completed!") + + +async def demonstrate_profile_analysis(preference_agent, user_history, + existing_profile, agent_profile): + """Demonstrate comprehensive profile analysis.""" + + print("\n🔍 Analyzing User Profile with AI...") + print("-" * 40) + + try: + # Call the PreferenceAgent to analyze user data + print("🤖 Running AI analysis on user behavior patterns...") + + # The analyse method expects a tuple of (history, profile, agent_profiles) + user_profiles_tuple = (user_history, existing_profile, agent_profile) + + # Perform the analysis + analyzed_profile = await preference_agent.analyse(user_profiles_tuple) + + print("✅ Profile analysis completed!") + + # Check if we got valid results + if analyzed_profile and 'profile_summary' in analyzed_profile: + # Display the results + display_analysis_results(analyzed_profile) + else: + print("⚠️ API returned no data, showing mock results instead...") + analyzed_profile = get_mock_profile_analysis() + display_analysis_results(analyzed_profile) + + except Exception as e: + print(f"❌ Profile analysis failed: {e}") + print( + "💡 Showing mock results to demonstrate expected functionality...") + + # Show realistic mock results + analyzed_profile = get_mock_profile_analysis() + display_analysis_results(analyzed_profile) + + +def display_analysis_results(profile): + """Display the analysis results in a formatted way.""" + print("\n📊 AI Analysis Results:") + print("=" * 30) + + # Handle both dict and Pydantic model formats + if hasattr(profile, 'profile_summary'): + # Pydantic model format + summary = profile.profile_summary + interests = [interest.value if hasattr(interest, 'value') else str(interest) for interest in summary.interests] + + print(f"🎯 Identified Interests ({len(interests)}):") + for interest in interests: + print(f" • {interest}") + + # Display preferences + if summary.preferences: + prefs = summary.preferences + print(f"\n✍️ Content Style:") + print(f" Description: {prefs.content_style or 'N/A'}") + + # Handle content style tags + content_tags = [] + if prefs.content_style_tags: + content_tags = [tag.value if hasattr(tag, 'value') else str(tag) for tag in prefs.content_style_tags] + print(f" Tags: {content_tags}") + + print(f"\n💬 Interaction Style:") + print(f" Description: {prefs.interaction_style or 'N/A'}") + + # Handle interaction style tags + interaction_tags = [] + if prefs.interaction_style_tags: + interaction_tags = [tag.value if hasattr(tag, 'value') else str(tag) for tag in prefs.interaction_style_tags] + print(f" Tags: {interaction_tags}") + + # Handle active periods + active_periods = [] + if prefs.active_periods: + active_periods = [period.value if hasattr(period, 'value') else str(period) for period in prefs.active_periods] + print(f"\n🕒 Active Periods: {active_periods}") + + # Display behavioral summary + if summary.behavioral_summary: + print(f"\n🧠 Behavioral Analysis:") + print(f" {summary.behavioral_summary}") + + # Handle behavioral archetype tags + if summary.behavioral_archetype_tags: + behavioral_tags = [tag.value if hasattr(tag, 'value') else str(tag) for tag in summary.behavioral_archetype_tags] + print(f" Archetype Tags: {behavioral_tags}") + + # Display community profile + if summary.community_profile: + cp = summary.community_profile + print(f"\n👥 Community Profile:") + print(f" Affinity: {cp.affinity or 'N/A'}") + print(f" Potential Role: {cp.potential_role or 'N/A'}") + + # Handle role tags + role_tags = [] + if cp.potential_role_tags: + role_tags = [tag.value if hasattr(tag, 'value') else str(tag) for tag in cp.potential_role_tags] + print(f" Role Tags: {role_tags}") + + elif isinstance(profile, dict) and 'profile_summary' in profile: + # Original dict format (fallback) + summary = profile['profile_summary'] + interests = summary.get('interests', []) + print(f"🎯 Identified Interests ({len(interests)}):") + for interest in interests: + print(f" • {interest}") + + # Display preferences + if 'preferences' in summary: + prefs = summary['preferences'] + print(f"\n✍️ Content Style:") + print(f" Description: {prefs.get('content_style', 'N/A')}") + print(f" Tags: {prefs.get('content_style_tags', [])}") + + print(f"\n💬 Interaction Style:") + print(f" Description: {prefs.get('interaction_style', 'N/A')}") + print(f" Tags: {prefs.get('interaction_style_tags', [])}") + + print(f"\n🕒 Active Periods: {prefs.get('active_periods', [])}") + + # Display behavioral summary + behavioral_summary = summary.get('behavioral_summary', '') + if behavioral_summary: + print(f"\n🧠 Behavioral Analysis:") + print(f" {behavioral_summary}") + + behavioral_tags = summary.get('behavioral_archetype_tags', []) + if behavioral_tags: + print(f" Archetype Tags: {behavioral_tags}") + + # Display community profile + if 'community_profile' in summary: + cp = summary['community_profile'] + print(f"\n👥 Community Profile:") + print(f" Affinity: {cp.get('affinity', 'N/A')}") + print(f" Potential Role: {cp.get('potential_role', 'N/A')}") + print(f" Role Tags: {cp.get('potential_role_tags', [])}") + else: + print("❌ No valid profile data received") + + +async def demonstrate_new_user_scenario(preference_agent): + """Demonstrate profile generation for a new user with minimal data.""" + + print("\n🆕 New User Scenario - Profile Generation from Bio Only") + print("-" * 50) + + # Create data for a new user with no history + new_user_history = { + "user_id": 2, + "actions": { + "create_post": [], + "create_comment": [], + "like_post": [], + "sign_up": [], + "refresh": [], + "do_nothing": [] + }, + "time_distribution": { + "morning": 0, + "afternoon": 0, + "evening": 0, + "night": 0 + } + } + + new_user_profile = {} + + new_agent_profile = { + "agent_id": 2, + "name": "BusinessGuru_Bob", + "bio": + "Entrepreneur and business strategist. Focuses on market trends, startup advice, and investment opportunities.", + "personality_traits": ["ambitious", "strategic", "networking-focused"] + } + + print("📝 New user data:") + print(f" • Name: {new_agent_profile['name']}") + print(f" • Bio: {new_agent_profile['bio']}") + print(f" • Historical actions: None") + + try: + print("\n🤖 Generating profile from bio and personality traits...") + + new_user_tuple = (new_user_history, new_user_profile, + new_agent_profile) + new_analyzed_profile = await preference_agent.analyse(new_user_tuple) + + print("✅ Profile generated successfully!") + + # Check if we got valid results + if not new_analyzed_profile or 'profile_summary' not in new_analyzed_profile: + print("⚠️ API returned no data, showing mock results instead...") + new_analyzed_profile = get_mock_new_user_profile() + + if new_analyzed_profile and 'profile_summary' in new_analyzed_profile: + summary = new_analyzed_profile['profile_summary'] + interests = summary.get('interests', []) + print(f"\n🎯 Predicted Interests: {interests}") + + if 'preferences' in summary: + content_tags = summary['preferences'].get( + 'content_style_tags', []) + interaction_tags = summary['preferences'].get( + 'interaction_style_tags', []) + print(f"✍️ Predicted Content Style: {content_tags}") + print(f"💬 Predicted Interaction Style: {interaction_tags}") + + except Exception as e: + print(f"❌ New user profile generation failed: {e}") + print( + "💡 Showing mock results to demonstrate expected functionality...") + + # Show realistic mock results for new user + new_analyzed_profile = get_mock_new_user_profile() + if new_analyzed_profile and 'profile_summary' in new_analyzed_profile: + summary = new_analyzed_profile['profile_summary'] + interests = summary.get('interests', []) + print(f"\n🎯 Predicted Interests: {interests}") + + if 'preferences' in summary: + content_tags = summary['preferences'].get( + 'content_style_tags', []) + interaction_tags = summary['preferences'].get( + 'interaction_style_tags', []) + print(f"✍️ Predicted Content Style: {content_tags}") + print(f"💬 Predicted Interaction Style: {interaction_tags}") + + +def get_mock_profile_analysis(): + """Generate mock profile analysis data for demonstration.""" + return { + "profile_summary": { + "interests": + ["Artificial Intelligence", "Software Development", "Technology"], + "preferences": { + "content_style": + "Technical and educational content with detailed explanations and practical examples", + "content_style_tags": + ["Educational & Tutorials", "Professional Insights"], + "interaction_style": + "Helpful and supportive, professional tone with focus on knowledge sharing", + "interaction_style_tags": + ["Formal & Professional", "Supportive & Encouraging"], + "active_periods": ["下午", "晚上"] + }, + "behavioral_summary": + "User demonstrates expert technical knowledge and actively helps others learn. Shows consistent pattern of sharing detailed tutorials and engaging in helpful discussions about AI and programming topics.", + "behavioral_archetype_tags": + ["Thought Leader", "Knowledge Seeker"], + "community_profile": { + "affinity": + "Technical communities focused on AI and software development, particularly those emphasizing education and knowledge sharing", + "potential_role": + "Technical mentor and knowledge contributor who helps others understand complex concepts", + "potential_role_tags": + ["Expert Contributor", "Mentor & Guide"] + } + } + } + + +def get_mock_new_user_profile(): + """Generate mock profile for new user scenario.""" + return { + "profile_summary": { + "interests": [ + "Business Strategy", "Economics & Markets", + "Investing & Personal Finance" + ], + "preferences": { + "content_style": + "Strategic business insights with market analysis and practical advice for entrepreneurs", + "content_style_tags": + ["Professional Insights", "Opinion & Editorials"], + "interaction_style": + "Confident and strategic, focused on networking and business opportunities", + "interaction_style_tags": + ["Formal & Professional", "Direct & Straightforward"], + "active_periods": ["上午", "下午"] + }, + "behavioral_summary": + "User exhibits strong business acumen and entrepreneurial mindset. Likely to share market insights, startup advice, and investment strategies with a focus on practical business applications.", + "behavioral_archetype_tags": + ["Thought Leader", "Active Participant"], + "community_profile": { + "affinity": + "Business and entrepreneurship communities, particularly those focused on market trends and investment opportunities", + "potential_role": + "Business advisor and strategic thinker who provides market insights and startup guidance", + "potential_role_tags": + ["Expert Contributor", "Leader & Organizer"] + } + } + } + + +def show_expected_output_structure(): + """Show the expected output structure of PreferenceAgent analysis.""" + + print("\n📋 Expected PreferenceAgent Output Structure:") + print("-" * 45) + + expected_structure = { + "profile_summary": { + "interests": + ["Artificial Intelligence", "Software Development", "Technology"], + "preferences": { + "content_style": + "Technical and educational content with detailed explanations", + "content_style_tags": + ["Educational & Tutorials", "Professional Insights"], + "interaction_style": + "Helpful and supportive, professional tone", + "interaction_style_tags": + ["Formal & Professional", "Supportive & Encouraging"], + "active_periods": ["下午", "晚上"] + }, + "behavioral_summary": + "User demonstrates expert technical knowledge and actively helps others learn", + "behavioral_archetype_tags": + ["Thought Leader", "Knowledge Seeker"], + "community_profile": { + "affinity": + "Technical communities focused on AI and software development", + "potential_role": "Technical mentor and knowledge contributor", + "potential_role_tags": + ["Expert Contributor", "Mentor & Guide"] + } + } + } + + print(json.dumps(expected_structure, indent=2, ensure_ascii=False)) + + +def print_feature_overview(): + """Print an overview of PreferenceAgent features.""" + + print("\n" + "=" * 60) + print("🌟 PREFERENCEAGENT - AI-POWERED PROFILE ANALYSIS") + print("=" * 60) + print(""" +🔧 KEY FEATURES: + • AI-powered behavioral analysis using LLM + • Structured profile generation with Pydantic validation + • Support for both data-rich and new user scenarios + • Comprehensive categorization using standardized enums + • Automatic data type conversion and error handling + +📊 ANALYSIS COMPONENTS: + • User Interests: Categorized from predefined enum values + • Content Style: How users create and share content + • Interaction Style: Communication patterns and tone + • Active Periods: Temporal behavior patterns + • Behavioral Archetypes: Personality and engagement types + • Community Roles: Social roles and group affiliations + +🤖 TECHNICAL CAPABILITIES: + • Handles incomplete or missing data gracefully + • Converts string responses to proper array formats + • Provides fallback mechanisms for API failures + • Supports both historical data analysis and bio-based prediction + • Generates structured JSON output for easy integration + +💡 USE CASES: + • Dynamic user profiling in social media simulations + • Personalized content recommendation systems + • Behavioral pattern analysis for research + • Community formation and role assignment + • User segmentation and targeting +""") + print("=" * 60) + + +if __name__ == "__main__": + print_feature_overview() + + try: + asyncio.run(demonstrate_preference_agent()) + except KeyboardInterrupt: + print("\n⚠️ Demo interrupted by user") + except Exception as e: + print(f"\n❌ Demo failed: {e}") + import traceback + traceback.print_exc() diff --git a/oasis/environment/env.py b/oasis/environment/env.py index 4d815c60..0083acbd 100644 --- a/oasis/environment/env.py +++ b/oasis/environment/env.py @@ -12,10 +12,11 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== import asyncio +import json import logging import os from datetime import datetime -from typing import List, Union +from typing import List, Union, Dict, Any from oasis.environment.env_action import LLMAction, ManualAction from oasis.social_agent.agent import SocialAgent @@ -25,6 +26,7 @@ from oasis.social_platform.platform import Platform from oasis.social_platform.typing import (ActionType, DefaultPlatformType, RecsysType) +from oasis.user_profile_agent.agent import PreferenceAgent # Create log directory if it doesn't exist log_dir = "./log" @@ -53,6 +55,7 @@ def __init__( platform: Union[DefaultPlatformType, Platform], database_path: str = None, semaphore: int = 128, + profile_update_enabled: bool = True, ) -> None: r"""Init the oasis environment. @@ -114,12 +117,245 @@ def __init__( raise ValueError( f"Invalid platform: {platform}. You should pass a " "DefaultPlatformType or a Platform instance.") + + # Initialize user profile management + self.user_profiles: Dict[int, Dict[str, Any]] = {} # Store user profile data + self.user_profile_agent: PreferenceAgent = None # User profile analysis agent + self.profile_update_enabled: bool = profile_update_enabled # Whether to enable dynamic profile updates + self.user_profile_file: str = "user_profile.json" # User profile storage file async def reset(self) -> None: r"""Start the platform and sign up the agents.""" self.platform_task = asyncio.create_task(self.platform.running()) self.agent_graph = await generate_custom_agents( channel=self.channel, agent_graph=self.agent_graph) + + # Initialize user profile agent and load existing profiles + await self._initialize_user_profile_system() + + async def _initialize_user_profile_system(self) -> None: + """Initialize user profile system""" + try: + # Initialize user profile analysis agent + # Need to pass in model, can get model configuration from the first agent + agents_list = self.agent_graph.get_agents() + if agents_list: + first_agent = agents_list[0][1] # get_agents() returns [(agent_id, agent), ...] + self.user_profile_agent = PreferenceAgent(model=first_agent.model_backend) + env_log.info("User profile agent initialized successfully") + + # Load existing user profile data + await self._load_user_profiles() + env_log.info("User profile system initialized") + except Exception as e: + env_log.error(f"Failed to initialize user profile system: {e}") + self.profile_update_enabled = False + + async def _load_user_profiles(self) -> None: + """Load existing user profile data from file""" + try: + if os.path.exists(self.user_profile_file): + with open(self.user_profile_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Compatible with new and old formats + if isinstance(data, dict) and "user_profiles" in data: + # New format: data with timestamps + raw_profiles = data["user_profiles"] + self.user_profiles = {str(k): v for k, v in raw_profiles.items()} + self._profile_update_count = data.get("update_count", 0) + env_log.info(f"Loaded {len(self.user_profiles)} user profiles from {self.user_profile_file} (last updated: {data.get('last_updated', 'unknown')}, update #{self._profile_update_count})") + env_log.info(f"Profile keys loaded: {list(self.user_profiles.keys())}") + else: + # Old format: direct user profile data + self.user_profiles = {str(k): v for k, v in data.items()} + self._profile_update_count = 0 + env_log.info(f"Loaded {len(self.user_profiles)} user profiles from {self.user_profile_file} (legacy format)") + env_log.info(f"Profile keys loaded: {list(self.user_profiles.keys())}") + else: + self.user_profiles = {} + self._profile_update_count = 0 + env_log.info("No existing user profile file found, starting with empty profiles") + except Exception as e: + env_log.error(f"Failed to load user profiles: {e}") + self.user_profiles = {} + self._profile_update_count = 0 + + async def _save_user_profiles(self) -> None: + """Save user profile data to file""" + try: + # Add update time field + from datetime import datetime + + profile_data_with_timestamp = { + "user_profiles": self.user_profiles, + "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "update_count": getattr(self, '_profile_update_count', 0) + 1 + } + + # Record update count + self._profile_update_count = profile_data_with_timestamp["update_count"] + + with open(self.user_profile_file, 'w', encoding='utf-8') as f: + json.dump(profile_data_with_timestamp, f, ensure_ascii=False, indent=2) + env_log.info(f"Saved {len(self.user_profiles)} user profiles to {self.user_profile_file} at {profile_data_with_timestamp['last_updated']} (update #{profile_data_with_timestamp['update_count']})") + except Exception as e: + env_log.error(f"Failed to save user profiles: {e}") + + async def _collect_user_history(self, agent_id: int) -> Dict[str, Any]: + """Collect historical behavior data for specified user""" + try: + # Use database path from environment + import sqlite3 + + db_path = self.platform.db_path + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Collect user's post and comment history + user_history = { + "user_id": agent_id, + "actions": { + "create_post": [], + "create_comment": [], + "like_post": [], + "sign_up": [], + "refresh": [], + "do_nothing": [] + }, + "time_distribution": { + "morning": 0, + "afternoon": 0, + "evening": 0, + "night": 0 + } + } + + # Query user's posts + try: + cursor.execute("SELECT content FROM post WHERE agent_id = ?", (agent_id,)) + posts = cursor.fetchall() + user_history["actions"]["create_post"] = [post[0] for post in posts if post[0]] + env_log.info(f"Found {len(posts)} posts for agent {agent_id}") + except Exception as e: + env_log.warning(f"Failed to query posts for agent {agent_id}: {e}") + + # Query user's comments + try: + cursor.execute("SELECT content FROM comment WHERE agent_id = ?", (agent_id,)) + comments = cursor.fetchall() + user_history["actions"]["create_comment"] = [comment[0] for comment in comments if comment[0]] + env_log.info(f"Found {len(comments)} comments for agent {agent_id}") + except Exception as e: + env_log.warning(f"Failed to query comments for agent {agent_id}: {e}") + + # Query user's operation history (from trace table) + try: + cursor.execute(""" + SELECT action_type, COUNT(*) as count + FROM trace + WHERE agent_id = ? + GROUP BY action_type + """, (agent_id,)) + action_counts = cursor.fetchall() + for action_type, count in action_counts: + if action_type in user_history["actions"]: + # Only record count here, not specific content + pass + env_log.info(f"Found {len(action_counts)} action types for agent {agent_id}") + except Exception as e: + env_log.warning(f"Failed to query action history for agent {agent_id}: {e}") + + conn.close() + env_log.info(f"Successfully collected history for agent {agent_id}") + return user_history + + except Exception as e: + env_log.error(f"Failed to collect user history for agent {agent_id}: {e}") + return { + "user_id": agent_id, + "actions": {"create_post": [], "create_comment": []}, + "time_distribution": {"morning": 0, "afternoon": 0, "evening": 0, "night": 0} + } + + async def _update_user_profiles(self) -> None: + """Dynamically update all users' profile data""" + if not self.profile_update_enabled: + env_log.info("User profile update is disabled") + return + + if not self.user_profile_agent: + env_log.error("User profile agent is not initialized") + return + + try: + env_log.info("Starting dynamic user profile update...") + + # Get information for all agents + agents_list = self.agent_graph.get_agents() + env_log.info(f"Found {len(agents_list)} agents to update profiles for") + + agent_profile_dic = { + agent_id: agent.user_info.profile + for agent_id, agent in agents_list + } + + # Update user profile for each agent + updated_count = 0 + for agent_id, agent in agents_list: + try: + # Ensure agent_id is string type for consistency + agent_id_str = str(agent_id) + env_log.info(f"Processing profile update for agent {agent_id_str} (original: {agent_id}, type: {type(agent_id)})") + + # Collect user history data + user_history = await self._collect_user_history(agent_id) + env_log.info(f"Collected history for agent {agent_id_str}: {len(user_history.get('actions', {}).get('create_post', []))} posts, {len(user_history.get('actions', {}).get('create_comment', []))} comments") + + # Get previous user profile as previous_profile + previous_profile = self.user_profiles.get(agent_id_str, None) + env_log.info(f"Previous profile exists for agent {agent_id_str}: {previous_profile is not None}") + + # Call PreferenceAgent for profile analysis + env_log.info(f"Calling PreferenceAgent.analyse for agent {agent_id_str}") + new_user_profile = await self.user_profile_agent.analyse( + (user_history, self.user_profiles.get(agent_id_str, {}), agent_profile_dic.get(agent_id)), + previous_profile=previous_profile + ) + + # Check the returned result + if new_user_profile is None: + env_log.warning(f"PreferenceAgent returned None for agent {agent_id_str}") + # Keep original profile data, don't overwrite with None + continue + + # Update user profile data (using string type agent_id) + env_log.info(f"Updating profile for agent {agent_id_str}. Current profiles: {list(self.user_profiles.keys())}") + self.user_profiles[agent_id_str] = new_user_profile + updated_count += 1 + + env_log.info(f"Successfully updated user profile for agent {agent_id_str}. Total profiles now: {list(self.user_profiles.keys())}") + + except Exception as e: + env_log.error(f"Failed to update profile for agent {agent_id}: {e}") + import traceback + env_log.error(f"Traceback: {traceback.format_exc()}") + # On failure, keep original profile data unchanged + continue + + # Save updated user profile data + await self._save_user_profiles() + env_log.info(f"Saved {len(self.user_profiles)} user profiles to {self.user_profile_file}") + + if updated_count > 0: + env_log.info(f"Successfully updated {updated_count} user profiles") + else: + env_log.warning("No user profiles were successfully updated") + + except Exception as e: + env_log.error(f"Failed to update user profiles: {e}") + import traceback + env_log.error(f"Traceback: {traceback.format_exc()}") async def _perform_llm_action(self, agent): r"""Send the request to the llm model and execute the action. @@ -192,10 +428,32 @@ async def step( # Execute all tasks concurrently await asyncio.gather(*tasks) env_log.info("performed all actions.") + + # After all operations are completed, dynamically update user profiles + await self._update_user_profiles() + # # Control some agents to perform actions # Update the clock if self.platform_type == DefaultPlatformType.TWITTER: self.platform.sandbox_clock.time_step += 1 + + def enable_user_profile_update(self, enabled: bool = True) -> None: + """Enable or disable dynamic user profile updates""" + self.profile_update_enabled = enabled + env_log.info(f"User profile update {'enabled' if enabled else 'disabled'}") + + def set_user_profile_file(self, file_path: str) -> None: + """Set user profile storage file path""" + self.user_profile_file = file_path + env_log.info(f"User profile file set to: {file_path}") + + def get_user_profile(self, agent_id: int) -> Dict[str, Any]: + """Get user profile data for specified agent""" + return self.user_profiles.get(agent_id, {}) + + def get_all_user_profiles(self) -> Dict[int, Dict[str, Any]]: + """Get profile data for all users""" + return self.user_profiles.copy() async def close(self) -> None: r"""Stop the platform and close the environment. diff --git a/oasis/social_agent/agents_generator.py b/oasis/social_agent/agents_generator.py index 65435906..4a8cc808 100644 --- a/oasis/social_agent/agents_generator.py +++ b/oasis/social_agent/agents_generator.py @@ -359,7 +359,7 @@ async def generate_controllable_agents( }}, recsys_type="reddit", ) - # controllable的agent_id全都在llm agent的agent_id的前面 + # All controllable agent_ids come before LLM agent_ids agent = SocialAgent(agent_id=i, user_info=user_info, channel=channel, @@ -378,7 +378,7 @@ async def generate_controllable_agents( for i in range(control_user_num): for j in range(control_user_num): agent = agent_graph.get_agent(i) - # controllable agent互相也全部关注 + # All controllable agents follow each other if i != j: user_id = agent_user_id_mapping[j] await agent.env.action.follow(user_id) @@ -407,7 +407,7 @@ async def gen_control_agents_with_data( }, recsys_type="reddit", ) - # controllable的agent_id全都在llm agent的agent_id的前面 + # All controllable agent_ids come before LLM agent_ids agent = SocialAgent( agent_id=i, user_info=user_info, diff --git a/oasis/user_profile_agent/agent.py b/oasis/user_profile_agent/agent.py new file mode 100644 index 00000000..ee0de2f1 --- /dev/null +++ b/oasis/user_profile_agent/agent.py @@ -0,0 +1,291 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import json +from typing import List, Tuple +from camel.agents import ChatAgent +from camel.messages import BaseMessage +from .types import * + + +class PreferenceAgent(ChatAgent): + + def _format_system_message(self) -> BaseMessage: + return BaseMessage.make_user_message( + role_name="User Behavior Analyst", + content= + "You are an expert social media behavior analyst. Your task is to analyze user behavior patterns and generate structured profiles." + ) + + def _format_user_prompt(self, history: dict, profile: dict, + agent_profiles: dict) -> str: + user_history = json.dumps(history, indent=2) if history else "" + user_profile = json.dumps(profile, indent=2) if profile else "" + agent_profile = json.dumps(agent_profiles, + indent=2) if agent_profiles else "" + + # Define the prompt as a regular string to avoid f-string parsing issues + # with the JSON block. + static_prompt = static_prompt = """You are a highly precise data processing agent. Your ONLY function is to analyze user data and map it to a rigid JSON structure using ONLY the predefined tags provided. Any deviation from the exact tag values will cause a system failure. + +**CRITICAL DIRECTIVE**: For every field that requires tags, you MUST use the exact, case-sensitive, character-for-character string values from the "Valid Tags" lists provided in each section. +- DO NOT invent, create, or add any new tags. +- DO NOT paraphrase, simplify, or shorten the tags. For example, if a valid tag is "Economics & Markets", you MUST use "Economics & Markets", NOT "Economics". +- DO NOT extract partial keywords from the valid tags. +- IMPORTANT: Your output MUST be a single, valid JSON object and nothing else. Do not include any text before or after the JSON. + +--- + +### Analysis Workflow +Your analysis process depends on the input data provided below. READ THIS CAREFULLY. + +* **Case A: If you see a section titled "The following is the historical data of the user":** + You MUST base your entire analysis and all generated fields directly on the JSON data provided in that section. + +* **Case B: If the "historical data of the user" section is ABSENT or `null`:** + This means you must generate a profile based on imagination. Use the "profile data of the user" (e.g., their bio and name) to first mentally create a short, plausible history of actions (e.g., 1-2 posts or comments) that are consistent with that profile. Then, base your entire final JSON output on this **imagined history**. Do NOT output the imagined history itself; it is only an internal tool for you to generate the final, structured JSON profile. + +--- + +Your Instructions: +For each section below, you will provide a descriptive text summary AND select the most fitting tags from the predefined lists where applicable. + +1. **Identify User ID**: Extract the user identifier. + +2. **Analyze User Interests**: + * Analyze the user's content and map their topics to the EXACT string values from the `Valid InterestEnum Tags` list below. No other values are permitted. + * You MUST select 3 to 5 tags from this list. + * **Valid `InterestEnum` Tags**: "Artificial Intelligence", "Software Development", "Cryptocurrency & Blockchain", "Economics & Markets", "Investing & Personal Finance", "Travel & Adventure", "Health & Wellness", "Gaming", "Movies & TV Shows", "Music", "Books & Literature", "Politics & Governance", "Social Issues & Advocacy", "Education", "Philosophy", "Hospitality Industry", "Sustainability", "Eco-tourism", "Business Strategy", "Technology in Hospitality", "Logistics and Distribution Networks", "Cultural Intersections" + +3. **Analyze Behavioral Preferences**: + * **Content Style**: Provide a descriptive text. Then, map the findings to 1-2 tags chosen from the EXACT `Valid ContentStyleEnum Tags` list below. + - **Valid `ContentStyleEnum` Tags**: "Long-form Posts", "Short & Quick Updates", "Visual Content", "Opinion & Editorials", "Educational & Tutorials", "News & Updates", "Personal Stories", "Professional Insights", "Questions & Inquiries" + * **Interaction Style**: Provide a descriptive text. Then, map the findings to 1-2 tags chosen from the EXACT `Valid InteractionStyleEnum Tags` list below. + - **Valid `InteractionStyleEnum` Tags**: "Friendly & Supportive", "Formal & Professional", "Casual & Friendly", "Analytical & Inquisitive", "Supportive & Encouraging", "Humorous & Witty", "Direct & Straightforward", "Thoughtful & Reflective" + * **Active Periods**: Select one or more tags from the EXACT `Valid ActivePeriodEnum Tags` list below. + - **Valid `ActivePeriodEnum` Tags**: "Morning", "Afternoon", "Evening", "Night" + +4. **Summarize Behavioral Patterns**: + * Provide a qualitative summary text for `behavioral_summary`. + * Then, map the user's archetype to 1-2 tags chosen from the EXACT `Valid BehavioralArchetypeEnum Tags` list below. + - **Valid `BehavioralArchetypeEnum` Tags**: "Content Creator", "Active Participant", "Observer & Lurker", "Thought Leader", "Community Builder", "Knowledge Seeker", "Casual User" + +5. **Analyze Community & Network Profile**: + * **Affinity**: Provide a descriptive text for the `affinity` field. + * **Potential Role**: Provide a descriptive text for `potential_role`. Then, map the role to 1-2 tags chosen from the EXACT `Valid CommunityRoleEnum Tags` list below. + - **Valid `CommunityRoleEnum` Tags**: "Leader & Organizer", "Mentor & Guide", "Expert Contributor", "Active Participant", "Newcomer & Learner" + +**CRITICAL REQUIREMENTS - MANDATORY COMPLIANCE:** + +🚨 **ARRAY FORMAT REQUIREMENT**: +- `interests` → MUST BE ARRAY: ["value1", "value2"] +- `content_style_tags` → MUST BE ARRAY: ["value1", "value2"] +- `interaction_style_tags` → MUST BE ARRAY: ["value1", "value2"] +- `active_periods` → MUST BE ARRAY: ["value1", "value2"] +- `behavioral_archetype_tags` → MUST BE ARRAY: ["value1", "value2"] +- `potential_role_tags` → MUST BE ARRAY: ["value1", "value2"] + +🚨 **NEVER USE SINGLE STRINGS FOR THESE FIELDS** +❌ WRONG: "content_style_tags": "Long-form Posts" +✅ CORRECT: "content_style_tags": ["Long-form Posts"] + +🚨 **ALL FIELDS MUST BE PRESENT** +- interests, preferences, behavioral_summary, behavioral_archetype_tags, community_profile +- content_style, content_style_tags, interaction_style, interaction_style_tags, active_periods +- affinity, potential_role, potential_role_tags + +**FINAL CHECK**: Before outputting JSON, verify EVERY field ending with "_tags", "interests", or "active_periods" is an ARRAY with square brackets [] + +**Must strictly follow the following JSON structure format (example):** + +```json +{ + "profile_summary": { + "interests": ["Business Strategy", "Economics & Markets", "Hospitality Industry"], + "preferences": { + "content_style": "Professional in-depth analysis, sharing practical insights combined with industry experience", + "content_style_tags": ["Professional Insights", "Educational & Tutorials"], + "interaction_style": "Formal and professional yet friendly and supportive, willing to share experience and advice", + "interaction_style_tags": ["Formal & Professional", "Supportive & Encouraging"], + "active_periods": ["Afternoon", "Evening"] + }, + "behavioral_summary": "User demonstrates expertise as an experienced industry professional, actively participating in professional discussions, eager to share insights and advice, with clear thought leadership qualities", + "behavioral_archetype_tags": ["Thought Leader", "Knowledge Seeker"], + "community_profile": { + "affinity": "Tends to join professional communities in business, economics, and hospitality industries, values knowledge sharing and industry exchange", + "potential_role": "Acts as an experience sharer and mentor in professional communities, providing industry insights for newcomers", + "potential_role_tags": ["Expert Contributor", "Mentor & Guide"] + } + } +} +``` + +**Note: The above is a complete example, you must generate a complete JSON structure containing all fields.** + +**Important Notes:** +- All fields marked as "string arrays" must be in array format, not single strings +- All enum values must be strictly selected from the above lists, cannot be created arbitrarily +- JSON structure must be complete, cannot miss any required fields + +**Example Output:** +```json +{ + "profile_summary": { + "interests": ["Economics & Markets", "Investing & Personal Finance", "Business Strategy"], + "preferences": { + "content_style": "The user primarily writes long, detailed posts to share professional opinions.", + "content_style_tags": ["Long-form Posts", "Professional Insights"], + "interaction_style": "Their tone is formal and professional, often engaging in inquisitive discussions.", + "interaction_style_tags": ["Formal & Professional", "Analytical & Inquisitive"], + "active_periods": ["Evening", "Afternoon"] + }, + "behavioral_summary": "The user follows a pattern of observing first, then engaging deeply as a content creator.", + "behavioral_archetype_tags": ["Content Creator", "Thought Leader"], + "community_profile": { + "affinity": "The user would likely join academic circles focused on technology policy and ethics.", + "potential_role": "They would act as an expert contributor, sharing deep insights.", + "potential_role_tags": ["Expert Contributor"] + } + } +} +``` + +The following is the historical data of the user: +{user_history} + +The following is the profile data of the user: +{user_profile} + +The following is the agent profile of the user: +{agent_profile} +""" + + prompt = f""" +The following is the historical data of the user: +{user_history} + +The following is the profile data of the user: +{user_profile} + +The following is the agent profile of the user: +{agent_profile} +""" + + return static_prompt + prompt + + def _postprocess_response(self, response_content: dict) -> dict: + """ + Post-process model response, converting string tag fields to list format. + This handles cases where the model returns single strings instead of arrays. + """ + if isinstance(response_content, + dict) and 'profile_summary' in response_content: + profile = response_content['profile_summary'] + + # Handle tag fields in preferences + if 'preferences' in profile: + prefs = profile['preferences'] + # Convert strings to lists + for field in [ + 'content_style_tags', 'interaction_style_tags', + 'active_periods' + ]: + if field in prefs and isinstance(prefs[field], str): + prefs[field] = [prefs[field]] + + # Handle possible incorrect field names + if 'active_periods_tags' in prefs: + if 'active_periods' not in prefs: + prefs['active_periods'] = prefs[ + 'active_periods_tags'] if isinstance( + prefs['active_periods_tags'], + list) else [prefs['active_periods_tags']] + del prefs['active_periods_tags'] + + # Handle behavioral_archetype_tags + if 'behavioral_archetype_tags' in profile and isinstance( + profile['behavioral_archetype_tags'], str): + profile['behavioral_archetype_tags'] = [ + profile['behavioral_archetype_tags'] + ] + + # Handle tag fields in community_profile + if 'community_profile' in profile: + cp = profile['community_profile'] + if 'potential_role_tags' in cp and isinstance( + cp['potential_role_tags'], str): + cp['potential_role_tags'] = [cp['potential_role_tags']] + + # Handle interests field, split if it's a comma-separated string + if 'interests' in profile and isinstance(profile['interests'], + str): + profile['interests'] = [ + i.strip() for i in profile['interests'].split(',') + ] + + return response_content + + async def analyse(self, user_profiles: Tuple[dict, dict, dict]) -> dict: + r""" + Analyze user profile. + + Args: + user_profiles: Tuple containing user history, personal data, and agent information + + Returns: + Updated user profile data + """ + input_content = self._format_user_prompt(user_profiles[0], + user_profiles[1], + user_profiles[2]) + user_msg = BaseMessage.make_user_message(role_name="User", + content=input_content) + + try: + # First try using strict response_format + response = await self.astep(user_msg, + response_format=UserPreferenceProfile) + print(f'Strict response successful: {response}') + + # Ensure the return is in dictionary format, not string + content = response.msg.content + if isinstance(content, str): + # If it's a string, try to parse as JSON + try: + import json + content = json.loads(content) + except json.JSONDecodeError: + print( + f"Warning: Failed to parse response as JSON: {content}" + ) + elif hasattr(content, 'model_dump'): + # If it's a Pydantic model, convert to dictionary + content = content.model_dump() + elif hasattr(content, 'dict'): + # Compatible with older Pydantic versions + content = content.dict() + + return content + except Exception as e: + print(f"Strict response failed: {e}") + print("Falling back to flexible parsing...") + + def __init__(self, model): + system_message = self._format_system_message() + super().__init__(system_message=system_message, + model=model, + tools=[], + single_iteration=True, + scheduling_strategy='random_model') diff --git a/oasis/user_profile_agent/enums.py b/oasis/user_profile_agent/enums.py new file mode 100644 index 00000000..071b1ccf --- /dev/null +++ b/oasis/user_profile_agent/enums.py @@ -0,0 +1,100 @@ +from enum import Enum +from typing import List, Optional +from pydantic import BaseModel, Field + + +class InterestEnum(str, Enum): + r""" + Predefined interest tag enumeration. + Used to identify users' main areas of interest, helping to categorize and understand user focus points. + """ + ARTIFICIAL_INTELLIGENCE = "Artificial Intelligence" # AI technology, machine learning, deep learning, etc. + SOFTWARE_DEVELOPMENT = "Software Development" # Software development, programming languages, etc. + CRYPTO_BLOCKCHAIN = "Cryptocurrency & Blockchain" # Cryptocurrency, blockchain, etc. + ECONOMICS_MARKETS = "Economics & Markets" # Economics, market analysis, macroeconomics, etc. + INVESTING_FINANCE = "Investing & Personal Finance" # Investment and financial management, stocks, funds, etc. + TRAVEL_ADVENTURE = "Travel & Adventure" # Travel, adventure, etc. + HEALTH_WELLNESS = "Health & Wellness" # Health and wellness, medical care, etc. + GAMING = "Gaming" # Gaming, etc. + MOVIES_TV_SHOWS = "Movies & TV Shows" # Movies, TV series, etc. + MUSIC = "Music" # Music, etc. + BOOKS_LITERATURE = "Books & Literature" # Books, literature, etc. + POLITICS_GOVERNANCE = "Politics & Governance" # Politics, public policy, government administration, etc. + SOCIAL_ISSUES_ADVOCACY = "Social Issues & Advocacy" # Social issues, public welfare, etc. + EDUCATION = "Education" # Education, learning methods, knowledge sharing, etc. + PHILOSOPHY = "Philosophy" # Philosophy, ethics, logic, etc. + HOSPITALITY_INDUSTRY = "Hospitality Industry" # Hotels, tourism, etc. + SUSTAINABILITY = "Sustainability" # Environmental protection, sustainable development, etc. + ECO_TOURISM = "Eco-tourism" # Ecological tourism, etc. + BUSINESS_STRATEGY = "Business Strategy" # Business strategy, enterprise management, entrepreneurship, etc. + TECH_IN_HOSPITALITY = "Technology in Hospitality" # Hotel technology, tourism technology, etc. + LOGISTICS = "Logistics and Distribution Networks" # Logistics, distribution networks, etc. + CULTURAL_INTERSECTIONS = "Cultural Intersections" # Cultural intersections, etc. + + +class InteractionStyleEnum(str, Enum): + r""" + Interaction style enumeration. + Used to describe users' communication style and tone in interactions. + """ + FRIENDLY_SUPPORTIVE = "Friendly & Supportive" # Friendly and supportive, providing help and support to others + FORMAL_PROFESSIONAL = "Formal & Professional" # Formal and professional, using professional terminology and formal tone + CASUAL_FRIENDLY = "Casual & Friendly" # Casual and friendly, relaxed and warm communication style + ANALYTICAL_INQUISITIVE = "Analytical & Inquisitive" # Analytical and inquisitive, enjoys deep analysis and asking questions + SUPPORTIVE_ENCOURAGING = "Supportive & Encouraging" # Supportive and encouraging, providing positive feedback to others + HUMOROUS_WITTY = "Humorous & Witty" # Humorous and witty, good at using humor and wit + DIRECT_STRAIGHTFORWARD = "Direct & Straightforward" # Direct and straightforward, speaks frankly + THOUGHTFUL_REFLECTIVE = "Thoughtful & Reflective" # Thoughtful and reflective, enjoys reflection and thinking + + +class ContentStyleEnum(str, Enum): + r""" + Content style enumeration. + Used to describe the style and type of content created by users. + """ + LONG_FORM_POSTS = "Long-form Posts" # Long articles, detailed and in-depth content + SHORT_QUICK_UPDATES = "Short & Quick Updates" # Brief and quick updates, fragmented information + VISUAL_CONTENT = "Visual Content" # Visual content such as images, videos, etc. + OPINION_EDITORIALS = "Opinion & Editorials" # Opinion commentary, personal views and positions + EDUCATIONAL_TUTORIALS = "Educational & Tutorials" # Educational tutorials, knowledge sharing + NEWS_UPDATES = "News & Updates" # News updates, current affairs information + PERSONAL_STORIES = "Personal Stories" # Personal stories, life experience sharing + PROFESSIONAL_INSIGHTS = "Professional Insights" # Professional insights, industry experience sharing + QUESTIONS_INQUIRIES = "Questions & Inquiries" # Questions and inquiries, exploratory content + + +class ActivePeriodEnum(str, Enum): + """ + Active period enumeration. + Used to describe users' main active time periods during the day. + """ + MORNING = "Morning" # Morning period, usually 6:00-12:00 + AFTERNOON = "Afternoon" # Afternoon period, usually 12:00-18:00 + EVENING = "Evening" # Evening period, usually 18:00-22:00 + NIGHT = "Night" # Night period, usually 22:00-6:00 + + +class BehavioralArchetypeEnum(str, Enum): + """ + Behavioral archetype enumeration. + Used to describe users' main behavioral patterns and role positioning on social platforms. + """ + CONTENT_CREATOR = "Content Creator" # Content creator, actively creates and publishes content + ACTIVE_PARTICIPANT = "Active Participant" # Active participant, frequently engages in discussions and interactions + OBSERVER_LURKER = "Observer & Lurker" # Observer, mainly browses and observes, rarely speaks + THOUGHT_LEADER = "Thought Leader" # Thought leader, leads discussions and shares insights + COMMUNITY_BUILDER = "Community Builder" # Community builder, dedicated to building and maintaining communities + KNOWLEDGE_SEEKER = "Knowledge Seeker" # Knowledge seeker, actively learns and acquires knowledge + CASUAL_USER = "Casual User" # Casual user, occasional use, low engagement + + +class CommunityRoleEnum(str, Enum): + """ + Community role enumeration. + Used to describe the roles and status that users may play in a specific community. + """ + LEADER_ORGANIZER = "Leader & Organizer" # Leader and organizer, responsible for community management and event organization + MENTOR_GUIDE = "Mentor & Guide" # Mentor and guide, provides help and guidance to new members + EXPERT_CONTRIBUTOR = "Expert Contributor" # Expert contributor, has professional knowledge and experience in a specific field + ACTIVE_PARTICIPANT = "Active Participant" # Active participant, frequently participates in community activities and discussions + NEWCOMER_LEARNER = "Newcomer & Learner" # Newcomer and learner, just joined the community or is in the learning stage diff --git a/oasis/user_profile_agent/types.py b/oasis/user_profile_agent/types.py new file mode 100644 index 00000000..c173455a --- /dev/null +++ b/oasis/user_profile_agent/types.py @@ -0,0 +1,138 @@ +from typing import List, Optional, Union +from pydantic import BaseModel, Field, field_validator +from oasis.user_profile_agent.enums import ContentStyleEnum, InteractionStyleEnum, ActivePeriodEnum, BehavioralArchetypeEnum, CommunityRoleEnum, InterestEnum +import json + + +class Preferences(BaseModel): + """ + Defines the user's behavioral preferences. + Descriptive text is retained, and parallel enumeration tag lists are added for normalization. + """ + content_style: str = Field( + default=None, + description= + "String. Detailed descriptive text about the user's content creation style, e.g., 'The user mainly publishes long, detailed posts to share professional insights.'" + ) + content_style_tags: List[ContentStyleEnum] = Field( + default=[], + description= + "Array of strings. Standardized list of content style tags. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in ContentStyleEnum]) + + ". Example: ['Long-form Posts', 'Opinion & Editorials']") + interaction_style: str = Field( + default=None, + description= + "String. Detailed descriptive text about the user's interaction style, e.g., 'The user communicates in a formal and professional tone, often engaging in inquisitive discussions.'" + ) + interaction_style_tags: List[InteractionStyleEnum] = Field( + default=[], + description= + "Array of strings. Standardized list of interaction style tags. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in InteractionStyleEnum]) + + ". Example: ['Formal & Professional', 'Analytical & Inquisitive']") + active_periods: List[ActivePeriodEnum] = Field( + default=[], + description= + "Array of strings. List of user's core active period enums. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in ActivePeriodEnum]) + + ". Example: ['Evening', 'Late Night']") + + @field_validator('content_style_tags', + 'interaction_style_tags', + 'active_periods', + mode='before') + @classmethod + def convert_string_to_list(cls, v): + """Automatically convert a string to list format""" + if isinstance(v, str): + return [v] + return v + + +class CommunityProfile(BaseModel): + """Describes the user's potential community affiliations and roles.""" + affinity: str = Field( + default=None, + description= + "String. Detailed description of the specific types of communities the user might join, e.g., 'The user may join academic circles focused on technology policy and ethics.'" + ) + potential_role: str = Field( + default=None, + description= + "String. Detailed description of the social roles the user might play in these communities, e.g., 'They may act as an expert contributor, sharing in-depth insights.'" + ) + potential_role_tags: List[CommunityRoleEnum] = Field( + default=[], + description= + "Array of strings. Standardized list of potential community role tags. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in CommunityRoleEnum]) + + ". Example: ['Expert Contributor', 'Active Participant']") + + @field_validator('potential_role_tags', mode='before') + @classmethod + def convert_string_to_list(cls, v): + """Automatically convert a string to list format""" + if isinstance(v, str): + return [v] + return v + + +class ProfileSummary(BaseModel): + """ + Summary of the user's profile. + """ + interests: List[InterestEnum] = Field( + ..., + description= + "Array of strings. List of user's core interest tags. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in InterestEnum]) + + ". Example: ['Economics & Markets', 'Investing & Personal Finance', 'Business Strategy']" + ) + preferences: Preferences = Field( + default=None, + description= + "Object. Detailed information about the user's behavioral preferences, including content style, interaction style, and active periods." + ) + behavioral_summary: str = Field( + default=None, + description= + "String. Qualitative description of the user's participation patterns and behavioral archetypes, e.g., 'The user follows a pattern of observing first and then deeply engaging as a content creator.'" + ) + behavioral_archetype_tags: List[BehavioralArchetypeEnum] = Field( + default=[], + description= + "Array of strings. Standardized list of user's behavioral archetype tags. Must be an array of strings, each chosen from the following enum values: " + + ", ".join([e.value for e in BehavioralArchetypeEnum]) + + ". Example: ['Content Creator', 'Thought Leader']") + community_profile: CommunityProfile = Field( + default=None, + description= + "Object. Detailed information about the user's community affiliation and roles, including community affinity and potential roles." + ) + + @field_validator('interests', 'behavioral_archetype_tags', mode='before') + @classmethod + def convert_string_to_list(cls, v): + """Automatically convert a string to list format""" + if isinstance(v, str): + return [v] + return v + + +class UserPreferenceProfile(BaseModel): + """ + A structured user profile generated from behavioral analysis. + This model strictly follows the required JSON output format. + """ + profile_summary: ProfileSummary = Field( + ..., + description= + "Object. Detailed summary of the user's profile, including interests, preferences, behavioral summary, and community profile." + ) + + @classmethod + def description(cls) -> str: + """Generate a JSON Schema description for use as the LLM response format.""" + schema = cls.model_json_schema() + return json.dumps(schema, indent=2) diff --git a/test/agent/test_user_profile_agent.py b/test/agent/test_user_profile_agent.py new file mode 100644 index 00000000..99e97f71 --- /dev/null +++ b/test/agent/test_user_profile_agent.py @@ -0,0 +1,379 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== + +import pytest +import json +import asyncio +from unittest.mock import Mock, AsyncMock, patch, MagicMock +from datetime import datetime +from typing import Dict, Any +from camel.models import ModelFactory +from camel.types import ModelPlatformType + +# Import the modules to test +from oasis.user_profile_agent.agent import PreferenceAgent +from oasis.user_profile_agent.types import (UserPreferenceProfile, + ProfileSummary, Preferences, + CommunityProfile) +from oasis.user_profile_agent.enums import (InterestEnum, ContentStyleEnum, + InteractionStyleEnum, + ActivePeriodEnum, + BehavioralArchetypeEnum, + CommunityRoleEnum) + + +class TestPreferenceAgent: + """Test cases for PreferenceAgent class""" + + @pytest.fixture + def preference_agent(self): + """Create a mock PreferenceAgent instance for testing""" + # Create a mock agent to avoid model initialization issues + agent = Mock(spec=PreferenceAgent) + agent._postprocess_response = PreferenceAgent._postprocess_response.__get__(agent) + return agent + + @pytest.fixture + def sample_user_history(self): + """Sample user history data for testing""" + return { + "actions": { + "create_post": [{ + "timestamp": "2024-01-01T10:00:00", + "content": + "Discussing the latest trends in AI and machine learning", + "engagement": { + "likes": 15, + "comments": 3 + } + }, { + "timestamp": "2024-01-02T14:30:00", + "content": "Sharing insights on blockchain technology", + "engagement": { + "likes": 8, + "comments": 2 + } + }], + "create_comment": [{ + "timestamp": "2024-01-01T11:00:00", + "content": + "Great analysis! I agree with your points on decentralization.", + "post_id": "123" + }], + "like": [{ + "timestamp": "2024-01-01T09:00:00", + "post_id": "456" + }, { + "timestamp": "2024-01-01T15:00:00", + "post_id": "789" + }] + } + } + + @pytest.fixture + def sample_previous_profile(self): + """Sample previous profile for testing""" + return { + "profile_summary": { + "interests": ["Artificial Intelligence", "Software Development"], + "preferences": { + "content_style": "Technical and analytical", + "content_style_tags": ["Long-form Posts"], + "interaction_style": "Professional and engaging", + "interaction_style_tags": ["Formal & Professional"], + "active_periods": ["Morning"] + }, + "behavioral_summary": + "Active content creator with focus on technology", + "behavioral_archetype_tags": ["Content Creator"], + "community_profile": { + "affinity": "Technology communities", + "potential_role": "Expert contributor", + "potential_role_tags": ["Expert Contributor"] + } + } + } + + def test_postprocess_response_string_to_list_conversion( + self, preference_agent): + """Test _postprocess_response method converts strings to lists""" + response_content = { + "profile_summary": { + "preferences": { + "content_style_tags": "Long-form Posts", + "interaction_style_tags": "Formal & Professional", + "active_periods": "Morning" + }, + "behavioral_archetype_tags": "Content Creator", + "community_profile": { + "potential_role_tags": "Expert Contributor" + }, + "interests": "Artificial Intelligence, Software Development" + } + } + + result = preference_agent._postprocess_response(response_content) + + # Check that strings were converted to lists + prefs = result["profile_summary"]["preferences"] + assert isinstance(prefs["content_style_tags"], list) + assert prefs["content_style_tags"] == ["Long-form Posts"] + assert isinstance(prefs["interaction_style_tags"], list) + assert isinstance(prefs["active_periods"], list) + + assert isinstance( + result["profile_summary"]["behavioral_archetype_tags"], list) + assert isinstance( + result["profile_summary"]["community_profile"] + ["potential_role_tags"], list) + assert isinstance(result["profile_summary"]["interests"], list) + assert result["profile_summary"]["interests"] == [ + "Artificial Intelligence", "Software Development" + ] + + def test_postprocess_response_handles_active_periods_tags( + self, preference_agent): + """Test _postprocess_response handles incorrect field name active_periods_tags""" + response_content = { + "profile_summary": { + "preferences": { + "active_periods_tags": ["Morning", "Evening"] + } + } + } + + result = preference_agent._postprocess_response(response_content) + + prefs = result["profile_summary"]["preferences"] + assert "active_periods" in prefs + assert prefs["active_periods"] == ["Morning", "Evening"] + assert "active_periods_tags" not in prefs + + @pytest.mark.asyncio + async def test_analyse_with_valid_data(self, preference_agent, + sample_user_history, + sample_previous_profile): + """Test analyse method with valid input data""" + # Mock the LLM response + mock_response = { + "profile_summary": { + "interests": + ["Artificial Intelligence", "Software Development", "Cryptocurrency & Blockchain"], + "preferences": { + "content_style": + "Technical and analytical with practical insights", + "content_style_tags": + ["Long-form Posts", "Opinion & Editorials"], + "interaction_style": + "Professional and engaging", + "interaction_style_tags": + ["Formal & Professional", "Analytical & Inquisitive"], + "active_periods": ["Morning", "Afternoon"] + }, + "behavioral_summary": + "Active content creator with expertise in emerging technologies", + "behavioral_archetype_tags": + ["Content Creator", "Thought Leader"], + "community_profile": { + "affinity": "Technology and blockchain communities", + "potential_role": "Expert contributor and thought leader", + "potential_role_tags": + ["Expert Contributor", "Thought Leader"] + } + } + } + + with patch.object(preference_agent, 'astep', + new_callable=AsyncMock) as mock_astep: + mock_astep.return_value = Mock(msg=Mock( + content=json.dumps(mock_response))) + + result = await preference_agent.analyse( + (sample_user_history, sample_previous_profile, {}), + previous_profile=sample_previous_profile) + + assert result is not None + assert "profile_summary" in result + assert result["profile_summary"]["interests"] == [ + "Artificial Intelligence", "Software Development", "Cryptocurrency & Blockchain" + ] + + @pytest.mark.asyncio + async def test_analyse_with_invalid_json_response(self, preference_agent, + sample_user_history): + """Test analyse method handles invalid JSON response""" + with patch.object(preference_agent, 'astep', + new_callable=AsyncMock) as mock_astep: + mock_astep.return_value = Mock(msg=Mock( + content="Invalid JSON response")) + + result = await preference_agent.analyse( + (sample_user_history, {}, {}), previous_profile=None) + + assert result is None + + @pytest.mark.asyncio + async def test_analyse_with_exception(self, preference_agent, + sample_user_history): + """Test analyse method handles exceptions gracefully""" + with patch.object(preference_agent, 'astep', + new_callable=AsyncMock) as mock_astep: + mock_astep.side_effect = Exception("LLM service error") + + result = await preference_agent.analyse( + (sample_user_history, {}, {}), previous_profile=None) + + assert result is None + + +class TestUserProfileTypes: + """Test cases for user profile data types""" + + def test_preferences_model_validation(self): + """Test Preferences model validation""" + preferences_data = { + "content_style": "Technical and detailed", + "content_style_tags": ["Long-form Posts"], + "interaction_style": "Professional", + "interaction_style_tags": ["Formal & Professional"], + "active_periods": ["Morning"] + } + + preferences = Preferences(**preferences_data) + assert preferences.content_style == "Technical and detailed" + assert preferences.content_style_tags == [ + ContentStyleEnum.LONG_FORM_POSTS + ] + + def test_community_profile_model_validation(self): + """Test CommunityProfile model validation""" + community_data = { + "affinity": "Technology communities", + "potential_role": "Expert contributor", + "potential_role_tags": ["Expert Contributor"] + } + + community_profile = CommunityProfile(**community_data) + assert community_profile.affinity == "Technology communities" + assert community_profile.potential_role_tags == [ + CommunityRoleEnum.EXPERT_CONTRIBUTOR + ] + + def test_profile_summary_model_validation(self): + """Test ProfileSummary model validation""" + profile_data = { + "interests": ["Artificial Intelligence", "Software Development"], + "preferences": { + "content_style": "Technical", + "content_style_tags": ["Long-form Posts"], + "interaction_style": "Professional", + "interaction_style_tags": ["Formal & Professional"], + "active_periods": ["Morning"] + }, + "behavioral_summary": "Active technology enthusiast", + "behavioral_archetype_tags": ["Content Creator"], + "community_profile": { + "affinity": "Tech communities", + "potential_role": "Contributor", + "potential_role_tags": ["Expert Contributor"] + } + } + + profile_summary = ProfileSummary(**profile_data) + assert len(profile_summary.interests) == 2 + assert profile_summary.behavioral_archetype_tags == [ + BehavioralArchetypeEnum.CONTENT_CREATOR + ] + + def test_user_preference_profile_model_validation(self): + """Test UserPreferenceProfile model validation""" + profile_data = { + "profile_summary": { + "interests": ["Artificial Intelligence"], + "preferences": { + "content_style": "Technical", + "content_style_tags": ["Long-form Posts"], + "interaction_style": "Professional", + "interaction_style_tags": ["Formal & Professional"], + "active_periods": ["Morning"] + }, + "behavioral_summary": "Tech enthusiast", + "behavioral_archetype_tags": ["Content Creator"], + "community_profile": { + "affinity": "Tech communities", + "potential_role": "Contributor", + "potential_role_tags": ["Expert Contributor"] + } + } + } + + user_profile = UserPreferenceProfile(**profile_data) + assert user_profile.profile_summary.interests == [ + InterestEnum.ARTIFICIAL_INTELLIGENCE + ] + + def test_string_to_list_conversion(self): + """Test automatic string to list conversion in validators""" + # Test with string input that should be converted to list + preferences_data = { + "content_style_tags": "Long-form Posts", # String instead of list + "interaction_style_tags": "Formal & Professional", + "active_periods": "Morning" + } + + preferences = Preferences(**preferences_data) + assert isinstance(preferences.content_style_tags, list) + assert preferences.content_style_tags == [ + ContentStyleEnum.LONG_FORM_POSTS + ] + assert isinstance(preferences.interaction_style_tags, list) + assert isinstance(preferences.active_periods, list) + + +class TestEnums: + """Test cases for enum classes""" + + def test_interest_enum_values(self): + """Test InterestEnum has expected values""" + assert InterestEnum.ARTIFICIAL_INTELLIGENCE.value == "Artificial Intelligence" + assert InterestEnum.SOFTWARE_DEVELOPMENT.value == "Software Development" + assert InterestEnum.ECONOMICS_MARKETS.value == "Economics & Markets" + + def test_content_style_enum_values(self): + """Test ContentStyleEnum has expected values""" + assert ContentStyleEnum.LONG_FORM_POSTS.value == "Long-form Posts" + assert ContentStyleEnum.SHORT_QUICK_UPDATES.value == "Short & Quick Updates" + + def test_interaction_style_enum_values(self): + """Test InteractionStyleEnum has expected values""" + assert InteractionStyleEnum.FORMAL_PROFESSIONAL.value == "Formal & Professional" + assert InteractionStyleEnum.CASUAL_FRIENDLY.value == "Casual & Friendly" + + def test_active_period_enum_values(self): + """Test ActivePeriodEnum has expected English values""" + assert ActivePeriodEnum.MORNING.value == "Morning" + assert ActivePeriodEnum.AFTERNOON.value == "Afternoon" + assert ActivePeriodEnum.EVENING.value == "Evening" + assert ActivePeriodEnum.NIGHT.value == "Night" + + def test_behavioral_archetype_enum_values(self): + """Test BehavioralArchetypeEnum has expected values""" + assert BehavioralArchetypeEnum.CONTENT_CREATOR.value == "Content Creator" + assert BehavioralArchetypeEnum.THOUGHT_LEADER.value == "Thought Leader" + assert BehavioralArchetypeEnum.ACTIVE_PARTICIPANT.value == "Active Participant" + + def test_community_role_enum_values(self): + """Test CommunityRoleEnum has expected values""" + assert CommunityRoleEnum.EXPERT_CONTRIBUTOR.value == "Expert Contributor" + assert CommunityRoleEnum.ACTIVE_PARTICIPANT.value == "Active Participant"