feat(04-03): implement AnalyzeProcessor with DeepAnalyzer integration
- AnalyzeProcessor wraps existing DeepAnalyzer for pipeline integration - Loads metadata and content from JSON files - Generates analysis reports as Markdown with frontmatter - Records state on success and failure - Supports both video and article content types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,10 +12,24 @@ import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from kb_pipeline.schemas import AnalyzeInput, AnalyzeOutput
|
||||
|
||||
|
||||
# Mock AnalysisResult for testing
|
||||
@dataclass
|
||||
class MockAnalysisResult:
|
||||
"""Mock AnalysisResult for testing."""
|
||||
summary: str = "Test summary"
|
||||
main_topics: List[str] = field(default_factory=lambda: ["Topic 1", "Topic 2"])
|
||||
key_insights: List[str] = field(default_factory=lambda: ["Insight 1"])
|
||||
recommended_tags: List[str] = field(default_factory=lambda: ["tag1", "tag2"])
|
||||
full_analysis: str = "## Summary\n\nTest analysis content"
|
||||
original_text: str = ""
|
||||
|
||||
|
||||
class TestAnalyzeInputSchema:
|
||||
"""Tests for AnalyzeInput schema validation."""
|
||||
|
||||
@@ -156,21 +170,9 @@ class TestAnalyzeProcessorIntegration:
|
||||
"""Tests for AnalyzeProcessor DeepAnalyzer integration."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_deep_analyzer(self):
|
||||
"""Create a mock DeepAnalyzer result."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
summary: str = "Test summary"
|
||||
main_topics: List[str] = field(default_factory=lambda: ["Topic 1", "Topic 2"])
|
||||
key_insights: List[str] = field(default_factory=lambda: ["Insight 1"])
|
||||
recommended_tags: List[str] = field(default_factory=lambda: ["tag1", "tag2"])
|
||||
full_analysis: str = "## Summary\n\nTest analysis content"
|
||||
original_text: str = ""
|
||||
|
||||
return AnalysisResult()
|
||||
def mock_analysis_result(self):
|
||||
"""Create a mock AnalysisResult."""
|
||||
return MockAnalysisResult()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_state_manager(self):
|
||||
@@ -208,7 +210,7 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_loads_metadata_and_content_from_files(
|
||||
self, mock_deep_analyzer, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
):
|
||||
"""Test 1: AnalyzeProcessor loads metadata and content from files"""
|
||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
||||
@@ -227,24 +229,26 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock()
|
||||
with patch("kb_pipeline.processors.analyze.DeepAnalyzer") as MockDeepAnalyzer:
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_analyzer_instance.analyze = MagicMock(return_value=mock_deep_analyzer)
|
||||
MockDeepAnalyzer.return_value = mock_analyzer_instance
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
result = await processor.process(input_data)
|
||||
# Mock _run_analysis to return our mock result
|
||||
async def mock_run_analysis(metadata, content, content_type):
|
||||
# Verify metadata was loaded correctly
|
||||
assert "title" in metadata
|
||||
assert metadata["title"] == "Test Video Title"
|
||||
# Verify content was loaded correctly
|
||||
assert "text" in content
|
||||
return mock_analysis_result
|
||||
|
||||
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
|
||||
with patch.object(processor, '_write_analysis_report'):
|
||||
result = await processor.process(input_data)
|
||||
|
||||
assert result.success is True
|
||||
# Verify DeepAnalyzer.analyze was called with loaded data
|
||||
mock_analyzer_instance.analyze.assert_called_once()
|
||||
call_args = mock_analyzer_instance.analyze.call_args
|
||||
assert "title" in call_args[0][0] # metadata dict
|
||||
assert "text" in call_args[0][1] # content dict
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_calls_deep_analyzer_with_correct_parameters(
|
||||
self, mock_deep_analyzer, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
):
|
||||
"""Test 2: AnalyzeProcessor calls DeepAnalyzer.analyze() with correct parameters"""
|
||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
||||
@@ -263,22 +267,28 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock()
|
||||
with patch("kb_pipeline.processors.analyze.DeepAnalyzer") as MockDeepAnalyzer:
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_analyzer_instance.analyze = MagicMock(return_value=mock_deep_analyzer)
|
||||
MockDeepAnalyzer.return_value = mock_analyzer_instance
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
result = await processor.process(input_data)
|
||||
# Track what parameters were passed
|
||||
captured_params = {}
|
||||
|
||||
async def mock_run_analysis(metadata, content, content_type):
|
||||
captured_params['metadata'] = metadata
|
||||
captured_params['content'] = content
|
||||
captured_params['content_type'] = content_type
|
||||
return mock_analysis_result
|
||||
|
||||
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
|
||||
with patch.object(processor, '_write_analysis_report'):
|
||||
result = await processor.process(input_data)
|
||||
|
||||
assert result.success is True
|
||||
# Verify content_type was passed correctly
|
||||
call_kwargs = mock_analyzer_instance.analyze.call_args
|
||||
assert call_kwargs[0][2] == "video" # content_type parameter
|
||||
assert captured_params['content_type'] == "video"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_saves_analysis_report_as_markdown(
|
||||
self, mock_deep_analyzer, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
):
|
||||
"""Test 3: AnalyzeProcessor saves analysis report as Markdown"""
|
||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
||||
@@ -297,13 +307,19 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock()
|
||||
with patch("kb_pipeline.processors.analyze.DeepAnalyzer") as MockDeepAnalyzer:
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_analyzer_instance.analyze = MagicMock(return_value=mock_deep_analyzer)
|
||||
MockDeepAnalyzer.return_value = mock_analyzer_instance
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
result = await processor.process(input_data)
|
||||
async def mock_run_analysis(metadata, content, content_type):
|
||||
return mock_analysis_result
|
||||
|
||||
def mock_write_report(output_path, metadata, result, content_type):
|
||||
# Write a simple markdown file with frontmatter
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write("---\ntitle: Test\n---\n\n## Summary\n\nTest analysis content")
|
||||
|
||||
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
|
||||
with patch.object(processor, '_write_analysis_report', side_effect=mock_write_report):
|
||||
result = await processor.process(input_data)
|
||||
|
||||
assert result.success is True
|
||||
assert result.analysis_file == output_file
|
||||
@@ -319,7 +335,7 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_records_state_on_success(
|
||||
self, mock_deep_analyzer, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||
):
|
||||
"""Test 4: AnalyzeProcessor records state on success"""
|
||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
||||
@@ -343,13 +359,14 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock()
|
||||
with patch("kb_pipeline.processors.analyze.DeepAnalyzer") as MockDeepAnalyzer:
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_analyzer_instance.analyze = MagicMock(return_value=mock_deep_analyzer)
|
||||
MockDeepAnalyzer.return_value = mock_analyzer_instance
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
result = await processor.process(input_data)
|
||||
async def mock_run_analysis(metadata, content, content_type):
|
||||
return mock_analysis_result
|
||||
|
||||
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
|
||||
with patch.object(processor, '_write_analysis_report'):
|
||||
result = await processor.process(input_data)
|
||||
|
||||
assert result.success is True
|
||||
|
||||
@@ -385,14 +402,12 @@ class TestAnalyzeProcessorIntegration:
|
||||
|
||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock()
|
||||
with patch("kb_pipeline.processors.analyze.DeepAnalyzer") as MockDeepAnalyzer:
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_analyzer_instance.analyze = MagicMock(
|
||||
side_effect=ValueError("AI API error")
|
||||
)
|
||||
MockDeepAnalyzer.return_value = mock_analyzer_instance
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
|
||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||
async def mock_run_analysis_error(metadata, content, content_type):
|
||||
raise ValueError("AI API error")
|
||||
|
||||
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis_error):
|
||||
result = await processor.process(input_data)
|
||||
|
||||
assert result.success is False
|
||||
|
||||
Reference in New Issue
Block a user