Spaces:
Sleeping
Sleeping
Download tests/test_agents.py from 01ammu/data-cleaning-openenv: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/01ammu/data-cleaning-openenv/resolve/main/tests/test_agents.py
- Command line
-
hf download hf://spaces/01ammu/data-cleaning-openenv/tests/test_agents.py
-
curl -L -o test_agents.py https://huggingface.co/spaces/01ammu/data-cleaning-openenv/resolve/main/tests/test_agents.py
19.9 kB
| """ | |
| Comprehensive tests for multi-agent data cleaning system. | |
| This module tests all agent components including base agents, | |
| specialist agents, coordinator, and reward shaper. | |
| """ | |
| import pytest | |
| import numpy as np | |
| from unittest.mock import Mock, patch | |
| import sys | |
| import os | |
| # Add backend to path for imports | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| # Also add backend so package-style imports like `ml.*` work | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend")) | |
| from ml.base_agent import Agent, AgentFactory | |
| from ml.specialist_agents import ( | |
| FillMissingAgent, | |
| DuplicateDetector, | |
| OutlierHandler, | |
| CategoryStandardizer, | |
| SkipAgent, | |
| ) | |
| from ml.agent_coordinator import AgentCoordinator | |
| from ml.reward_shaper import RewardShaper | |
| class TestBaseAgent: | |
| """Test the abstract base agent class.""" | |
| def test_agent_initialization(self): | |
| """Test agent initialization with name.""" | |
| class TestAgent(Agent): | |
| def reset(self, observation): | |
| pass | |
| def get_action(self, observation, legal_actions): | |
| return {} | |
| def get_confidence(self): | |
| return 0.5 | |
| agent = TestAgent("TestAgent") | |
| assert agent.get_name() == "TestAgent" | |
| assert agent.get_last_reward() == 0.0 | |
| def test_update_reward(self): | |
| """Test reward update functionality.""" | |
| class TestAgent(Agent): | |
| def reset(self, observation): | |
| pass | |
| def get_action(self, observation, legal_actions): | |
| return {} | |
| def get_confidence(self): | |
| return 0.5 | |
| agent = TestAgent("TestAgent") | |
| agent.update_reward(0.5) | |
| assert agent.get_last_reward() == 0.5 | |
| agent.update_reward(-0.2) | |
| assert agent.get_last_reward() == -0.2 | |
| def test_agent_factory_unknown_type(self): | |
| """Test AgentFactory with unknown agent type.""" | |
| with pytest.raises(ValueError, match="Unknown agent type"): | |
| AgentFactory.create_agent("unknown_agent") | |
| class TestFillMissingAgent: | |
| """Test the FillMissingAgent specialist agent.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.agent = FillMissingAgent() | |
| def test_agent_initialization(self): | |
| """Test agent initialization.""" | |
| assert self.agent.get_name() == "FillMissingAgent" | |
| assert self.agent.get_confidence() == 0.0 | |
| def test_reset(self): | |
| """Test agent reset functionality.""" | |
| observation = {"test": "data"} | |
| self.agent.reset(observation) | |
| assert self.agent.get_confidence() == 0.0 | |
| def test_handle_missing_age(self): | |
| """Test handling missing age values.""" | |
| observation = { | |
| "current_data": {"age": None, "name": "John"}, | |
| "issues_detected": ["missing:age"], | |
| } | |
| legal_actions = ["fill_missing", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "fill_missing" | |
| assert action["column"] == "age" | |
| assert action["value"] == 30 | |
| assert self.agent.get_confidence() > 0.0 | |
| def test_handle_missing_salary(self): | |
| """Test handling missing salary values.""" | |
| observation = { | |
| "current_data": {"salary": None, "name": "John"}, | |
| "issues_detected": ["missing:salary"], | |
| } | |
| legal_actions = ["fill_missing", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "fill_missing" | |
| assert action["column"] == "salary" | |
| assert action["value"] == 60000 | |
| def test_skip_when_no_fill_missing_action(self): | |
| """Test skipping when fill_missing is not available.""" | |
| observation = { | |
| "current_data": {"age": None, "name": "John"}, | |
| "issues_detected": ["missing:age"], | |
| } | |
| legal_actions = ["skip"] # No fill_missing action | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "skip" | |
| assert self.agent.get_confidence() == 0.0 | |
| def test_can_handle_missing_issues(self): | |
| """Test can_handle method with missing issues.""" | |
| observation = {"issues_detected": ["missing:age", "duplicate:row"]} | |
| assert self.agent.can_handle(observation) == True | |
| observation = {"issues_detected": ["duplicate:row", "outlier:age"]} | |
| assert self.agent.can_handle(observation) == False | |
| class TestDuplicateDetector: | |
| """Test the DuplicateDetector specialist agent.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.agent = DuplicateDetector() | |
| def test_agent_initialization(self): | |
| """Test agent initialization.""" | |
| assert self.agent.get_name() == "DuplicateDetector" | |
| assert self.agent.get_confidence() == 0.0 | |
| def test_detect_duplicate_row(self): | |
| """Test duplicate detection with similar rows.""" | |
| # First observation | |
| obs1 = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": ["duplicate:row"], | |
| } | |
| legal_actions = ["remove_duplicate", "skip"] | |
| action1 = self.agent.get_action(obs1, legal_actions) | |
| assert action1["action_type"] == "skip" # No previous rows to compare | |
| # Second observation with similar data | |
| obs2 = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": ["duplicate:row"], | |
| } | |
| action2 = self.agent.get_action(obs2, legal_actions) | |
| assert action2["action_type"] == "remove_duplicate" | |
| assert self.agent.get_confidence() >= 0.8 | |
| def test_no_duplicate_with_different_rows(self): | |
| """Test no duplicate detection with different rows.""" | |
| # First observation | |
| obs1 = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": ["duplicate:row"], | |
| } | |
| legal_actions = ["remove_duplicate", "skip"] | |
| self.agent.get_action(obs1, legal_actions) | |
| # Second observation with different data | |
| obs2 = { | |
| "current_data": {"name": "Jane", "age": 25}, | |
| "issues_detected": ["duplicate:row"], | |
| } | |
| action2 = self.agent.get_action(obs2, legal_actions) | |
| assert action2["action_type"] == "skip" | |
| assert self.agent.get_confidence() < 0.8 | |
| class TestOutlierHandler: | |
| """Test the OutlierHandler specialist agent.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.agent = OutlierHandler() | |
| def test_detect_age_outlier(self): | |
| """Test outlier detection for age.""" | |
| observation = { | |
| "current_data": {"age": 150, "name": "John"}, # Extreme outlier | |
| "issues_detected": ["outlier:age"], | |
| } | |
| legal_actions = ["remove_outlier", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "remove_outlier" | |
| assert action["column"] == "age" | |
| assert action["value"] == 35 # Median replacement | |
| assert self.agent.get_confidence() >= 0.7 | |
| def test_detect_salary_outlier(self): | |
| """Test outlier detection for salary.""" | |
| observation = { | |
| "current_data": {"salary": 500000, "name": "John"}, # Extreme outlier | |
| "issues_detected": ["outlier:salary"], | |
| } | |
| legal_actions = ["remove_outlier", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "remove_outlier" | |
| assert action["column"] == "salary" | |
| assert action["value"] == 75000 # Median replacement | |
| def test_no_outlier_normal_values(self): | |
| """Test no outlier detection for normal values.""" | |
| observation = { | |
| "current_data": {"age": 35, "name": "John"}, # Normal value | |
| "issues_detected": ["outlier:age"], | |
| } | |
| legal_actions = ["remove_outlier", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "skip" | |
| assert self.agent.get_confidence() == 0.0 | |
| class TestCategoryStandardizer: | |
| """Test the CategoryStandardizer specialist agent.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.agent = CategoryStandardizer() | |
| def test_standardize_department_typo(self): | |
| """Test standardizing department name with typo.""" | |
| observation = { | |
| "current_data": {"dept": "enginering", "name": "John"}, # Typo | |
| "issues_detected": ["category:dept"], | |
| } | |
| legal_actions = ["fix_category", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "fix_category" | |
| assert action["column"] == "dept" | |
| assert action["value"] == "engineering" # Corrected spelling | |
| assert self.agent.get_confidence() > 0.0 | |
| def test_no_standardization_needed(self): | |
| """Test when no standardization is needed.""" | |
| observation = { | |
| "current_data": {"dept": "engineering", "name": "John"}, # Already correct | |
| "issues_detected": ["category:dept"], | |
| } | |
| legal_actions = ["fix_category", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| # The agent may still return fix_category even if no change is needed | |
| # This is acceptable behavior as it will determine if standardization is required | |
| assert action["action_type"] in ["fix_category", "skip"] | |
| def test_handle_case_variations(self): | |
| """Test handling case variations.""" | |
| observation = { | |
| "current_data": {"dept": "ENGINEERING", "name": "John"}, # Uppercase | |
| "issues_detected": ["category:dept"], | |
| } | |
| legal_actions = ["fix_category", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "fix_category" | |
| assert action["value"] == "engineering" # Lowercased | |
| class TestSkipAgent: | |
| """Test the SkipAgent specialist agent.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.agent = SkipAgent() | |
| def test_always_skip(self): | |
| """Test that SkipAgent always returns skip action.""" | |
| observation = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": ["missing:age", "duplicate:row"], | |
| } | |
| legal_actions = ["fill_missing", "remove_duplicate", "skip"] | |
| action = self.agent.get_action(observation, legal_actions) | |
| assert action["action_type"] == "skip" | |
| assert self.agent.get_confidence() == 0.2 | |
| def test_can_handle_anything(self): | |
| """Test that SkipAgent can handle any observation.""" | |
| observation = {"issues_detected": []} | |
| assert self.agent.can_handle(observation) == True | |
| observation = {"issues_detected": ["missing:age"]} | |
| assert self.agent.can_handle(observation) == True | |
| class TestAgentCoordinator: | |
| """Test the AgentCoordinator class.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.coordinator = AgentCoordinator() | |
| def test_initialization(self): | |
| """Test coordinator initialization.""" | |
| assert len(self.coordinator.agents) == 5 # 5 specialist agents | |
| assert all(isinstance(agent, Agent) for agent in self.coordinator.agents) | |
| def test_select_fill_missing_agent(self): | |
| """Test selecting FillMissingAgent for missing values.""" | |
| observation = { | |
| "current_data": {"age": None, "name": "John"}, | |
| "issues_detected": ["missing:age"], | |
| } | |
| legal_actions = ["fill_missing", "skip"] | |
| action = self.coordinator.get_best_action(observation, legal_actions) | |
| assert action["action_type"] == "fill_missing" | |
| assert action["column"] == "age" | |
| def test_fallback_to_skip_agent(self): | |
| """Test fallback to SkipAgent when no other agent can handle.""" | |
| observation = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": [], | |
| } | |
| legal_actions = ["skip"] | |
| action = self.coordinator.get_best_action(observation, legal_actions) | |
| assert action["action_type"] == "skip" | |
| def test_update_agent_performance(self): | |
| """Test updating agent performance tracking.""" | |
| self.coordinator.update_agent_performance("FillMissingAgent", 0.3) | |
| self.coordinator.update_agent_performance("FillMissingAgent", -0.1) | |
| stats = self.coordinator.get_agent_statistics() | |
| assert "FillMissingAgent" in stats | |
| assert ( | |
| stats["FillMissingAgent"]["usage_count"] == 2 | |
| ) # Two calls to update_agent_performance | |
| assert abs(stats["FillMissingAgent"]["average_reward"] - 0.1) < 1e-9 | |
| def test_agent_statistics(self): | |
| """Test getting agent statistics.""" | |
| # Simulate some usage | |
| self.coordinator.update_agent_performance("FillMissingAgent", 0.3) | |
| self.coordinator.update_agent_performance("DuplicateDetector", 0.2) | |
| stats = self.coordinator.get_agent_statistics() | |
| assert isinstance(stats, dict) | |
| assert "FillMissingAgent" in stats | |
| assert "DuplicateDetector" in stats | |
| class TestRewardShaper: | |
| """Test the RewardShaper class.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| self.reward_shaper = RewardShaper() | |
| def test_initialization(self): | |
| """Test reward shaper initialization.""" | |
| assert len(self.reward_shaper.base_rewards) > 0 | |
| assert "fill_missing" in self.reward_shaper.base_rewards | |
| assert self.reward_shaper.base_rewards["fill_missing"] == 0.3 | |
| def test_calculate_fill_missing_reward(self): | |
| """Test reward calculation for fill missing action.""" | |
| action = {"action_type": "fill_missing", "column": "age", "value": 30} | |
| observation = { | |
| "current_data": {"age": None, "name": "John"}, | |
| "issues_detected": ["missing:age"], | |
| } | |
| episode_state = {"step": 10, "total_steps": 50} | |
| reward = self.reward_shaper.calculate_reward(action, observation, episode_state) | |
| # Should include base reward + issue adjustment + progress bonus | |
| assert reward > 0.3 # Base reward is 0.3, should have bonuses | |
| def test_calculate_skip_reward(self): | |
| """Test reward calculation for skip action.""" | |
| action = {"action_type": "skip", "column": "name", "value": None} | |
| observation = { | |
| "current_data": {"name": "John", "age": 30}, | |
| "issues_detected": [], # No major issues | |
| } | |
| episode_state = {"step": 5, "total_steps": 50} | |
| reward = self.reward_shaper.calculate_reward(action, observation, episode_state) | |
| # Should be negative base reward + skip bonus | |
| assert reward > -0.15 # Base is -0.15, skip bonus should make it less negative | |
| def test_episode_completion_bonus(self): | |
| """Test episode completion bonus calculation.""" | |
| # Perfect score | |
| bonus = self.reward_shaper.calculate_episode_completion_bonus(0.95) | |
| assert bonus == 0.5 | |
| # Good score | |
| bonus = self.reward_shaper.calculate_episode_completion_bonus(0.85) | |
| assert bonus == 0.3 | |
| # Poor score | |
| bonus = self.reward_shaper.calculate_episode_completion_bonus(0.4) | |
| assert bonus == 0.0 | |
| def test_contradiction_penalty(self): | |
| """Test contradiction penalty for conflicting actions.""" | |
| # First action: fill missing | |
| action1 = {"action_type": "fill_missing", "column": "age", "value": 30} | |
| obs1 = {"current_data": {"age": None}, "issues_detected": ["missing:age"]} | |
| episode_state = {"step": 1, "total_steps": 50} | |
| self.reward_shaper.calculate_reward(action1, obs1, episode_state) | |
| # Second action: different action on same column | |
| action2 = {"action_type": "remove_outlier", "column": "age", "value": 35} | |
| obs2 = {"current_data": {"age": 30}, "issues_detected": ["outlier:age"]} | |
| episode_state = {"step": 2, "total_steps": 50} | |
| reward = self.reward_shaper.calculate_reward(action2, obs2, episode_state) | |
| # Should have contradiction penalty | |
| assert reward < 0.3 # Base reward reduced by penalty | |
| def test_reward_statistics(self): | |
| """Test getting reward statistics.""" | |
| # Generate some rewards | |
| for i in range(5): | |
| action = {"action_type": "fill_missing", "column": "age", "value": 30} | |
| observation = { | |
| "current_data": {"age": None}, | |
| "issues_detected": ["missing:age"], | |
| } | |
| episode_state = {"step": i, "total_steps": 50} | |
| self.reward_shaper.calculate_reward(action, observation, episode_state) | |
| stats = self.reward_shaper.get_reward_statistics() | |
| assert stats["total_actions"] == 5 | |
| assert "average_reward" in stats | |
| assert "action_type_distribution" in stats | |
| assert "fill_missing" in stats["action_type_distribution"] | |
| class TestIntegration: | |
| """Integration tests for the complete system.""" | |
| def test_full_episode_simulation(self): | |
| """Test a complete episode simulation.""" | |
| coordinator = AgentCoordinator() | |
| reward_shaper = RewardShaper() | |
| # Reset for new episode | |
| initial_obs = {"current_data": {}, "issues_detected": []} | |
| coordinator.reset(initial_obs) | |
| reward_shaper.reset_episode() | |
| # Simulate episode steps | |
| observations = [ | |
| { | |
| "current_data": {"age": None, "name": "John"}, | |
| "issues_detected": ["missing:age"], | |
| }, | |
| {"current_data": {"age": 30, "name": "John"}, "issues_detected": []}, | |
| ] | |
| legal_actions = ["fill_missing", "skip"] | |
| for i, obs in enumerate(observations): | |
| # Get action from coordinator | |
| action = coordinator.get_best_action(obs, legal_actions) | |
| # Calculate reward | |
| episode_state = {"step": i, "total_steps": len(observations)} | |
| reward = reward_shaper.calculate_reward(action, obs, episode_state) | |
| # Update coordinator | |
| agent_name = ( | |
| action.get("action_type", "skip").replace("_", "").capitalize() | |
| + "Agent" | |
| ) | |
| coordinator.update_agent_performance(agent_name, reward) | |
| # Verify action is valid | |
| assert ( | |
| action["action_type"] in legal_actions | |
| or action["action_type"] == "skip" | |
| ) | |
| assert isinstance(reward, float) | |
| # Get final statistics | |
| agent_stats = coordinator.get_agent_statistics() | |
| reward_stats = reward_shaper.get_reward_statistics() | |
| assert agent_stats["total_actions"] > 0 | |
| assert reward_stats["total_actions"] == len(observations) | |
| def test_error_handling_fallback(self): | |
| """Test error handling and fallback to SkipAgent.""" | |
| coordinator = AgentCoordinator() | |
| # Create observation that might cause issues | |
| observation = { | |
| "current_data": None, # None data might cause issues | |
| "issues_detected": ["missing:unknown_column"], | |
| } | |
| legal_actions = ["fill_missing", "skip"] | |
| # Should handle gracefully and return skip action | |
| action = coordinator.get_best_action(observation, legal_actions) | |
| assert action["action_type"] == "skip" | |
| assert "column" in action | |
| assert action["value"] is None | |
| if __name__ == "__main__": | |
| # Run tests when executed directly | |
| pytest.main([__file__, "-v"]) | |