| | |
| |
|
| | import pytest |
| | import json |
| | from unittest.mock import AsyncMock, MagicMock, patch |
| | from datetime import datetime |
| |
|
| | from ankigen_core.agents.generators import SubjectExpertAgent, PedagogicalAgent |
| | from ankigen_core.agents.base import AgentConfig |
| | from ankigen_core.models import Card, CardFront, CardBack |
| |
|
| |
|
| | |
| | @pytest.fixture |
| | def mock_openai_client(): |
| | """Mock OpenAI client for testing""" |
| | return MagicMock() |
| |
|
| |
|
| | @pytest.fixture |
| | def sample_card(): |
| | """Sample card for testing""" |
| | return Card( |
| | card_type="basic", |
| | front=CardFront(question="What is Python?"), |
| | back=CardBack( |
| | answer="A programming language", |
| | explanation="Python is a high-level, interpreted programming language", |
| | example="print('Hello, World!')" |
| | ), |
| | metadata={ |
| | "difficulty": "beginner", |
| | "subject": "programming", |
| | "topic": "Python Basics" |
| | } |
| | ) |
| |
|
| |
|
| | @pytest.fixture |
| | def sample_cards_json(): |
| | """Sample JSON response for card generation""" |
| | return { |
| | "cards": [ |
| | { |
| | "card_type": "basic", |
| | "front": { |
| | "question": "What is a Python function?" |
| | }, |
| | "back": { |
| | "answer": "A reusable block of code", |
| | "explanation": "Functions help organize code into reusable components", |
| | "example": "def hello(): print('hello')" |
| | }, |
| | "metadata": { |
| | "difficulty": "beginner", |
| | "prerequisites": ["variables"], |
| | "topic": "Functions", |
| | "subject": "programming", |
| | "learning_outcomes": ["understanding functions"], |
| | "common_misconceptions": ["functions are variables"] |
| | } |
| | }, |
| | { |
| | "card_type": "basic", |
| | "front": { |
| | "question": "How do you define a function in Python?" |
| | }, |
| | "back": { |
| | "answer": "Using the 'def' keyword", |
| | "explanation": "The 'def' keyword starts a function definition", |
| | "example": "def my_function(): pass" |
| | }, |
| | "metadata": { |
| | "difficulty": "beginner", |
| | "prerequisites": ["functions"], |
| | "topic": "Functions", |
| | "subject": "programming" |
| | } |
| | } |
| | ] |
| | } |
| |
|
| |
|
| | |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_subject_expert_agent_init_with_config(mock_get_config_manager, mock_openai_client): |
| | """Test SubjectExpertAgent initialization with existing config""" |
| | mock_config_manager = MagicMock() |
| | mock_config = AgentConfig( |
| | name="subject_expert", |
| | instructions="Test instructions", |
| | model="gpt-4o" |
| | ) |
| | mock_config_manager.get_agent_config.return_value = mock_config |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="mathematics") |
| | |
| | assert agent.subject == "mathematics" |
| | assert agent.config == mock_config |
| | mock_config_manager.get_agent_config.assert_called_once_with("subject_expert") |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_subject_expert_agent_init_fallback_config(mock_get_config_manager, mock_openai_client): |
| | """Test SubjectExpertAgent initialization with fallback config""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="physics") |
| | |
| | assert agent.subject == "physics" |
| | assert agent.config.name == "subject_expert" |
| | assert "physics" in agent.config.instructions |
| | assert agent.config.model == "gpt-4o" |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_subject_expert_agent_init_with_custom_prompts(mock_get_config_manager, mock_openai_client): |
| | """Test SubjectExpertAgent initialization with custom prompts""" |
| | mock_config_manager = MagicMock() |
| | mock_config = AgentConfig( |
| | name="subject_expert", |
| | instructions="Base instructions", |
| | model="gpt-4o", |
| | custom_prompts={"mathematics": "Focus on mathematical rigor"} |
| | ) |
| | mock_config_manager.get_agent_config.return_value = mock_config |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="mathematics") |
| | |
| | assert "Focus on mathematical rigor" in agent.config.instructions |
| |
|
| |
|
| | def test_subject_expert_agent_build_generation_prompt(): |
| | """Test building generation prompt""" |
| | with patch('ankigen_core.agents.generators.get_config_manager'): |
| | agent = SubjectExpertAgent(MagicMock(), subject="programming") |
| | |
| | prompt = agent._build_generation_prompt( |
| | topic="Python Functions", |
| | num_cards=3, |
| | difficulty="intermediate", |
| | prerequisites=["variables", "basic syntax"], |
| | context={"source_text": "Some source material about functions"} |
| | ) |
| | |
| | assert "Python Functions" in prompt |
| | assert "3" in prompt |
| | assert "intermediate" in prompt |
| | assert "programming" in prompt |
| | assert "variables, basic syntax" in prompt |
| | assert "Some source material" in prompt |
| |
|
| |
|
| | def test_subject_expert_agent_parse_cards_response_success(sample_cards_json): |
| | """Test successful card parsing""" |
| | with patch('ankigen_core.agents.generators.get_config_manager'): |
| | agent = SubjectExpertAgent(MagicMock(), subject="programming") |
| | |
| | |
| | json_string = json.dumps(sample_cards_json) |
| | cards = agent._parse_cards_response(json_string, "Functions") |
| | |
| | assert len(cards) == 2 |
| | assert cards[0].front.question == "What is a Python function?" |
| | assert cards[0].back.answer == "A reusable block of code" |
| | assert cards[0].metadata["subject"] == "programming" |
| | assert cards[0].metadata["topic"] == "Functions" |
| | |
| | |
| | cards = agent._parse_cards_response(sample_cards_json, "Functions") |
| | assert len(cards) == 2 |
| |
|
| |
|
| | def test_subject_expert_agent_parse_cards_response_invalid_json(): |
| | """Test parsing invalid JSON response""" |
| | with patch('ankigen_core.agents.generators.get_config_manager'): |
| | agent = SubjectExpertAgent(MagicMock(), subject="programming") |
| | |
| | with pytest.raises(ValueError, match="Invalid JSON response"): |
| | agent._parse_cards_response("invalid json {", "topic") |
| |
|
| |
|
| | def test_subject_expert_agent_parse_cards_response_missing_cards_field(): |
| | """Test parsing response missing cards field""" |
| | with patch('ankigen_core.agents.generators.get_config_manager'): |
| | agent = SubjectExpertAgent(MagicMock(), subject="programming") |
| | |
| | invalid_response = {"wrong_field": []} |
| | with pytest.raises(ValueError, match="Response missing 'cards' field"): |
| | agent._parse_cards_response(invalid_response, "topic") |
| |
|
| |
|
| | def test_subject_expert_agent_parse_cards_response_invalid_card_data(): |
| | """Test parsing response with invalid card data""" |
| | with patch('ankigen_core.agents.generators.get_config_manager'): |
| | agent = SubjectExpertAgent(MagicMock(), subject="programming") |
| | |
| | invalid_cards = { |
| | "cards": [ |
| | { |
| | "front": {"question": "Valid question"}, |
| | "back": {"answer": "Valid answer"} |
| | }, |
| | { |
| | "front": {}, |
| | "back": {"answer": "Answer"} |
| | }, |
| | { |
| | "front": {"question": "Question"}, |
| | "back": {} |
| | }, |
| | "invalid_card_data" |
| | ] |
| | } |
| | |
| | with patch('ankigen_core.logging.logger') as mock_logger: |
| | cards = agent._parse_cards_response(invalid_cards, "topic") |
| | |
| | |
| | assert len(cards) == 1 |
| | assert cards[0].front.question == "Valid question" |
| | |
| | |
| | assert mock_logger.warning.call_count >= 3 |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.record_agent_execution') |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_subject_expert_agent_generate_cards_success(mock_get_config_manager, mock_record, sample_cards_json, mock_openai_client): |
| | """Test successful card generation""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="programming") |
| | |
| | |
| | agent.execute = AsyncMock(return_value=json.dumps(sample_cards_json)) |
| | |
| | cards = await agent.generate_cards( |
| | topic="Python Functions", |
| | num_cards=2, |
| | difficulty="beginner", |
| | prerequisites=["variables"], |
| | context={"source": "test"} |
| | ) |
| | |
| | assert len(cards) == 2 |
| | assert cards[0].front.question == "What is a Python function?" |
| | assert cards[0].metadata["subject"] == "programming" |
| | assert cards[0].metadata["topic"] == "Python Functions" |
| | |
| | |
| | mock_record.assert_called() |
| | assert mock_record.call_args[1]["success"] is True |
| | assert mock_record.call_args[1]["metadata"]["cards_generated"] == 2 |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.record_agent_execution') |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_subject_expert_agent_generate_cards_error(mock_get_config_manager, mock_record, mock_openai_client): |
| | """Test card generation with error""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="programming") |
| | |
| | |
| | agent.execute = AsyncMock(side_effect=Exception("Generation failed")) |
| | |
| | with pytest.raises(Exception, match="Generation failed"): |
| | await agent.generate_cards(topic="Test", num_cards=1) |
| | |
| | |
| | mock_record.assert_called() |
| | assert mock_record.call_args[1]["success"] is False |
| | assert "Generation failed" in mock_record.call_args[1]["error_message"] |
| |
|
| |
|
| | |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_pedagogical_agent_init_with_config(mock_get_config_manager, mock_openai_client): |
| | """Test PedagogicalAgent initialization with existing config""" |
| | mock_config_manager = MagicMock() |
| | mock_config = AgentConfig( |
| | name="pedagogical", |
| | instructions="Pedagogical instructions", |
| | model="gpt-4o" |
| | ) |
| | mock_config_manager.get_agent_config.return_value = mock_config |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | assert agent.config == mock_config |
| | mock_config_manager.get_agent_config.assert_called_once_with("pedagogical") |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_pedagogical_agent_init_fallback_config(mock_get_config_manager, mock_openai_client): |
| | """Test PedagogicalAgent initialization with fallback config""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | assert agent.config.name == "pedagogical" |
| | assert "educational specialist" in agent.config.instructions.lower() |
| | assert agent.config.temperature == 0.6 |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.record_agent_execution') |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_pedagogical_agent_review_cards_success(mock_get_config_manager, mock_record, mock_openai_client, sample_card): |
| | """Test successful card review""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | |
| | review_response = json.dumps({ |
| | "pedagogical_quality": 8, |
| | "clarity": 9, |
| | "learning_effectiveness": 7, |
| | "suggestions": ["Add more examples"], |
| | "cognitive_load": "appropriate", |
| | "bloom_taxonomy_level": "application" |
| | }) |
| | |
| | agent.execute = AsyncMock(return_value=review_response) |
| | |
| | reviews = await agent.review_cards([sample_card]) |
| | |
| | assert len(reviews) == 1 |
| | assert reviews[0]["pedagogical_quality"] == 8 |
| | assert reviews[0]["clarity"] == 9 |
| | assert "Add more examples" in reviews[0]["suggestions"] |
| | |
| | |
| | mock_record.assert_called() |
| | assert mock_record.call_args[1]["success"] is True |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_pedagogical_agent_build_review_prompt(mock_get_config_manager, mock_openai_client, sample_card): |
| | """Test building review prompt""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | prompt = agent._build_review_prompt(sample_card, 0) |
| | |
| | assert "What is Python?" in prompt |
| | assert "A programming language" in prompt |
| | assert "pedagogical quality" in prompt.lower() |
| | assert "bloom's taxonomy" in prompt.lower() |
| | assert "cognitive load" in prompt.lower() |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_pedagogical_agent_parse_review_response_success(mock_get_config_manager, mock_openai_client): |
| | """Test successful review response parsing""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | review_data = { |
| | "pedagogical_quality": 8, |
| | "clarity": 9, |
| | "learning_effectiveness": 7, |
| | "suggestions": ["Add more examples", "Improve explanation"], |
| | "cognitive_load": "appropriate", |
| | "bloom_taxonomy_level": "application" |
| | } |
| | |
| | |
| | result = agent._parse_review_response(json.dumps(review_data)) |
| | assert result == review_data |
| | |
| | |
| | result = agent._parse_review_response(review_data) |
| | assert result == review_data |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | def test_pedagogical_agent_parse_review_response_invalid_json(mock_get_config_manager, mock_openai_client): |
| | """Test parsing invalid review response""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | |
| | with pytest.raises(ValueError, match="Invalid review response"): |
| | agent._parse_review_response("invalid json {") |
| | |
| | |
| | incomplete_response = {"pedagogical_quality": 8} |
| | with pytest.raises(ValueError, match="Invalid review response"): |
| | agent._parse_review_response(incomplete_response) |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.record_agent_execution') |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_pedagogical_agent_review_cards_error(mock_get_config_manager, mock_record, mock_openai_client, sample_card): |
| | """Test card review with error""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | |
| | agent.execute = AsyncMock(side_effect=Exception("Review failed")) |
| | |
| | with pytest.raises(Exception, match="Review failed"): |
| | await agent.review_cards([sample_card]) |
| | |
| | |
| | mock_record.assert_called() |
| | assert mock_record.call_args[1]["success"] is False |
| |
|
| |
|
| | |
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_subject_expert_agent_end_to_end(mock_get_config_manager, mock_openai_client, sample_cards_json): |
| | """Test end-to-end SubjectExpertAgent workflow""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = SubjectExpertAgent(mock_openai_client, subject="programming") |
| | |
| | |
| | with patch.object(agent, 'initialize') as mock_init, \ |
| | patch.object(agent, '_run_agent') as mock_run: |
| | |
| | mock_run.return_value = json.dumps(sample_cards_json) |
| | |
| | cards = await agent.generate_cards( |
| | topic="Python Functions", |
| | num_cards=2, |
| | difficulty="beginner", |
| | prerequisites=["variables"], |
| | context={"source_text": "Function tutorial content"} |
| | ) |
| | |
| | |
| | assert len(cards) == 2 |
| | assert all(isinstance(card, Card) for card in cards) |
| | assert cards[0].front.question == "What is a Python function?" |
| | assert cards[0].metadata["subject"] == "programming" |
| | assert cards[0].metadata["topic"] == "Python Functions" |
| | |
| | |
| | mock_init.assert_called_once() |
| | mock_run.assert_called_once() |
| | |
| | |
| | call_args = mock_run.call_args[0][0] |
| | assert "Python Functions" in call_args |
| | assert "2" in call_args |
| | assert "beginner" in call_args |
| | assert "variables" in call_args |
| | assert "Function tutorial content" in call_args |
| |
|
| |
|
| | @patch('ankigen_core.agents.generators.get_config_manager') |
| | async def test_pedagogical_agent_end_to_end(mock_get_config_manager, mock_openai_client, sample_card): |
| | """Test end-to-end PedagogicalAgent workflow""" |
| | mock_config_manager = MagicMock() |
| | mock_config_manager.get_agent_config.return_value = None |
| | mock_get_config_manager.return_value = mock_config_manager |
| | |
| | agent = PedagogicalAgent(mock_openai_client) |
| | |
| | review_response = { |
| | "pedagogical_quality": 8, |
| | "clarity": 9, |
| | "learning_effectiveness": 7, |
| | "suggestions": ["Add more practical examples"], |
| | "cognitive_load": "appropriate", |
| | "bloom_taxonomy_level": "knowledge" |
| | } |
| | |
| | |
| | with patch.object(agent, 'initialize') as mock_init, \ |
| | patch.object(agent, '_run_agent') as mock_run: |
| | |
| | mock_run.return_value = json.dumps(review_response) |
| | |
| | reviews = await agent.review_cards([sample_card]) |
| | |
| | |
| | assert len(reviews) == 1 |
| | assert reviews[0]["pedagogical_quality"] == 8 |
| | assert reviews[0]["clarity"] == 9 |
| | assert "Add more practical examples" in reviews[0]["suggestions"] |
| | |
| | |
| | mock_init.assert_called_once() |
| | mock_run.assert_called_once() |
| | |
| | |
| | call_args = mock_run.call_args[0][0] |
| | assert sample_card.front.question in call_args |
| | assert sample_card.back.answer in call_args |