diff --git a/src/python/role_play/chat/handler.py b/src/python/role_play/chat/handler.py index 668931e..76e3dc1 100644 --- a/src/python/role_play/chat/handler.py +++ b/src/python/role_play/chat/handler.py @@ -139,9 +139,9 @@ async def _load_session_content(self, adk_session: Any, resource_loader: Resourc raise HTTPException(status_code=500, detail="Failed to load session character/scenario configuration.") return character_dict, scenario_dict - async def _generate_character_response(self, adk_session: Any, message: str, user_id: str, - session_id: str, character_dict: Dict, scenario_dict: Dict, - adk_session_service: InMemorySessionService) -> str: + async def _generate_character_response(self, adk_session: Any, message: str, user_id: str, session_id: str, + character_dict: Dict, scenario_dict: Dict, + adk_session_service: InMemorySessionService, resource_loader) -> str: """Generate character response using ADK Runner.""" # Get character, scenario IDs and language from session state character_id = adk_session.state.get("character_id") @@ -149,8 +149,8 @@ async def _generate_character_response(self, adk_session: Any, message: str, use script_id = adk_session.state.get("script_id") language = adk_session.state.get("language", "en") - # Use get_production_agent from roleplay_agent module - agent = await get_production_agent(character_id, scenario_id, language, scripted=(bool(script_id) or (script_id is not None))) + # Use get_production_agent from roleplay_agent module with injected resource_loader + agent = await get_production_agent(character_id, scenario_id, language, scripted=(bool(script_id) or (script_id is not None)), resource_loader=resource_loader) if not agent: raise HTTPException(status_code=500, detail="Failed to create roleplay agent") @@ -487,10 +487,9 @@ async def send_message( character_dict, scenario_dict = await self._load_session_content(adk_session, resource_loader) # Generate character response - response_text = await self._generate_character_response( - adk_session, request.message, current_user.id, session_id, - character_dict, scenario_dict, adk_session_service - ) + response_text = await self._generate_character_response(adk_session, request.message, current_user.id, + session_id, character_dict, scenario_dict, + adk_session_service, resource_loader) # Log character response await self._log_character_message( diff --git a/src/python/role_play/dev_agents/roleplay_agent/agent.py b/src/python/role_play/dev_agents/roleplay_agent/agent.py index f0e3d17..0fff2f9 100644 --- a/src/python/role_play/dev_agents/roleplay_agent/agent.py +++ b/src/python/role_play/dev_agents/roleplay_agent/agent.py @@ -3,6 +3,7 @@ """ import os import sys +import logging from pathlib import Path from typing import Dict, Optional from google.adk.agents import Agent @@ -13,7 +14,9 @@ DEFAULT_MODEL = "gemini-2.5-flash" # Set a reasonable default AGENT_MODEL = os.getenv("ADK_MODEL", DEFAULT_MODEL) # <-- Read from env -from .tools import dev_tools, resource_loader +logger = logging.getLogger(__name__) + +from .tools import dev_tools # --- Development Agent for adk web --- @@ -46,7 +49,7 @@ def __init__(self, **kwargs): # --- Configuration Export for Production --- -async def get_production_agent(character_id: str, scenario_id: str, language: str = "en", scripted: bool = False, agent_model: str = AGENT_MODEL) -> Optional[Agent]: +async def get_production_agent(character_id: str, scenario_id: str, language: str = "en", scripted: bool = False, agent_model: str = AGENT_MODEL, resource_loader=None) -> Optional[Agent]: """ Creates a production-ready RolePlayAgent for a specific character, scenario, and language. @@ -57,10 +60,26 @@ async def get_production_agent(character_id: str, scenario_id: str, language: st language: The language code (e.g., "en", "zh-TW", "ja") scripted: whether the session is scripted or not agent_model: id of the llm model to use + resource_loader: ResourceLoader instance (injected from handler) Returns: A configured RolePlayAgent instance or None if character/scenario not found """ + # Use the injected resource_loader or create a fallback one + if resource_loader is None: + # Try to get from dependency injection system first + try: + from ...server.dependencies import get_resource_loader + resource_loader = get_resource_loader() + logger.info(f"Using dependency-injected ResourceLoader from server") + except ImportError: + # Last resort: Use the fallback FileStorage ResourceLoader + from .tools import resource_loader as fallback_loader + resource_loader = fallback_loader + logger.warning(f"Using fallback FileStorage ResourceLoader (dev only)") + else: + logger.info(f"Creating agent with {type(resource_loader).__name__}") + # Use await since resource_loader methods are async character = await resource_loader.get_character_by_id(character_id, language) scenario = await resource_loader.get_scenario_by_id(scenario_id, language) diff --git a/src/python/role_play/server/dependencies.py b/src/python/role_play/server/dependencies.py index 25967f6..1bedb1b 100644 --- a/src/python/role_play/server/dependencies.py +++ b/src/python/role_play/server/dependencies.py @@ -66,7 +66,9 @@ def get_storage_backend() -> StorageBackend: # Use storage configuration if config.storage: - return create_storage_backend(config.storage, env_enum) + backend = create_storage_backend(config.storage, env_enum) + logger.info(f"Storage backend: {type(backend).__name__} for {environment}") + return backend else: raise ValueError("Storage configuration is required") diff --git a/test/python/unit/chat/test_chat_handler.py b/test/python/unit/chat/test_chat_handler.py index e3fc09a..9900490 100644 --- a/test/python/unit/chat/test_chat_handler.py +++ b/test/python/unit/chat/test_chat_handler.py @@ -69,21 +69,20 @@ async def test_system_prompt_english_character(self, sample_english_character, s mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en") - - # Check that agent was created - assert agent is not None - assert agent.name == "roleplay_patient_en_medical_interview" - - # Check system prompt contains English language instruction - instruction = agent.instruction - assert "You are Sarah, a 65-year-old woman with chronic back pain" in instruction - assert "Practice taking medical history from a patient" in instruction - assert "Respond in English language" in instruction - assert "Stay fully in character" in instruction - assert "Do NOT break character" in instruction + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None + assert agent.name == "roleplay_patient_en_medical_interview" + + # Check system prompt contains English language instruction + instruction = agent.instruction + assert "You are Sarah, a 65-year-old woman with chronic back pain" in instruction + assert "Practice taking medical history from a patient" in instruction + assert "Respond in English language" in instruction + assert "Stay fully in character" in instruction + assert "Do NOT break character" in instruction @pytest.mark.asyncio async def test_system_prompt_chinese_character(self, sample_chinese_character, sample_scenario, mock_resource_loader): @@ -92,20 +91,19 @@ async def test_system_prompt_chinese_character(self, sample_chinese_character, s mock_resource_loader.get_character_by_id.return_value = sample_chinese_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_zh_tw", "medical_interview", "zh-TW") - - # Check that agent was created - assert agent is not None - assert agent.name == "roleplay_patient_zh_tw_medical_interview" + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_zh_tw", "medical_interview", "zh-TW", resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None + assert agent.name == "roleplay_patient_zh_tw_medical_interview" - # Check system prompt contains Traditional Chinese language instruction - instruction = agent.instruction - assert "你是李小姐,一位65歲患有慢性背痛的女性" in instruction - assert "Practice taking medical history from a patient" in instruction - assert "Respond in Traditional Chinese language" in instruction - assert "Stay fully in character" in instruction + # Check system prompt contains Traditional Chinese language instruction + instruction = agent.instruction + assert "你是李小姐,一位65歲患有慢性背痛的女性" in instruction + assert "Practice taking medical history from a patient" in instruction + assert "Respond in Traditional Chinese language" in instruction + assert "Stay fully in character" in instruction @pytest.mark.asyncio async def test_system_prompt_japanese_character(self, sample_japanese_character, sample_scenario, mock_resource_loader): @@ -114,20 +112,19 @@ async def test_system_prompt_japanese_character(self, sample_japanese_character, mock_resource_loader.get_character_by_id.return_value = sample_japanese_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_ja", "medical_interview", "ja") - - # Check that agent was created - assert agent is not None - assert agent.name == "roleplay_patient_ja_medical_interview" + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_ja", "medical_interview", "ja", resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None + assert agent.name == "roleplay_patient_ja_medical_interview" - # Check system prompt contains Japanese language instruction - instruction = agent.instruction - assert "あなたは田中さん、65歳の慢性的な腰痛を持つ女性です" in instruction - assert "Practice taking medical history from a patient" in instruction - assert "Respond in Japanese language" in instruction - assert "Stay fully in character" in instruction + # Check system prompt contains Japanese language instruction + instruction = agent.instruction + assert "あなたは田中さん、65歳の慢性的な腰痛を持つ女性です" in instruction + assert "Practice taking medical history from a patient" in instruction + assert "Respond in Japanese language" in instruction + assert "Stay fully in character" in instruction @pytest.mark.asyncio async def test_system_prompt_character_without_language_defaults_to_english(self, sample_scenario, mock_resource_loader): @@ -143,17 +140,16 @@ async def test_system_prompt_character_without_language_defaults_to_english(self mock_resource_loader.get_character_by_id.return_value = character_no_lang mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_no_lang", "medical_interview", "en") - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_no_lang", "medical_interview", "en", resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt defaults to English - instruction = agent.instruction - assert "You are a test patient." in instruction - assert "Respond in English language" in instruction + # Check system prompt defaults to English + instruction = agent.instruction + assert "You are a test patient." in instruction + assert "Respond in English language" in instruction @pytest.mark.asyncio async def test_system_prompt_unsupported_language_defaults_to_english(self, sample_scenario, mock_resource_loader): @@ -170,17 +166,16 @@ async def test_system_prompt_unsupported_language_defaults_to_english(self, samp mock_resource_loader.get_character_by_id.return_value = character_unsupported mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_fr", "medical_interview", "fr") - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_fr", "medical_interview", "fr", resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt defaults to English for unsupported language - instruction = agent.instruction - assert "Vous êtes un patient français." in instruction - assert "Respond in English language" in instruction # Should default to English + # Check system prompt defaults to English for unsupported language + instruction = agent.instruction + assert "Vous êtes un patient français." in instruction + assert "Respond in English language" in instruction # Should default to English @pytest.mark.asyncio async def test_system_prompt_with_script(self, sample_english_character, sample_scenario, mock_resource_loader): @@ -189,16 +184,15 @@ async def test_system_prompt_with_script(self, sample_english_character, sample_ mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=True) - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=True, resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt contains scripted prompt - instruction = agent.instruction - assert 'You are improvising based on "character" part of the script below' in instruction + # Check system prompt contains scripted prompt + instruction = agent.instruction + assert 'You are improvising based on "character" part of the script below' in instruction @pytest.mark.asyncio async def test_system_prompt_without_script(self, sample_english_character, sample_scenario, mock_resource_loader): @@ -207,16 +201,15 @@ async def test_system_prompt_without_script(self, sample_english_character, samp mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=False) - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=False, resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt does not contain scripted prompt - instruction = agent.instruction - assert 'You are improvising based on "character" part of the script below' not in instruction + # Check system prompt does not contain scripted prompt + instruction = agent.instruction + assert 'You are improvising based on "character" part of the script below' not in instruction @pytest.mark.asyncio async def test_system_prompt_with_script(self, sample_english_character, sample_scenario, mock_resource_loader): @@ -225,16 +218,15 @@ async def test_system_prompt_with_script(self, sample_english_character, sample_ mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=True) - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=True, resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt contains scripted prompt - instruction = agent.instruction - assert 'You are improvising based on "character" part of the script below' in instruction + # Check system prompt contains scripted prompt + instruction = agent.instruction + assert 'You are improvising based on "character" part of the script below' in instruction @pytest.mark.asyncio async def test_system_prompt_without_script(self, sample_english_character, sample_scenario, mock_resource_loader): @@ -243,16 +235,15 @@ async def test_system_prompt_without_script(self, sample_english_character, samp mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=False) - - # Check that agent was created - assert agent is not None + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", scripted=False, resource_loader=mock_resource_loader) + + # Check that agent was created + assert agent is not None - # Check system prompt does not contain scripted prompt - instruction = agent.instruction - assert 'You are improvising based on "character" part of the script below' not in instruction + # Check system prompt does not contain scripted prompt + instruction = agent.instruction + assert 'You are improvising based on "character" part of the script below' not in instruction @pytest.mark.asyncio async def test_system_prompt_structure(self, sample_english_character, sample_scenario, mock_resource_loader): @@ -261,21 +252,20 @@ async def test_system_prompt_structure(self, sample_english_character, sample_sc mock_resource_loader.get_character_by_id.return_value = sample_english_character mock_resource_loader.get_scenario_by_id.return_value = sample_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("patient_en", "medical_interview", "en") - instruction = agent.instruction + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("patient_en", "medical_interview", "en", resource_loader=mock_resource_loader) + instruction = agent.instruction - # Check that all required sections are present - assert "**Current Scenario:**" in instruction - assert "**Roleplay Instructions:**" in instruction + # Check that all required sections are present + assert "**Current Scenario:**" in instruction + assert "**Roleplay Instructions:**" in instruction - # Check all roleplay instructions are present - assert "Stay fully in character" in instruction - assert "Do NOT break character or mention you are an AI" in instruction - assert "Respond naturally based on your character's personality" in instruction - assert "IMPORTANT: Respond in" in instruction # Language instruction - assert "Engage with the user's messages within the roleplay context" in instruction + # Check all roleplay instructions are present + assert "Stay fully in character" in instruction + assert "Do NOT break character or mention you are an AI" in instruction + assert "Respond naturally based on your character's personality" in instruction + assert "IMPORTANT: Respond in" in instruction # Language instruction + assert "Engage with the user's messages within the roleplay context" in instruction @pytest.mark.asyncio async def test_system_prompt_with_missing_fields(self, mock_resource_loader): @@ -287,16 +277,15 @@ async def test_system_prompt_with_missing_fields(self, mock_resource_loader): mock_resource_loader.get_character_by_id.return_value = minimal_character mock_resource_loader.get_scenario_by_id.return_value = minimal_scenario - with patch('role_play.dev_agents.roleplay_agent.agent.resource_loader', mock_resource_loader): - from role_play.dev_agents.roleplay_agent.agent import get_production_agent - agent = await get_production_agent("min_char", "min_scenario", "en") + from role_play.dev_agents.roleplay_agent.agent import get_production_agent + agent = await get_production_agent("min_char", "min_scenario", "en", resource_loader=mock_resource_loader) - # Should handle missing fields gracefully - assert agent is not None - instruction = agent.instruction - assert "You are a helpful assistant." in instruction # Default system prompt - assert "No specific scenario description." in instruction # Default scenario description - assert "Respond in English language" in instruction # Default language + # Should handle missing fields gracefully + assert agent is not None + instruction = agent.instruction + assert "You are a helpful assistant." in instruction # Default system prompt + assert "No specific scenario description." in instruction # Default scenario description + assert "Respond in English language" in instruction # Default language class TestChatHandlerReadOnlySession: