---
phase: 04-retrieval-nodes
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- src/skill_retriever/nodes/retrieval/__init__.py
- src/skill_retriever/nodes/retrieval/models.py
- src/skill_retriever/nodes/retrieval/query_planner.py
- src/skill_retriever/nodes/retrieval/vector_search.py
- tests/test_query_planner.py
- tests/test_vector_search.py
autonomous: true
must_haves:
truths:
- "Query planner classifies queries as simple/moderate/complex based on heuristics"
- "Vector search returns semantically relevant components for natural language queries"
- "Type filtering works after vector retrieval (not during)"
artifacts:
- path: "src/skill_retriever/nodes/retrieval/models.py"
provides: "QueryComplexity enum, RetrievalPlan dataclass, RankedComponent model"
exports: ["QueryComplexity", "RetrievalPlan", "RankedComponent"]
- path: "src/skill_retriever/nodes/retrieval/query_planner.py"
provides: "Query classification and entity extraction"
exports: ["plan_retrieval", "extract_query_entities"]
- path: "src/skill_retriever/nodes/retrieval/vector_search.py"
provides: "Vector search with embedding generation and type filtering"
exports: ["search_by_text", "search_with_type_filter"]
key_links:
- from: "query_planner.py"
to: "models.py"
via: "imports QueryComplexity, RetrievalPlan"
pattern: "from.*models import"
- from: "vector_search.py"
to: "FAISSVectorStore"
via: "search method call"
pattern: "vector_store\\.search"
- from: "vector_search.py"
to: "fastembed"
via: "embedding generation"
pattern: "TextEmbedding|embed"
---
<objective>
Create the query planner and vector search retrieval node for Phase 4.
Purpose: Enable natural language search over the component graph with query complexity classification to optimize retrieval strategy downstream.
Output:
- `models.py` with QueryComplexity enum, RetrievalPlan, and RankedComponent Pydantic models
- `query_planner.py` with heuristic-based query classification and entity extraction
- `vector_search.py` with text-to-embedding search and post-retrieval type filtering
- Test suites for both modules
</objective>
<execution_context>
@C:\Users\33641\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\33641\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-retrieval-nodes/04-RESEARCH.md
@src/skill_retriever/memory/vector_store.py
@src/skill_retriever/memory/graph_store.py
@src/skill_retriever/entities/components.py
@src/skill_retriever/config.py
</context>
<tasks>
<task type="auto">
<name>Task 1: Create retrieval models and query planner</name>
<files>
src/skill_retriever/nodes/retrieval/__init__.py
src/skill_retriever/nodes/retrieval/models.py
src/skill_retriever/nodes/retrieval/query_planner.py
tests/test_query_planner.py
</files>
<action>
Create the retrieval nodes directory and implement:
**models.py:**
- `QueryComplexity(StrEnum)`: SIMPLE, MODERATE, COMPLEX
- `RetrievalPlan` dataclass with fields: complexity, use_ppr, use_flow_pruning, ppr_alpha, max_results
- `RankedComponent` Pydantic model with: component_id, score, rank, source (vector/graph/fused)
**query_planner.py:**
Port heuristics from z-commands query-planner.js:
- `plan_retrieval(query: str, entity_count: int) -> RetrievalPlan`
- SIMPLE: query < 300 chars AND entity_count <= 2 (skip PPR, alpha=0.85, max=10)
- MODERATE: 300-600 chars OR 3-5 entities (use PPR, no flow pruning, alpha=0.85, max=20)
- COMPLEX: query > 600 chars OR > 5 entities (use PPR + flow pruning, alpha=0.7, max=30)
- `extract_query_entities(query: str, graph_store: GraphStore) -> set[str]`
- Tokenize query by whitespace and punctuation
- Filter stopwords (the, a, an, is, are, to, for, with, how, what, etc.)
- Match remaining tokens against graph node labels (case-insensitive)
- Return set of matching node IDs
- Add STOPWORDS constant with common words to filter
**tests/test_query_planner.py:**
- Test plan_retrieval returns SIMPLE for short single-entity queries
- Test plan_retrieval returns MODERATE for medium queries
- Test plan_retrieval returns COMPLEX for long multi-entity queries
- Test extract_query_entities filters stopwords
- Test extract_query_entities matches graph node labels case-insensitively
- Test extract_query_entities returns empty set when no matches
Use `from __future__ import annotations` in all files. Follow existing project patterns (pyright strict, ruff).
</action>
<verify>
```bash
uv run pytest tests/test_query_planner.py -v
uv run pyright src/skill_retriever/nodes/retrieval/
uv run ruff check src/skill_retriever/nodes/retrieval/
```
All tests pass, pyright 0 errors, ruff 0 errors.
</verify>
<done>
Query planner correctly classifies queries by complexity and extracts entities from text.
</done>
</task>
<task type="auto">
<name>Task 2: Create vector search node with type filtering</name>
<files>
src/skill_retriever/nodes/retrieval/vector_search.py
tests/test_vector_search.py
src/skill_retriever/nodes/retrieval/__init__.py
</files>
<action>
**vector_search.py:**
- `search_by_text(query: str, vector_store: FAISSVectorStore, top_k: int = 10) -> list[RankedComponent]`
- Generate embedding for query using fastembed TextEmbedding (BAAI/bge-small-en-v1.5 from EMBEDDING_CONFIG)
- Call vector_store.search(embedding, top_k)
- Convert to RankedComponent list with source="vector"
- Cache TextEmbedding instance at module level (expensive to create)
- `search_with_type_filter(query: str, vector_store: FAISSVectorStore, graph_store: GraphStore, component_type: ComponentType | None = None, top_k: int = 10) -> list[RankedComponent]`
- Fetch 3x top_k from vector_store (to allow filtering)
- If component_type is None, return first top_k
- Otherwise, filter results by checking graph_store.get_node(id).component_type == component_type
- Return first top_k that match filter
- This implements the "type filter AFTER score fusion" pattern from research
**tests/test_vector_search.py:**
- Create fixtures with FAISSVectorStore and NetworkXGraphStore populated with test data
- Test search_by_text returns results sorted by score descending
- Test search_by_text returns RankedComponent with source="vector"
- Test search_with_type_filter returns only components of specified type
- Test search_with_type_filter with None type returns all types
- Test search returns empty list for query with no similar vectors
**Update __init__.py:**
Export: QueryComplexity, RetrievalPlan, RankedComponent, plan_retrieval, extract_query_entities, search_by_text, search_with_type_filter
Handle fastembed TextEmbedding lazy initialization to avoid startup cost. Use existing EMBEDDING_CONFIG.model_name for model selection.
</action>
<verify>
```bash
uv run pytest tests/test_vector_search.py -v
uv run pyright src/skill_retriever/nodes/retrieval/
uv run ruff check src/skill_retriever/nodes/retrieval/
```
All tests pass, pyright 0 errors, ruff 0 errors.
</verify>
<done>
Vector search node generates embeddings from text queries, searches FAISS, and filters by component type after retrieval.
</done>
</task>
</tasks>
<verification>
```bash
# Full test suite for retrieval nodes
uv run pytest tests/test_query_planner.py tests/test_vector_search.py -v
# Type checking
uv run pyright src/skill_retriever/nodes/retrieval/
# Linting
uv run ruff check src/skill_retriever/nodes/retrieval/
# Verify exports work
uv run python -c "from skill_retriever.nodes.retrieval import QueryComplexity, plan_retrieval, search_by_text; print('Imports OK')"
```
</verification>
<success_criteria>
1. Query planner classifies "find agent" as SIMPLE (short, likely 0-1 entities)
2. Query planner classifies 500-char query with 4 entities as MODERATE
3. Query planner classifies 800-char query with 7 entities as COMPLEX
4. Vector search returns RankedComponent objects with scores
5. Type filter returns only components of requested type
6. All pyright and ruff checks pass
</success_criteria>
<output>
After completion, create `.planning/phases/04-retrieval-nodes/04-01-SUMMARY.md`
</output>