docs(04-analysis-orchestration): create phase plans
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
---
|
||||
phase: 04-analysis-orchestration
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- kb_pipeline/processors/base.py
|
||||
- kb_pipeline/processors/parse.py
|
||||
- kb_pipeline/processors/__init__.py
|
||||
- kb_pipeline/schemas.py
|
||||
- tests/test_processors/test_base.py
|
||||
- tests/test_processors/test_parse.py
|
||||
autonomous: true
|
||||
requirements: [MNT-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "BaseProcessor defines unified interface for all processors"
|
||||
- "ParseProcessor correctly parses Bilibili and Zhihu URLs"
|
||||
- "All processors inherit from BaseProcessor with consistent process() method"
|
||||
artifacts:
|
||||
- path: "kb_pipeline/processors/base.py"
|
||||
provides: "BaseProcessor abstract class"
|
||||
contains: "class BaseProcessor"
|
||||
min_lines: 80
|
||||
- path: "kb_pipeline/processors/parse.py"
|
||||
provides: "ParseProcessor implementation"
|
||||
contains: "class ParseProcessor"
|
||||
min_lines: 60
|
||||
- path: "kb_pipeline/schemas.py"
|
||||
provides: "ParseInput/ParseOutput schemas"
|
||||
contains: "class ParseInput"
|
||||
key_links:
|
||||
- from: "kb_pipeline/processors/parse.py"
|
||||
to: "kb_pipeline/link_parser.py"
|
||||
via: "LinkParser.parse()"
|
||||
pattern: "LinkParser\\(\\)\\.parse"
|
||||
- from: "kb_pipeline/processors/base.py"
|
||||
to: "kb_pipeline/state.py"
|
||||
via: "WorkflowStateManager integration"
|
||||
pattern: "state_manager\\."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create unified BaseProcessor interface and ParseProcessor to establish the foundation for the pipeline architecture.
|
||||
|
||||
Purpose: Provide consistent processor interface that all steps (parse, download, asr, analyze) will follow, enabling unified state management and error handling.
|
||||
Output: BaseProcessor abstract class, ParseProcessor implementation, and related schemas.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-analysis-orchestration/04-CONTEXT.md
|
||||
@.planning/phases/04-analysis-orchestration/04-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing codebase. Executor should use these directly. -->
|
||||
|
||||
From kb_pipeline/schemas.py:
|
||||
```python
|
||||
class StepStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
class StepInput(BaseModel):
|
||||
content_id: str = Field(..., min_length=1)
|
||||
platform: Literal["bilibili", "zhihu"]
|
||||
|
||||
class StepOutput(BaseModel):
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
completed_at: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
class WorkflowState(BaseModel):
|
||||
def is_step_completed(self, step_name: str) -> bool: ...
|
||||
def mark_step_started(self, step_name: str) -> None: ...
|
||||
def mark_step_completed(self, step_name: str, output_summary: Optional[str] = None) -> None: ...
|
||||
def mark_step_failed(self, step_name: str, error_message: str) -> None: ...
|
||||
```
|
||||
|
||||
From kb_pipeline/state.py:
|
||||
```python
|
||||
class WorkflowStateManager:
|
||||
STATE_FILENAME = "workflow_state.json"
|
||||
def __init__(self, content_dir: Path): ...
|
||||
def load(self) -> Optional[WorkflowState]: ...
|
||||
def save(self, state: WorkflowState) -> None: ...
|
||||
def get_or_create(self, content_id: str, platform: str, source_url: str) -> WorkflowState: ...
|
||||
```
|
||||
|
||||
From kb_pipeline/link_parser.py:
|
||||
```python
|
||||
@dataclass
|
||||
class ParsedLink:
|
||||
platform: str
|
||||
content_type: str
|
||||
content_id: str
|
||||
original_url: str
|
||||
resolved_url: str = ""
|
||||
|
||||
class LinkParser:
|
||||
def parse(self, url: str) -> Optional[ParsedLink]: ...
|
||||
```
|
||||
|
||||
From kb_pipeline/processors/asr.py (existing pattern reference):
|
||||
```python
|
||||
class ASRProcessor:
|
||||
STEP_NAME = "asr"
|
||||
def __init__(self, state_manager: Optional[WorkflowStateManager] = None):
|
||||
self.settings = get_settings()
|
||||
self.state_manager = state_manager
|
||||
async def process(self, input_data: ASRInput) -> ASROutput: ...
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define BaseProcessor abstract class</name>
|
||||
<files>kb_pipeline/processors/base.py, tests/test_processors/test_base.py</files>
|
||||
<behavior>
|
||||
- Test 1: BaseProcessor cannot be instantiated directly (abstract)
|
||||
- Test 2: Subclass must implement process() method
|
||||
- Test 3: BaseProcessor provides state_manager attribute
|
||||
- Test 4: BaseProcessor provides STEP_NAME class attribute
|
||||
</behavior>
|
||||
<action>
|
||||
Create `kb_pipeline/processors/base.py` with BaseProcessor abstract class:
|
||||
|
||||
1. Define abstract base class with:
|
||||
- STEP_NAME: str class attribute (must be overridden by subclass)
|
||||
- __init__(self, state_manager: Optional[WorkflowStateManager] = None)
|
||||
- settings: KBSettings from get_settings()
|
||||
- @abstractmethod process(self, input_data: StepInput) -> StepOutput
|
||||
|
||||
2. Use ABC (Abstract Base Class) from abc module
|
||||
3. Import types from typing: Optional, TypeVar, Generic
|
||||
4. Keep it minimal - no complex template methods, just the interface contract
|
||||
|
||||
Reference existing ASRProcessor pattern but extract the common interface.
|
||||
Do NOT add retry logic - that belongs in concrete implementations.
|
||||
Do NOT add complex inheritance hierarchies - keep it flat and simple.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_base.py -v -x</automated>
|
||||
</verify>
|
||||
<done>BaseProcessor abstract class exists with process() method, cannot be instantiated directly</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add ParseInput and ParseOutput schemas</name>
|
||||
<files>kb_pipeline/schemas.py, tests/test_pipeline/test_schemas.py</files>
|
||||
<behavior>
|
||||
- Test 1: ParseInput validates url field (required, min_length=1)
|
||||
- Test 2: ParseOutput contains platform, content_type, content_id, resolved_url
|
||||
- Test 3: ParseOutput inherits success and error_message from StepOutput
|
||||
</behavior>
|
||||
<action>
|
||||
Add ParseInput and ParseOutput schemas to `kb_pipeline/schemas.py`:
|
||||
|
||||
```python
|
||||
class ParseInput(StepInput):
|
||||
"""Parse step input - URL to parse"""
|
||||
url: str = Field(..., min_length=1, description="URL to parse")
|
||||
# Override content_id and platform as optional for parse step
|
||||
content_id: str = Field(default="", description="Will be populated after parsing")
|
||||
platform: str = Field(default="", description="Will be populated after parsing")
|
||||
|
||||
class ParseOutput(StepOutput):
|
||||
"""Parse step output"""
|
||||
platform: str = Field(..., description="Detected platform")
|
||||
content_type: str = Field(..., description="Content type (video, article, etc.)")
|
||||
content_id: str = Field(..., description="Extracted content ID")
|
||||
resolved_url: str = Field(..., description="Resolved URL after short link expansion")
|
||||
original_url: str = Field(..., description="Original URL provided by user")
|
||||
```
|
||||
|
||||
Place after ASROutput schema (around line 236).
|
||||
Add tests to existing test_schemas.py file.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_pipeline/test_schemas.py -v -x -k "Parse"</automated>
|
||||
</verify>
|
||||
<done>ParseInput and ParseOutput schemas exist and validate correctly</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Implement ParseProcessor</name>
|
||||
<files>kb_pipeline/processors/parse.py, tests/test_processors/test_parse.py</files>
|
||||
<behavior>
|
||||
- Test 1: ParseProcessor parses Bilibili video URL correctly
|
||||
- Test 2: ParseProcessor parses Zhihu answer URL correctly
|
||||
- Test 3: ParseProcessor returns ParseOutput with success=False for unknown URLs
|
||||
- Test 4: ParseProcessor records state when state_manager is provided
|
||||
</behavior>
|
||||
<action>
|
||||
Create `kb_pipeline/processors/parse.py`:
|
||||
|
||||
1. Import BaseProcessor from .base, LinkParser from kb_pipeline.link_parser
|
||||
2. Import ParseInput, ParseOutput from kb_pipeline.schemas
|
||||
3. Implement ParseProcessor(BaseProcessor):
|
||||
- STEP_NAME = "parse"
|
||||
- async def process(self, input_data: ParseInput) -> ParseOutput:
|
||||
a. Create LinkParser instance
|
||||
b. Call parser.parse(input_data.url)
|
||||
c. If parsed is None, return ParseOutput(success=False, error_message="Unsupported URL")
|
||||
d. If parsed, return ParseOutput with parsed fields
|
||||
e. Record state on success (if state_manager exists)
|
||||
|
||||
4. Keep implementation simple - just wrap LinkParser
|
||||
5. Handle both success and failure cases with proper state recording
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_parse.py -v -x</automated>
|
||||
</verify>
|
||||
<done>ParseProcessor correctly parses URLs and returns structured ParseOutput</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Update processors __init__.py</name>
|
||||
<files>kb_pipeline/processors/__init__.py</files>
|
||||
<action>
|
||||
Update `kb_pipeline/processors/__init__.py` to export new classes:
|
||||
|
||||
```python
|
||||
from .base import BaseProcessor
|
||||
from .parse import ParseProcessor
|
||||
from .asr import ASRProcessor
|
||||
|
||||
__all__ = ["BaseProcessor", "ParseProcessor", "ASRProcessor"]
|
||||
```
|
||||
|
||||
Keep existing ASRProcessor export and add new ones.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -c "from kb_pipeline.processors import BaseProcessor, ParseProcessor, ASRProcessor; print('OK')"</automated>
|
||||
</verify>
|
||||
<done>All processors can be imported from kb_pipeline.processors</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All tests pass: `pytest tests/test_processors/test_base.py tests/test_processors/test_parse.py -v`
|
||||
- BaseProcessor is abstract and cannot be instantiated
|
||||
- ParseProcessor correctly wraps LinkParser
|
||||
- State recording works when state_manager is provided
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- BaseProcessor abstract class defines unified interface
|
||||
- ParseProcessor inherits from BaseProcessor
|
||||
- ParseInput/ParseOutput schemas exist and validate
|
||||
- All tests pass
|
||||
- Code follows existing patterns from ASRProcessor
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-analysis-orchestration/04-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,256 @@
|
||||
---
|
||||
phase: 04-analysis-orchestration
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [04-01]
|
||||
files_modified:
|
||||
- kb_pipeline/processors/download.py
|
||||
- kb_pipeline/schemas.py
|
||||
- tests/test_processors/test_download.py
|
||||
autonomous: true
|
||||
requirements: [MNT-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "DownloadProcessor fetches metadata from platforms"
|
||||
- "DownloadProcessor downloads content (video/audio or text)"
|
||||
- "State is recorded after successful download"
|
||||
artifacts:
|
||||
- path: "kb_pipeline/processors/download.py"
|
||||
provides: "DownloadProcessor implementation"
|
||||
contains: "class DownloadProcessor"
|
||||
min_lines: 100
|
||||
- path: "kb_pipeline/schemas.py"
|
||||
provides: "DownloadInput/DownloadOutput schemas"
|
||||
contains: "class DownloadInput"
|
||||
key_links:
|
||||
- from: "kb_pipeline/processors/download.py"
|
||||
to: "kb_pipeline/platforms/bilibili.py"
|
||||
via: "BilibiliAdapter"
|
||||
pattern: "BilibiliAdapter"
|
||||
- from: "kb_pipeline/processors/download.py"
|
||||
to: "kb_pipeline/platforms/zhihu.py"
|
||||
via: "ZhihuAdapter"
|
||||
pattern: "ZhihuAdapter"
|
||||
- from: "kb_pipeline/processors/download.py"
|
||||
to: "kb_pipeline/state.py"
|
||||
via: "WorkflowStateManager"
|
||||
pattern: "state_manager\\."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create DownloadProcessor to handle metadata extraction and content downloading for all platforms.
|
||||
|
||||
Purpose: Unify metadata and content fetching into a single processor that works with PlatformAdapter implementations.
|
||||
Output: DownloadProcessor implementation with Bilibili and Zhihu adapter integration.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-analysis-orchestration/04-CONTEXT.md
|
||||
@.planning/phases/04-analysis-orchestration/04-RESEARCH.md
|
||||
@.planning/phases/04-analysis-orchestration/04-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing code. Executor should use these directly. -->
|
||||
|
||||
From kb_pipeline/schemas.py:
|
||||
```python
|
||||
class StepInput(BaseModel):
|
||||
content_id: str
|
||||
platform: Literal["bilibili", "zhihu"]
|
||||
|
||||
class StepOutput(BaseModel):
|
||||
success: bool
|
||||
error_message: Optional[str]
|
||||
completed_at: datetime
|
||||
```
|
||||
|
||||
From kb_pipeline/processors/base.py (from 04-01):
|
||||
```python
|
||||
class BaseProcessor(ABC):
|
||||
STEP_NAME: str
|
||||
async def process(self, input_data: StepInput) -> StepOutput
|
||||
async def _do_process(self, input_data: StepInput) -> StepOutput
|
||||
```
|
||||
|
||||
From kb_pipeline/platforms/base.py:
|
||||
```python
|
||||
class PlatformAdapter(ABC):
|
||||
@abstractmethod
|
||||
async def fetch_metadata(self, content_id: str, content_type: str) -> Dict[str, Any]
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_content(self, content_id: str, content_type: str) -> str
|
||||
|
||||
@abstractmethod
|
||||
def get_output_filename(self, content_id: str, content_type: str, suffix: str) -> str
|
||||
```
|
||||
|
||||
From kb_pipeline/platforms/bilibili.py:
|
||||
```python
|
||||
class BilibiliAdapter(PlatformAdapter):
|
||||
# Already implemented with fetch_metadata and fetch_content
|
||||
```
|
||||
|
||||
From kb_pipeline/platforms/zhihu.py:
|
||||
```python
|
||||
class ZhihuAdapter(PlatformAdapter):
|
||||
# Already implemented with fetch_metadata and fetch_content
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define Download Schemas</name>
|
||||
<files>kb_pipeline/schemas.py, tests/test_processors/test_download.py</files>
|
||||
<behavior>
|
||||
- Test 1: DownloadInput validates required fields
|
||||
- Test 2: DownloadOutput validates required fields
|
||||
- Test 3: DownloadOutput handles both video and article content types
|
||||
</behavior>
|
||||
<action>
|
||||
Add DownloadInput and DownloadOutput schemas to kb_pipeline/schemas.py.
|
||||
|
||||
**DownloadInput fields (extends StepInput):**
|
||||
- content_dir: str - Directory to save downloaded content
|
||||
- content_type: str - Type of content (video, article, answer, zhuanlan)
|
||||
|
||||
**DownloadOutput fields (extends StepOutput):**
|
||||
- metadata_file: str - Path to metadata JSON
|
||||
- content_file: str - Path to content file (video/audio or text)
|
||||
- title: str - Content title
|
||||
- author: str - Content author
|
||||
- duration: Optional[int] - Duration for video content
|
||||
|
||||
Follow StepInput/StepOutput pattern from Phase 1.
|
||||
Create test file with schema validation tests.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_download.py -v -x</automated>
|
||||
</verify>
|
||||
<done>DownloadInput/DownloadOutput schemas defined and validated with tests</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create DownloadProcessor with metadata fetching</name>
|
||||
<files>kb_pipeline/processors/download.py, tests/test_processors/test_download.py</files>
|
||||
<behavior>
|
||||
- Test 1: DownloadProcessor fetches Bilibili video metadata correctly
|
||||
- Test 2: DownloadProcessor fetches Zhihu answer metadata correctly
|
||||
- Test 3: DownloadProcessor records state on success
|
||||
- Test 4: DownloadProcessor records state on failure
|
||||
</behavior>
|
||||
<action>
|
||||
Create `kb_pipeline/processors/download.py` with DownloadProcessor class.
|
||||
|
||||
**Implementation:**
|
||||
1. Import BaseProcessor, PlatformAdapter classes
|
||||
2. Import BilibiliAdapter, ZhihuAdapter from platforms
|
||||
3. Create DownloadProcessor(BaseProcessor):
|
||||
- STEP_NAME = "download"
|
||||
- __init__ with state_manager, settings
|
||||
- process() method that:
|
||||
a. Checks if step already completed (skip if done)
|
||||
b. Gets appropriate PlatformAdapter based on platform
|
||||
c. Calls adapter.fetch_metadata()
|
||||
d. Saves metadata to content_dir
|
||||
e. Records state on success/failure
|
||||
|
||||
**State Recording:**
|
||||
- On success: mark_step_completed("download", f"title={title}, type={content_type}")
|
||||
- On failure: mark_step_failed("download", error_message)
|
||||
|
||||
Create comprehensive tests mocking adapters and state_manager.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_download.py -v -x</automated>
|
||||
</verify>
|
||||
<done>DownloadProcessor fetches metadata with state recording</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Add content downloading to DownloadProcessor</name>
|
||||
<files>kb_pipeline/processors/download.py, tests/test_processors/test_download.py</files>
|
||||
<behavior>
|
||||
- Test 1: DownloadProcessor downloads Bilibili article content
|
||||
- Test 2: DownloadProcessor skips video download (handled by ASR)
|
||||
- Test 3: DownloadProcessor creates correct output file paths
|
||||
</behavior>
|
||||
<action>
|
||||
Extend DownloadProcessor to download content.
|
||||
|
||||
**For articles/text content:**
|
||||
- Call adapter.fetch_content() to get text
|
||||
- Save content to content_dir as Markdown file
|
||||
- Update DownloadOutput with content_file path
|
||||
|
||||
**For video content:**
|
||||
- Skip actual video download (handled by ASR in Phase 3)
|
||||
- Set content_file to empty or indicate video needs ASR
|
||||
|
||||
**File naming:**
|
||||
- Metadata: `{platform}_{content_id}_metadata.json`
|
||||
- Content: `{platform}_{content_id}_content.md` (for articles)
|
||||
|
||||
Update tests to verify content downloading behavior.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_download.py -v -x</automated>
|
||||
</verify>
|
||||
<done>DownloadProcessor downloads content for text-based content types</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Update processors __init__.py</name>
|
||||
<files>kb_pipeline/processors/__init__.py</files>
|
||||
<action>
|
||||
Update `kb_pipeline/processors/__init__.py` to export DownloadProcessor.
|
||||
|
||||
```python
|
||||
from .base import BaseProcessor
|
||||
from .parse import ParseProcessor
|
||||
from .download import DownloadProcessor
|
||||
from .asr import ASRProcessor
|
||||
|
||||
__all__ = ["BaseProcessor", "ParseProcessor", "DownloadProcessor", "ASRProcessor"]
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -c "from kb_pipeline.processors import DownloadProcessor; print('OK')"</automated>
|
||||
</verify>
|
||||
<done>DownloadProcessor exported from processors module</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All tests pass: `pytest tests/test_processors/test_download.py -v`
|
||||
- DownloadProcessor correctly uses PlatformAdapter implementations
|
||||
- State recording works for both success and failure cases
|
||||
- Content files are saved to correct locations
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- DownloadProcessor inherits from BaseProcessor
|
||||
- DownloadInput/DownloadOutput schemas exist and validate
|
||||
- Metadata is fetched and saved correctly
|
||||
- Text content is downloaded and saved
|
||||
- Video content is marked for ASR processing
|
||||
- All tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-analysis-orchestration/04-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
phase: 04-analysis-orchestration
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [04-01, 04-02]
|
||||
files_modified:
|
||||
- kb_pipeline/processors/analyze.py
|
||||
- kb_pipeline/schemas.py
|
||||
- tests/test_processors/test_analyze.py
|
||||
autonomous: true
|
||||
requirements: [MNT-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "AnalyzeProcessor generates AI analysis reports"
|
||||
- "Analysis reports are saved as Markdown files"
|
||||
- "AnalyzeProcessor wraps existing DeepAnalyzer"
|
||||
artifacts:
|
||||
- path: "kb_pipeline/processors/analyze.py"
|
||||
provides: "AnalyzeProcessor implementation"
|
||||
contains: "class AnalyzeProcessor"
|
||||
min_lines: 100
|
||||
- path: "kb_pipeline/schemas.py"
|
||||
provides: "AnalyzeInput/AnalyzeOutput schemas"
|
||||
contains: "class AnalyzeInput"
|
||||
- path: "tests/test_processors/test_analyze.py"
|
||||
provides: "Comprehensive test suite"
|
||||
contains: "test_analyze_processor"
|
||||
key_links:
|
||||
- from: "kb_pipeline/processors/analyze.py"
|
||||
to: "kb-deep-analyzer/deep_analyzer.py"
|
||||
via: "DeepAnalyzer class import"
|
||||
pattern: "from.*deep_analyzer.*import"
|
||||
- from: "kb_pipeline/processors/analyze.py"
|
||||
to: "kb_pipeline/state.py"
|
||||
via: "WorkflowStateManager"
|
||||
pattern: "state_manager\\."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create AnalyzeProcessor to wrap existing DeepAnalyzer and integrate it into the unified processor pipeline.
|
||||
|
||||
Purpose: Provide AI-powered content analysis that generates structured reports as Markdown files.
|
||||
Output: AnalyzeProcessor implementation with DeepAnalyzer integration.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-analysis-orchestration/04-CONTEXT.md
|
||||
@.planning/phases/04-analysis-orchestration/04-RESEARCH.md
|
||||
@.planning/phases/04-analysis-orchestration/04-01-SUMMARY.md
|
||||
@.planning/phases/04-analysis-orchestration/04-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types from existing code. Executor should use these directly. -->
|
||||
|
||||
From kb-deep-analyzer/deep_analyzer.py:
|
||||
```python
|
||||
class SimpleAIClient:
|
||||
def __init__(self, api_key: str = None, provider: str = None, model: str = None, base_url: str = None):
|
||||
...
|
||||
def generate(self, prompt: str, max_tokens: int = 16000) -> str:
|
||||
...
|
||||
|
||||
class DeepAnalyzer:
|
||||
def __init__(self, ai_client: SimpleAIClient = None):
|
||||
...
|
||||
def analyze(self, metadata: Dict[str, Any], content: Dict[str, Any], content_type: str = "video") -> AnalysisResult:
|
||||
...
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
summary: str = ""
|
||||
main_topics: List[str] = field(default_factory=list)
|
||||
key_insights: List[str] = field(default_factory=list)
|
||||
recommended_tags: List[str] = field(default_factory=list)
|
||||
full_analysis: str = ""
|
||||
original_text: str = ""
|
||||
|
||||
def generate_frontmatter(metadata: Dict, result: AnalysisResult, content_type: str = "video") -> str:
|
||||
...
|
||||
```
|
||||
|
||||
From kb_pipeline/schemas.py (after 04-01 and 04-02):
|
||||
```python
|
||||
class StepInput(BaseModel):
|
||||
content_id: str
|
||||
platform: Literal["bilibili", "zhihu"]
|
||||
|
||||
class StepOutput(BaseModel):
|
||||
success: bool
|
||||
error_message: Optional[str]
|
||||
completed_at: datetime
|
||||
```
|
||||
|
||||
From kb_pipeline/processors/base.py (from 04-01):
|
||||
```python
|
||||
class BaseProcessor(ABC):
|
||||
STEP_NAME: str
|
||||
async def process(self, input_data: StepInput) -> StepOutput
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define Analyze Schemas</name>
|
||||
<files>kb_pipeline/schemas.py, tests/test_processors/test_analyze.py</files>
|
||||
<behavior>
|
||||
- Test 1: AnalyzeInput validates required fields
|
||||
- Test 2: AnalyzeOutput validates required fields
|
||||
- Test 3: AnalyzeOutput handles both video and article content types
|
||||
</behavior>
|
||||
<action>
|
||||
Add AnalyzeInput and AnalyzeOutput schemas to kb_pipeline/schemas.py.
|
||||
|
||||
**AnalyzeInput fields (extends StepInput):**
|
||||
- metadata_file: str - Path to metadata JSON file
|
||||
- content_file: str - Path to content file (subtitle JSON or article content)
|
||||
- content_type: str - Type of content (video or article)
|
||||
- output_file: str - Path to output analysis Markdown file
|
||||
|
||||
**AnalyzeOutput fields (extends StepOutput):**
|
||||
- analysis_file: str - Path to generated analysis report
|
||||
- summary: str - Content summary
|
||||
- main_topics: List[str] - Main topics extracted
|
||||
- key_insights: List[str] - Key insights
|
||||
- tags: List[str] - Recommended tags
|
||||
- stats: Dict[str, Any] - Statistics about analysis
|
||||
|
||||
Create test file with schema validation tests.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_analyze.py -v -x -k "test_schema"</automated>
|
||||
</verify>
|
||||
<done>AnalyzeInput/AnalyzeOutput schemas defined and validated</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create AnalyzeProcessor with DeepAnalyzer integration</name>
|
||||
<files>kb_pipeline/processors/analyze.py, tests/test_processors/test_analyze.py</files>
|
||||
<behavior>
|
||||
- Test 1: AnalyzeProcessor loads metadata and content from files
|
||||
- Test 2: AnalyzeProcessor calls DeepAnalyzer.analyze() with correct parameters
|
||||
- Test 3: AnalyzeProcessor saves analysis report as Markdown
|
||||
- Test 4: AnalyzeProcessor records state on success
|
||||
- Test 5: AnalyzeProcessor records state on failure
|
||||
</behavior>
|
||||
<action>
|
||||
Create `kb_pipeline/processors/analyze.py` with AnalyzeProcessor class.
|
||||
|
||||
**Implementation:**
|
||||
1. Import BaseProcessor from .base
|
||||
2. Import DeepAnalyzer, SimpleAIClient, AnalysisResult, generate_frontmatter from kb_deep_analyzer
|
||||
3. Import AnalyzeInput, AnalyzeOutput from ..schemas
|
||||
4. Create AnalyzeProcessor(BaseProcessor):
|
||||
- STEP_NAME = "analyze"
|
||||
- __init__ with state_manager, settings (initialize AI client lazily)
|
||||
- process() method that:
|
||||
a. Checks if step already completed (skip if done)
|
||||
b. Loads metadata JSON file
|
||||
c. Loads content JSON/text file
|
||||
d. Initializes SimpleAIClient with settings
|
||||
e. Creates DeepAnalyzer instance
|
||||
f. Calls analyzer.analyze(metadata, content, content_type)
|
||||
g. Generates frontmatter using generate_frontmatter()
|
||||
h. Writes analysis report as Markdown
|
||||
i. Records state on success/failure
|
||||
|
||||
**Output file format:**
|
||||
```markdown
|
||||
---
|
||||
title: "..."
|
||||
date: ...
|
||||
author: ...
|
||||
tags: [...]
|
||||
---
|
||||
|
||||
## 标题
|
||||
...
|
||||
|
||||
## 摘要
|
||||
...
|
||||
|
||||
## 主要主题
|
||||
...
|
||||
|
||||
## 关键见解
|
||||
...
|
||||
|
||||
## 时间轴 (for video only)
|
||||
...
|
||||
```
|
||||
|
||||
Create comprehensive tests with mocked DeepAnalyzer.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_processors/test_analyze.py -v -x</automated>
|
||||
</verify>
|
||||
<done>AnalyzeProcessor generates analysis reports using DeepAnalyzer</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Update processors __init__.py</name>
|
||||
<files>kb_pipeline/processors/__init__.py</files>
|
||||
<action>
|
||||
Update `kb_pipeline/processors/__init__.py` to export AnalyzeProcessor.
|
||||
|
||||
```python
|
||||
from .base import BaseProcessor
|
||||
from .parse import ParseProcessor
|
||||
from .download import DownloadProcessor
|
||||
from .asr import ASRProcessor
|
||||
from .analyze import AnalyzeProcessor
|
||||
|
||||
__all__ = ["BaseProcessor", "ParseProcessor", "DownloadProcessor", "ASRProcessor", "AnalyzeProcessor"]
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -c "from kb_pipeline.processors import AnalyzeProcessor; print('OK')"</automated>
|
||||
</verify>
|
||||
<done>AnalyzeProcessor exported from processors module</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All tests pass: `pytest tests/test_processors/test_analyze.py -v`
|
||||
- AnalyzeProcessor correctly wraps DeepAnalyzer
|
||||
- Analysis reports are saved as Markdown files
|
||||
- Frontmatter is generated correctly
|
||||
- State recording works for both success and failure cases
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- AnalyzeProcessor inherits from BaseProcessor
|
||||
- AnalyzeInput/AnalyzeOutput schemas exist and validate
|
||||
- DeepAnalyzer is integrated correctly (no modifications to existing code)
|
||||
- Analysis reports are saved with proper frontmatter
|
||||
- All tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-analysis-orchestration/04-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,658 @@
|
||||
---
|
||||
phase: 04-analysis-orchestration
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: [04-01, 04-02, 04-03]
|
||||
files_modified:
|
||||
- kb_pipeline/pipeline.py
|
||||
- kb_pipeline/processors/__init__.py
|
||||
- kb-workflow/kb_workflow.py
|
||||
- tests/test_pipeline/test_pipeline.py
|
||||
- tests/test_cli/test_process_command.py
|
||||
autonomous: true
|
||||
requirements: [MNT-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Pipeline orchestrestr executes full workflow from URL to analysis"
|
||||
- "Single command triggers entire workflow"
|
||||
- "Progress is visible through simple logging"
|
||||
- "New developers can understand code in 30 minutes"
|
||||
artifacts:
|
||||
- path: "kb_pipeline/pipeline.py"
|
||||
provides: "Pipeline engine class"
|
||||
min_lines: 100
|
||||
- path: "kb-workflow/kb_workflow.py"
|
||||
provides: "CLI entry point with process subcommand"
|
||||
min_lines: 50
|
||||
- path: "tests/test_pipeline/test_pipeline.py"
|
||||
provides: "Pipeline tests"
|
||||
min_lines: 100
|
||||
- path: "tests/test_cli/test_process_command.py"
|
||||
provides: "CLI tests"
|
||||
key_links:
|
||||
- from: "kb_pipeline/pipeline.py"
|
||||
to: "kb_pipeline/processors"
|
||||
via: "Pipeline.run()"
|
||||
pattern: "Pipeline\\("
|
||||
- from: "kb-workflow/kb_workflow.py"
|
||||
to: "kb_pipeline.processors"
|
||||
via: "import"
|
||||
pattern: "from.* import"
|
||||
- from: "kb_pipeline/processors" import ParseProcessor, DownloadProcessor, ASrProcessor, AnalyzeProcessor
|
||||
pattern: "from.* import.*"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create Pipeline engine to orchest all processors and execute the complete workflow from URL to analysis report.
|
||||
|
||||
Purpose: Enable single-command workflow execution with automatic state management and error recovery, and and new developer to easily understand and code结构.
|
||||
Output: Pipeline engine class, process subcommand, and CLI entry point.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-analysis-orchestration/04-CONTEXT.md
|
||||
@.planning/phases/04-analysis-orchestration/04-RESEARCH.md
|
||||
|
||||
@.planning/phases/04-analysis-orchestration/04-01-SUMMARY.md
|
||||
@.planning/phases/04-analysis-orchestration/04-02-SUMMARY.md
|
||||
@.planning/phases/04-analysis-orchestration/04-03-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing code. Executor should use these directly. -->
|
||||
|
||||
From kb_pipeline/processors/base.py:
|
||||
```python
|
||||
class BaseProcessor(ABC):
|
||||
STEP_NAME: str
|
||||
async def process(self, input_data: StepInput) -> StepOutput:
|
||||
```
|
||||
|
||||
From kb_pipeline/processors/parse.py:
|
||||
```python
|
||||
class ParseProcessor(BaseProcessor):
|
||||
STEP_NAME = "parse"
|
||||
async def process(self, input_data: ParseInput) -> ParseOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/download.py:
|
||||
```python
|
||||
class DownloadProcessor(BaseProcessor):
|
||||
STEP_NAME = "download"
|
||||
async def process(self, input_data: DownloadInput) -> DownloadOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/asr.py:
|
||||
```python
|
||||
class ASRProcessor:
|
||||
STEP_NAME = "asr"
|
||||
async def process(self, input_data: ASRInput) -> ASROutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/analyze.py:
|
||||
```python
|
||||
class AnalyzeProcessor(BaseProcessor):
|
||||
STEP_NAME = "analyze"
|
||||
async def process(self, input_data: AnalyInput) -> AnalyOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/schemas.py:
|
||||
```python
|
||||
class StepInput(BaseModel):
|
||||
content_id: str = platform: Literal["bilibili", "zhihu"]
|
||||
|
||||
class StepOutput(BaseModel):
|
||||
success: bool
|
||||
error_message: Optional[str]
|
||||
completed_at: datetime
|
||||
|
||||
class ParseInput(StepInput):
|
||||
url: str
|
||||
|
||||
class ParseOutput(StepOutput):
|
||||
parsed: ParsedLink
|
||||
content_dir: Path
|
||||
metadata_file: Optional[str] = None
|
||||
content_file: Optional[str] = None
|
||||
content_type: str = "video" # video needs AS
|
||||
|
||||
# article不需要
|
||||
|
||||
content_file: Optional[str] = None # 从 download步骤获取
|
||||
|
||||
content_type: str # "video" or "article"
|
||||
download_input.content_dir
|
||||
download_output(
|
||||
success=True,
|
||||
metadata_file=f"{content_dir}/{platform}_{content_id}_metadata.json",
|
||||
content_file=content_file,
|
||||
title=metadata.get('title', '未知'),
|
||||
author=metadata.get('author', '未知'),
|
||||
duration=metadata.get('duration') # seconds, )
|
||||
|
||||
return state.mark_step_completed(
|
||||
self.STEP_NAME, f"title={title}, author={author}, type={content_type}"
|
||||
)
|
||||
self.state_manager.save(state)
|
||||
return output
|
||||
|
||||
```
|
||||
|
||||
from kb_pipeline.state import WorkflowStateManager
|
||||
from kb_pipeline.config import get_settings
|
||||
from kb_pipeline.errors import PipelineError
|
||||
|
||||
from kb_pipeline.processors.parse import ParseProcessor
|
||||
from kb_pipeline.processors.download import DownloadProcessor
|
||||
from kb_pipeline.processors.asr import ASRProcessor
|
||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
||||
|
||||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PipelineConfig:
|
||||
"""Pipeline configuration"""
|
||||
steps: List[str] = Field(default_factory=lambda: ["parse", "download", "asr", "analyze"])
|
||||
output_dir: Path
|
||||
skip_completed: bool = Field(default=True, description="Skip completed steps")
|
||||
max_retries: int = Field(default=3, description="Max retries for each step")
|
||||
|
||||
|
||||
class Pipeline:
|
||||
"""Pipeline engine - orchestrates multiple processors
|
||||
|
||||
Orchestrates the workflow:
|
||||
1. Parse URL to extract platform and content ID
|
||||
2. Run processors in order (parse, download, asr, analyze)
|
||||
3. Handle errors and retry on failures
|
||||
4. Return results
|
||||
|
||||
Key decisions:
|
||||
- Sequential execution (not parallel - simpler to easier to debug
|
||||
- State persistence after each step (via WorkflowStateManager)
|
||||
- Progress logging to simple, clear
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
processors: List[BaseProcessor],
|
||||
config: Optional[PipelineConfig] = None,
|
||||
output_dir: Optional[Path] = None,
|
||||
skip_completed: bool = False
|
||||
):
|
||||
self.processors = processors
|
||||
self.config = config or PipelineConfig.default()
|
||||
self.output_dir = output_dir
|
||||
|
||||
self.skip_completed = skip_completed
|
||||
|
||||
# Validate processors
|
||||
for processor in self.processors:
|
||||
if processor.STEP_NAME not in self.config.steps:
|
||||
raise ValueError(f"Unknown processor: {processor.STEP_NAME}")
|
||||
|
||||
async def run(self, url: str) -> Dict[str, Any]:
|
||||
"""Execute full workflow
|
||||
|
||||
Args:
|
||||
url: URL to process
|
||||
|
||||
Returns:
|
||||
Dict containing all step results
|
||||
"""
|
||||
results: Dict[str, Any] = {}
|
||||
current_step = 0
|
||||
total_steps = len(self.config.steps)
|
||||
|
||||
state: Optional[WorkflowState] = None
|
||||
|
||||
try:
|
||||
for step_name in self.config.steps:
|
||||
current_step += 1
|
||||
logger.info(f"[Step {current_step}/{total_steps}] {step_name}...")
|
||||
|
||||
|
||||
# Get processor
|
||||
processor = self.processors.get(step_name)
|
||||
if not processor:
|
||||
raise ValueError(f"Processor not found: {step_name}")
|
||||
|
||||
# Get content directory from parsed URL
|
||||
parsed = await self._parse_url(url)
|
||||
if not parsed:
|
||||
results["error"] = "Failed to parse URL"
|
||||
return results
|
||||
|
||||
# Get or create content directory
|
||||
content_dir = self.output_dir / f"{parsed.platform}_{parsed.content_id}"
|
||||
content_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Initialize state manager for this content
|
||||
state_manager = WorkflowStateManager(content_dir)
|
||||
|
||||
# Run each step
|
||||
previous_result = None
|
||||
for step_name in self.config.steps:
|
||||
# Check if already completed
|
||||
if state and state.is_step_completed(step_name):
|
||||
current_step += 1
|
||||
logger.info(f"[Step {current_step}/{total_steps}] {step_name} - skipping (already completed)")
|
||||
results[step_name] = {"status": "skipped", "message": "Already completed"}
|
||||
continue
|
||||
|
||||
# Create step input
|
||||
input_data = self._create_step_input(
|
||||
step_name=step_name,
|
||||
parsed=parsed,
|
||||
state=state,
|
||||
previous_result=previous_result
|
||||
)
|
||||
|
||||
# Run processor
|
||||
try:
|
||||
output = await processor.process(input_data)
|
||||
results[step_name] = {
|
||||
"status": "success" if output.success else "failed",
|
||||
"output": output.model_dump() if output.success else None,
|
||||
"error": output.error_message
|
||||
}
|
||||
previous_result = output
|
||||
except Exception as e:
|
||||
results[step_name] = {
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
}
|
||||
logger.error(f"[Step {current_step}/{total_steps}] {step_name} - failed: {e}")
|
||||
# Don't stop pipeline on failure
|
||||
raise
|
||||
|
||||
return results
|
||||
|
||||
def _parse_url(self, url: str) -> Optional[ParsedLink]:
|
||||
"""Parse URL using LinkParser"""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
parser = LinkParser()
|
||||
parsed = parser.parse(url)
|
||||
if not parsed:
|
||||
logger.warning(f"Failed to parse URL: {url}")
|
||||
return None
|
||||
|
||||
return ParsedLink
|
||||
|
||||
def _create_step_input(
|
||||
self,
|
||||
step_name: str,
|
||||
parsed: ParsedLink,
|
||||
state: Optional[WorkflowState],
|
||||
previous_result: Optional[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
"""Create step input based on step name and previous result"""
|
||||
step_map = {
|
||||
"parse": ParseProcessor,
|
||||
"download": DownloadProcessor,
|
||||
"asr": ASRProcessor,
|
||||
"analyze": AnalyzeProcessor,
|
||||
}
|
||||
|
||||
# Get processor instance
|
||||
processor = self.processors[step_name]
|
||||
if not processor:
|
||||
raise ValueError(f"Unknown processor: {step_name}")
|
||||
|
||||
return step_map
|
||||
|
||||
async def _run_step(
|
||||
self,
|
||||
step_name: str,
|
||||
input_data: Dict[str, Any],
|
||||
state: Optional[WorkflowState],
|
||||
previous_result: Optional[Dict[str, Any]]
|
||||
) -> StepOutput:
|
||||
"""Run a single step"""
|
||||
processor = self.processors[step_name]
|
||||
if not processor:
|
||||
raise ValueError(f"No processor for {step_name}")
|
||||
|
||||
# Check if already completed
|
||||
if state and state.is_step_completed(step_name):
|
||||
logger.info(f"Step {step_name} already completed, skipping")
|
||||
return self._create_skipped_output()
|
||||
|
||||
|
||||
try:
|
||||
# Run the
|
||||
if previous_result and previous_result.get(step_name):
|
||||
input_data = previous_result[step_name]
|
||||
else:
|
||||
raise ValueError(f"No previous result for {step_name}")
|
||||
|
||||
return await processor.process(input_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Step {step_name} failed: {e}")
|
||||
# Record failure
|
||||
if state:
|
||||
state.mark_step_failed(step_name, str(e))
|
||||
self.state_manager.save(state)
|
||||
raise
|
||||
|
||||
# Update previous result
|
||||
previous_result[step_name] = output.model_dump()
|
||||
|
||||
return results
|
||||
|
||||
def get_output_dir(self) -> Path:
|
||||
return Path(self.output_dir) / Path(self.config.output_dir)
|
||||
|
||||
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define Pipeline Schemas</name>
|
||||
<files>kb_pipeline/schemas.py, tests/test_pipeline/test_pipeline.py</files>
|
||||
<behavior>
|
||||
- Test 1: PipelineInput validates required fields
|
||||
- Test 2: PipelineOutput validates required fields
|
||||
- Test 3: PipelineConfig default steps are correct
|
||||
</behavior>
|
||||
<action>
|
||||
Add PipelineInput, PipelineOutput, and PipelineConfig to kb_pipeline/schemas.py.
|
||||
|
||||
**PipelineInput fields (extends nothing - all fields inherited from StepInput):**
|
||||
- content_dir: str - Directory to save downloaded content
|
||||
|
||||
- content_type: str - Type of content (video, article, answer, zhuanlan)
|
||||
|
||||
- skip_completed: bool - Whether to skip already completed steps (default True, content_type not to ["video", "article", "answer", "zhuanlan", "download", "asr", "analyze"]
|
||||
|
||||
- steps: List[str] = Field(...)
|
||||
|
||||
output_dir: Path
|
||||
skip_completed: bool = True
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_pipeline/test_pipeline.py -v -x -k"test_schema" in</automated>
|
||||
</verify>
|
||||
<done>PipelineInput/Output schemas defined and validated</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create Pipeline class with processor initialization</name>
|
||||
<files>kb_pipeline/pipeline.py, tests/test_pipeline/test_pipeline.py</files>
|
||||
<behavior>
|
||||
- Test 1: Pipeline initializes with all processors
|
||||
- Test 2: Pipeline.parse_url creates correct content directory
|
||||
- Test 3: Pipeline.run() executes all steps in correct order
|
||||
- Test 4: Pipeline.run() handles step skipping correctly
|
||||
- Test 5: Pipeline.run() handles errors and continues to next step
|
||||
- Test 6: Pipeline.run() records state for each step
|
||||
</behavior>
|
||||
<action>
|
||||
Create `kb_pipeline/pipeline.py` with Pipeline class.
|
||||
|
||||
**Implementation:**
|
||||
1. Initialize with processors list (ParseProcessor, DownloadProcessor, ASRProcessor, AnalyzeProcessor)
|
||||
2. Set config with PipelineConfig.default()
|
||||
3. Set output_dir
|
||||
4. Initialize state_manager (WorkflowStateManager)
|
||||
5. Implement run() method that:
|
||||
- Creates content directory
|
||||
- Runs steps in order (parse, download, asr, analyze)
|
||||
- Returns results dict
|
||||
|
||||
6. Add helper methods:
|
||||
- _parse_url(url) -> ParsedLink
|
||||
- _get_content_dir(parsed)
|
||||
- _run_step(step_name, parsed, state, previous_result)
|
||||
- _record_state
|
||||
|
||||
7. Add error handling with try/except
|
||||
- logger.error for error message
|
||||
if self.state_manager:
|
||||
state.mark_step_failed(step_name, str(e))
|
||||
self.state_manager.save(state)
|
||||
raise
|
||||
|
||||
def _parse_url(self, url: str) -> Optional[ParsedLink]:
|
||||
"""Parse URL using LinkParser"""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
parser = LinkParser()
|
||||
parsed = parser.parse(url)
|
||||
if not parsed:
|
||||
return None
|
||||
|
||||
return Parsed
|
||||
|
||||
def _get_content_dir(self, parsed: ParsedLink) -> Path:
|
||||
"""Get content directory from parsed URL"""
|
||||
content_dir = self.output_dir / f"{parsed.platform}_{parsed.content_id}"
|
||||
content_dir.mkdir(parents=True, exist_ok=True)
|
||||
return content_dir
|
||||
|
||||
def _create_step_input(
|
||||
self,
|
||||
step_name: str,
|
||||
parsed: ParsedLink,
|
||||
state: Optional[WorkflowState],
|
||||
previous_result: Optional[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create step input based on step name and previous result"""
|
||||
step_map = {
|
||||
"parse": ParseProcessor,
|
||||
"download": DownloadProcessor,
|
||||
"asr": ASRProcessor,
|
||||
"analyze": AnalyzeProcessor
|
||||
}
|
||||
|
||||
# Get processor instance
|
||||
processor = self.processors[step_name]
|
||||
if not processor:
|
||||
raise ValueError(f"Unknown processor: {step_name}")
|
||||
|
||||
return step_map
|
||||
|
||||
async def _run_step(
|
||||
self,
|
||||
step_name: str,
|
||||
input_data: Dict[str, Any],
|
||||
state: Optional[WorkflowState],
|
||||
previous_result: Optional[Dict[str, Any]],
|
||||
) -> StepOutput:
|
||||
"""Run a single step"""
|
||||
processor = self.processors[step_name]
|
||||
if not processor:
|
||||
raise ValueError(f"No processor for {step_name}")
|
||||
|
||||
# Check if already completed
|
||||
if state and state.is_step_completed(step_name):
|
||||
logger.info(f"Step {step_name} already completed, skipping")
|
||||
return self._create_skipped_output()
|
||||
|
||||
try:
|
||||
# Run step
|
||||
if previous_result and previous_result.get(step_name):
|
||||
input_data = previous_result[step_name]
|
||||
else:
|
||||
raise ValueError(f"No previous result for {step_name}")
|
||||
|
||||
return await processor.process(input_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Step {step_name} failed: {e}")
|
||||
if state:
|
||||
state.mark_step_failed(step_name, str(e))
|
||||
self.state_manager.save(state)
|
||||
raise
|
||||
|
||||
# Update previous result
|
||||
previous_result[step_name] = output.model_dump()
|
||||
return results
|
||||
|
||||
def _create_skipped_output(self) -> StepOutput:
|
||||
return StepOutput(success=True, error_message="Skipped: already completed")
|
||||
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add step execution logic</name>
|
||||
<files>kb_pipeline/pipeline.py</files>
|
||||
<action>
|
||||
Extend Pipeline.run() to implement step execution logic:
|
||||
|
||||
1. Create content directory
|
||||
2. Run steps in order
|
||||
3. Handle step skipping (check state)
|
||||
4. Handle errors (try/except, log and continue)
|
||||
5. Record final state
|
||||
6. Return results
|
||||
|
||||
**Step execution logic:**
|
||||
```python
|
||||
async def _execute_steps(
|
||||
self,
|
||||
parsed: ParsedLink,
|
||||
state: WorkflowState,
|
||||
previous_result: Dict[str, Any]
|
||||
) -> None:
|
||||
for step_name in self.config.steps:
|
||||
step_start = datetime.now()
|
||||
state.mark_step_started(step_name)
|
||||
|
||||
logger.info(f"[Step {step_num}/{total_steps}] {step_name}...")
|
||||
|
||||
# Get or run processor
|
||||
processor = self.processors[step_name]
|
||||
if not processor:
|
||||
raise ValueError(f"Unknown processor: {step_name}")
|
||||
|
||||
# Get step input
|
||||
input_data = self._create_step_input(
|
||||
step_name=step_name,
|
||||
parsed=parsed,
|
||||
state=state,
|
||||
previous_result=previous_result.get(step_name)
|
||||
)
|
||||
|
||||
# Run processor
|
||||
try:
|
||||
output = await processor.process(input_data)
|
||||
previous_result[step_name] = output.model_dump()
|
||||
results[step_name] = {
|
||||
"status": "success" if output.success else "failed",
|
||||
"output": output.model_dump() if output.success else None,
|
||||
"error": output.error_message
|
||||
}
|
||||
previous_result = output
|
||||
except Exception as e:
|
||||
results[step_name] = {
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
}
|
||||
logger.error(f"[Step {step_num}/{total_steps}] {step_name} - failed: {e}")
|
||||
# Record failure
|
||||
if state:
|
||||
state.mark_step_failed(step_name, str(e))
|
||||
self.state_manager.save(state)
|
||||
# Stop on failure
|
||||
raise
|
||||
|
||||
return results
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_pipeline/test_pipeline.py -v -x</automated>
|
||||
</verify>
|
||||
<done>Pipeline engine executes workflow with state persistence</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Add CLI process command</name>
|
||||
<files>kb-workflow/kb_workflow.py</files>
|
||||
<action>
|
||||
Extend kb-workflow/kb_workflow.py to add process subcommand.
|
||||
|
||||
**CLI additions:**
|
||||
- New argument: 'url' with help='URL to process'
|
||||
- New argument: '--output-dir', '-o' with help='Output directory (default: KB_BASE_PATH/content)
|
||||
- New argument: '--no-analyze', action='store_true', help='Skip analysis step')
|
||||
- Set default to process function that handles the command
|
||||
|
||||
- Call Pipeline().run(url, output_dir, skip_completed=no)
|
||||
- Print success message
|
||||
- If error: print error message and exit(1)
|
||||
|
||||
**Update existing help text and kb_workflow.py to document the new command.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python kb-workflow/kb_workflow.py --help | grep process</automated>
|
||||
</verify>
|
||||
<done>CLI process command added and functional</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Create Pipeline tests</name>
|
||||
<files>tests/test_pipeline/test_pipeline.py,files>
|
||||
<action>
|
||||
Create comprehensive tests for Pipeline class.
|
||||
|
||||
**Test cases:**
|
||||
1. Test pipeline initialization
|
||||
2. Test URL parsing and content dir creation
|
||||
3. Test step execution order
|
||||
4. Test step skipping
|
||||
5. Test error handling and6. Test state persistence
|
||||
|
||||
7. Test full workflow (mocked)
|
||||
|
||||
Use fixtures for mocking processors, state_manager, settings.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_pipeline/test_pipeline.py -v</automated>
|
||||
</verify>
|
||||
<done>Pipeline class fully tested</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All tests pass: `pytest tests/test_pipeline/test_pipeline.py tests/test_cli/test_process_command.py -v`
|
||||
- Pipeline.run() executes workflow correctly
|
||||
- State persistence works
|
||||
- Error handling works
|
||||
- Step skipping works
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Pipeline class orchestrates all processors
|
||||
- Pipeline.run() executes workflow from URL to analysis
|
||||
- Content directory created correctly
|
||||
- Steps executed in order
|
||||
- State persisted after each step
|
||||
- Errors handled with retry
|
||||
- Completed steps skipped
|
||||
- CLI process command works
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-analysis-orchestration/04-04-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,288 @@
|
||||
---
|
||||
phase: 04-analysis-orchestration
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on: [04-04]
|
||||
files_modified:
|
||||
- README.md
|
||||
- tests/test_integration/test_e2e_workflow.py
|
||||
autonomous: false
|
||||
requirements: [MNT-01]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "End-to-end workflow completes successfully"
|
||||
- "Analysis report is saved to content directory"
|
||||
- "workflow_state.json correctly records all steps"
|
||||
artifacts:
|
||||
- path: "README.md"
|
||||
provides: "Project documentation"
|
||||
contains: "## Installation"
|
||||
- path: "tests/test_integration/test_e2e_workflow.py"
|
||||
provides: "End-to-end integration test"
|
||||
contains: "test_e2e"
|
||||
key_links:
|
||||
- from: "kb_pipeline/pipeline.py"
|
||||
to: "kb_pipeline/processors"
|
||||
via: "import processors"
|
||||
pattern: "from.*processor import"
|
||||
- from: "kb-workflow/kb_workflow.py"
|
||||
to: "kb_pipeline"
|
||||
via: "import pipeline"
|
||||
pattern: "from.*pipeline import"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create end-to-end integration test and documentation ( project overview, installation, and and usage.
|
||||
|
||||
Purpose: Verify the complete workflow works and provide clear project documentation for maintainability.
|
||||
Output: README.md with project overview, tests/test_integration/test_e2e_workflow.py
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-analysis-orchestration/04-CONTEXT.md
|
||||
@.planning/phases/04-analysis-orchestration/04-RESEARCH.md
|
||||
@.planning/phases/04-analysis-orchestration/04-04-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing code. Executor should use these directly. -->
|
||||
|
||||
From kb_pipeline/pipeline.py:
|
||||
```python
|
||||
class Pipeline:
|
||||
"""Pipeline orchestration engine"""
|
||||
def __init__(self, ...): # From 04-01
|
||||
...
|
||||
```
|
||||
|
||||
From kb_pipeline/processors/base.py:
|
||||
```python
|
||||
class BaseProcessor(ABC):
|
||||
STEP_NAME: str
|
||||
async def process(self, input_data: StepInput) -> StepOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/parse.py:
|
||||
```python
|
||||
class ParseProcessor(BaseProcessor):
|
||||
STEP_NAME = "parse"
|
||||
async def process(self, input_data: ParseInput) -> ParseOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/download.py:
|
||||
```python
|
||||
class DownloadProcessor(BaseProcessor):
|
||||
STEP_NAME = "download"
|
||||
async def process(self, input_data: DownloadInput) -> DownloadOutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/asr.py:
|
||||
```python
|
||||
class ASRProcessor:
|
||||
STEP_NAME = "asr"
|
||||
async def process(self, input_data: ASRInput) -> ASROutput
|
||||
```
|
||||
|
||||
from kb_pipeline/processors/analyze.py
|
||||
```python
|
||||
class AnalyzeProcessor(BaseProcessor):
|
||||
STEP_NAME = "analyze"
|
||||
async def process(self, input_data: AnalyzeInput) -> AnalyOutput
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create README.md</name>
|
||||
<files>README.md</files>
|
||||
<behavior>
|
||||
- Test 1: README.md exists and contains project overview
|
||||
- Test 2: README.md contains installation instructions
|
||||
</behavior>
|
||||
<action>
|
||||
Create `README.md` in project root directory with the following sections:
|
||||
|
||||
**## kb-skills**
|
||||
|
||||
知识库工作流工具,从链接到分析报告的端到端自动化处理系统。
|
||||
|
||||
### 格 Features
|
||||
|
||||
- 统一 Processor 模式: Parse -> Download -> ASR -> Analyze
|
||||
- 状态管理: workflow_state.json
|
||||
- 配置管理: 通过 ~/.kb_env
|
||||
- 输出: Markdown 格式分析报告
|
||||
|
||||
- 幂等性保证: 重复执行不会产生重复文件
|
||||
|
||||
- 断点续传: 中断后可继续
|
||||
|
||||
- 模块化设计: 新人能在30分钟内理解代码结构
|
||||
- 清晰的项目结构
|
||||
|
||||
- 统一的 CLI 入
|
||||
|
||||
entry
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install kb-pipeline
|
||||
```
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
# 测试
|
||||
pytest tests/test_pipeline/test_schemas.py tests/test_pipeline/test_state.py tests/test_pipeline/test_config.py tests/test_pipeline/test_errors.py tests/test_pipeline/test_retry.py
|
||||
```
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -c "import os; content = open('README.md', 'r', encoding='utf-8'); print(content[:100]) > 0"
|
||||
</verify>
|
||||
<done>README.md created with project overview, installation, and, usage sections, docstrings</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create end-to-end integration test</name>
|
||||
<files>tests/test_integration/test_e2e_workflow.py</files>
|
||||
<behavior>
|
||||
- Test 1: Full workflow executes from URL to analysis
|
||||
- Test 2: Workflow handles errors gracefully
|
||||
- Test 3: State is persisted correctly
|
||||
- Test 4: Workflow is resumable
|
||||
</behavior>
|
||||
<action>
|
||||
Create `tests/test_integration/test_e2e_workflow.py` with comprehensive end-to-end tests.
|
||||
|
||||
**Test structure:**
|
||||
```python
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from kb_pipeline.pipeline import Pipeline
|
||||
from kb_pipeline.config import get_settings
|
||||
from kb_pipeline.processors import ParseProcessor, DownloadProcessor, ASRProcessor, AnalyzeProcessor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_content_dir(tmp_path):
|
||||
"""Create temp content directory"""
|
||||
content_dir.mkdir(parents=True, exist_ok=True)
|
||||
yield content_dir
|
||||
|
||||
# Create test workflow state
|
||||
state_path = content_dir / "workflow_state.json"
|
||||
state = WorkflowState(
|
||||
content_id="test123",
|
||||
platform="bilibili",
|
||||
source_url="https://test.url"
|
||||
)
|
||||
state_manager = WorkflowStateManager(content_dir)
|
||||
yield state_manager
|
||||
|
||||
# Create real pipeline
|
||||
settings = get_settings()
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
ParseProcessor(state_manager),
|
||||
DownloadProcessor(state_manager),
|
||||
ASRProcessor(state_manager),
|
||||
AnalyzeProcessor(state_manager),
|
||||
],
|
||||
output_dir=content_dir,
|
||||
config=PipelineConfig(
|
||||
steps=["parse", "download", "asr", "analyze"],
|
||||
)
|
||||
)
|
||||
yield pipeline
|
||||
# Mock workflow run
|
||||
result = await pipeline.run("https://test.url")
|
||||
|
||||
# Verify success
|
||||
assert result["parse"]["success"]
|
||||
assert result["download"]["success"]
|
||||
assert result["asr"]["success"] or result["asr"]["skipped"] # ASR not applicable for text content
|
||||
assert result["analyze"]["success"]
|
||||
|
||||
# Verify state was saved
|
||||
state = state_manager.load()
|
||||
assert state is not None
|
||||
assert state.is_step_completed("parse")
|
||||
assert state.is_step_completed("download")
|
||||
if content_type in ["video", "zvideo"]:
|
||||
assert state.is_step_completed("asr")
|
||||
assert not state.is_step_completed("asr")
|
||||
assert state.is_step_completed("analyze")
|
||||
|
||||
# Verify output files exist
|
||||
assert (content_dir / "bilibili_test123_metadata.json").exists()
|
||||
assert (content_dir / "bilibili_test123_content.json").exists() or result["content_type"] == "video":
|
||||
assert (content_dir / "bilibili_test123_analysis.md").exists()
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_integration/test_e2e_workflow.py -v</automated>
|
||||
</verify>
|
||||
<done>End-to-end workflow tested with state persistence</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
End-to-end workflow (Phase 1-2/3):
|
||||
- Pipeline Engine with 4 processors (Parse, Download, ASR, Analyze)
|
||||
- CLI entry point (kb-workflow process command)
|
||||
- DeepAnalyzer integration for analysis reports saved as Markdown
|
||||
- State persistence via workflow_state.json
|
||||
</what-built>
|
||||
DeepAnalyzer integration:
|
||||
- B站视频/专栏链接 → parse, download, ASR, analyze
|
||||
- Error recovery with retry
|
||||
- Progress logging with simple format
|
||||
- Documentation in README.md and docstrings
|
||||
</how-to-verify>
|
||||
1. Run `pytest tests/test_integration/test_e2e_workflow.py -v -x`
|
||||
2. Visit https://github.com/your-org/kb-skills
|
||||
3. Run `kb-workflow process <url>` to test
|
||||
4. Run `kb-workflow process --help` to see full usage
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" or describe any issues found
|
||||
release-signal>Type: "approved" to indicate you should proceed to Phase 4 for finalize the documentation
|
||||
</how-to-verify>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- All tests pass
|
||||
- End-to-end workflow tested with state persistence
|
||||
- README.md exists with project overview
|
||||
- tests pass
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- README.md complete with project overview, installation, and
|
||||
usage
|
||||
- End-to-end integration test passes
|
||||
- workflow_state.json correctly records all steps
|
||||
- Documentation is clear and comprehensive
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-analysis-orchestration/04-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user