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:
2026-03-13 23:32:31 +08:00
parent 51644997e4
commit 0b7d8707bf
2 changed files with 324 additions and 57 deletions
+72 -57
View File
@@ -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