Spaces:
Build error
Build error
|
Download docs/plans/dynamic-smart-crusher.md from minhtudragon/headroom_3: direct link, hf CLI and curl.
- Browser
- Download file 25.2 kB
-
https://huggingface.co/spaces/minhtudragon/headroom_3/resolve/3d4e512bd84d4abf86585f60770234d527222955/docs/plans/dynamic-smart-crusher.md
- Command line
-
hf download hf://spaces/minhtudragon/headroom_3@3d4e512bd84d4abf86585f60770234d527222955/docs/plans/dynamic-smart-crusher.md
-
curl -L -o dynamic-smart-crusher.md https://huggingface.co/spaces/minhtudragon/headroom_3/resolve/3d4e512bd84d4abf86585f60770234d527222955/docs/plans/dynamic-smart-crusher.md
25.2 kB
| # Dynamic SmartCrusher Preservation Plan | |
| ## Problem Statement | |
| Current SmartCrusher uses **static "First 3 + Last 2" preservation** regardless of: | |
| - Array size (100 items vs 10,000 items get same treatment) | |
| - Data pattern (time series vs search results vs logs) | |
| - Query context (user asking about "latest" vs "oldest") | |
| - Position importance (first items might all be identical/wasteful) | |
| - Learned retrieval patterns (which positions do users actually need?) | |
| This is too simplistic for a production-grade compression system. | |
| --- | |
| ## Current Implementation Analysis | |
| **Location:** `headroom/transforms/smart_crusher.py` | |
| **Current Logic (lines 2273-2279, 2353-2359, 2561-2567):** | |
| ```python | |
| # Always keep first 3 | |
| for i in range(min(3, n)): | |
| keep_indices.add(i) | |
| # Always keep last 2 | |
| for i in range(max(0, n - 2), n): | |
| keep_indices.add(i) | |
| ``` | |
| **Problems:** | |
| 1. **Fixed slots waste budget** - If first 3 items are identical, we've wasted 3 slots | |
| 2. **No size adaptation** - 20-item array loses 25% to anchors; 1000-item array loses 0.5% | |
| 3. **Pattern-agnostic** - Search results don't need "last 2"; time series might need more recency | |
| 4. **No learning** - Doesn't adapt based on what users actually retrieve | |
| --- | |
| ## Proposed Solution: Adaptive Slot Allocation | |
| ### Idea 1: Size-Proportional Anchor Budget | |
| Instead of fixed counts, allocate a **percentage budget** for position-based anchors: | |
| ```python | |
| def calculate_anchor_budget(array_size: int, max_items: int) -> AnchorBudget: | |
| """Allocate slots proportionally, with floors and ceilings.""" | |
| # Base: 20% of output budget for position anchors | |
| total_anchor_slots = max(3, min(10, int(max_items * 0.20))) | |
| # Distribution: 60% front, 40% back (front-weighted for context) | |
| front_slots = max(1, int(total_anchor_slots * 0.6)) | |
| back_slots = max(1, total_anchor_slots - front_slots) | |
| return AnchorBudget(front=front_slots, back=back_slots) | |
| ``` | |
| **Example:** | |
| | Array Size | Max Items | Anchor Budget | Front | Back | | |
| |------------|-----------|---------------|-------|------| | |
| | 50 | 10 | 3 | 2 | 1 | | |
| | 200 | 15 | 3 | 2 | 1 | | |
| | 1000 | 20 | 4 | 3 | 1 | | |
| | 5000 | 30 | 6 | 4 | 2 | | |
| --- | |
| ### Idea 2: Pattern-Aware Anchor Weighting | |
| Different data patterns need different position importance: | |
| ```python | |
| class AnchorStrategy(Enum): | |
| FRONT_HEAVY = "front_heavy" # Search results: top items matter most | |
| BACK_HEAVY = "back_heavy" # Logs: recent items matter most | |
| BALANCED = "balanced" # Time series: both ends matter | |
| MIDDLE_AWARE = "middle_aware" # Database: order might be arbitrary | |
| def get_anchor_strategy(pattern: DataPattern) -> AnchorStrategy: | |
| return { | |
| DataPattern.SEARCH_RESULTS: AnchorStrategy.FRONT_HEAVY, # Top N by score | |
| DataPattern.LOGS: AnchorStrategy.BACK_HEAVY, # Recency matters | |
| DataPattern.TIME_SERIES: AnchorStrategy.BALANCED, # Both ends for trend | |
| DataPattern.GENERIC: AnchorStrategy.MIDDLE_AWARE, # Don't assume order | |
| }.get(pattern, AnchorStrategy.BALANCED) | |
| ``` | |
| **FRONT_HEAVY (Search Results):** | |
| - Front: 80% of anchor budget | |
| - Back: 20% of anchor budget | |
| - Rationale: Top search results are ranked by relevance | |
| **BACK_HEAVY (Logs):** | |
| - Front: 20% of anchor budget | |
| - Back: 80% of anchor budget | |
| - Rationale: Most recent logs are usually most relevant | |
| **BALANCED (Time Series):** | |
| - Front: 50% of anchor budget | |
| - Back: 50% of anchor budget | |
| - Rationale: Need both start and end for trend analysis | |
| **MIDDLE_AWARE (Generic/Database):** | |
| - Front: 30% of anchor budget | |
| - Back: 30% of anchor budget | |
| - Middle sample: 40% of anchor budget (stratified) | |
| - Rationale: Order might be arbitrary; sample across positions | |
| --- | |
| ### Idea 3: Query-Aware Dynamic Weighting | |
| Adjust anchor strategy based on user's query: | |
| ```python | |
| def adjust_for_query(base_strategy: AnchorStrategy, query: str) -> AnchorWeights: | |
| """Shift anchor weights based on query intent.""" | |
| weights = base_strategy.default_weights() | |
| # Recency signals | |
| recency_keywords = ["latest", "recent", "last", "newest", "current"] | |
| if any(kw in query.lower() for kw in recency_keywords): | |
| weights.back_weight *= 1.5 | |
| weights.front_weight *= 0.7 | |
| # Historical signals | |
| historical_keywords = ["first", "oldest", "earliest", "original", "initial"] | |
| if any(kw in query.lower() for kw in historical_keywords): | |
| weights.front_weight *= 1.5 | |
| weights.back_weight *= 0.7 | |
| # Range signals | |
| range_keywords = ["all", "every", "complete", "full"] | |
| if any(kw in query.lower() for kw in range_keywords): | |
| weights.middle_weight *= 1.3 # Better coverage | |
| return weights.normalize() | |
| ``` | |
| --- | |
| ### Idea 4: Information-Density Anchor Selection | |
| Don't blindly take first N - select **most informative** items from anchor regions: | |
| ```python | |
| def select_informative_anchors( | |
| items: list[dict], | |
| region: str, # "front", "back", "middle" | |
| slots: int, | |
| all_items_hash: set[str] | |
| ) -> list[int]: | |
| """Select most informative items from a region.""" | |
| if region == "front": | |
| candidates = list(range(min(slots * 3, len(items)))) # Consider 3x candidates | |
| elif region == "back": | |
| start = max(0, len(items) - slots * 3) | |
| candidates = list(range(start, len(items))) | |
| else: # middle | |
| step = len(items) // (slots * 3 + 1) | |
| candidates = [i * step for i in range(1, slots * 3 + 1)] | |
| # Score each candidate by information content | |
| scored = [] | |
| for idx in candidates: | |
| item = items[idx] | |
| item_hash = hash_item(item) | |
| # Skip if we've seen identical item | |
| if item_hash in all_items_hash: | |
| continue | |
| score = calculate_information_score(item, items) | |
| scored.append((idx, score, item_hash)) | |
| # Select top N by information score | |
| scored.sort(key=lambda x: x[1], reverse=True) | |
| selected = [] | |
| for idx, _, item_hash in scored[:slots]: | |
| selected.append(idx) | |
| all_items_hash.add(item_hash) | |
| return sorted(selected) | |
| def calculate_information_score(item: dict, all_items: list[dict]) -> float: | |
| """Score item by how much unique information it contributes.""" | |
| score = 0.0 | |
| # 1. Field uniqueness - rare field values score higher | |
| for field, value in item.items(): | |
| field_values = [i.get(field) for i in all_items if field in i] | |
| value_frequency = field_values.count(value) / len(field_values) | |
| score += (1 - value_frequency) # Rare values score higher | |
| # 2. Structural uniqueness - different fields than typical | |
| typical_fields = get_typical_fields(all_items) | |
| unique_fields = set(item.keys()) - typical_fields | |
| score += len(unique_fields) * 0.5 | |
| # 3. Content length - longer items often more informative | |
| content_length = len(json.dumps(item)) | |
| avg_length = sum(len(json.dumps(i)) for i in all_items) / len(all_items) | |
| if content_length > avg_length: | |
| score += 0.3 | |
| return score | |
| ``` | |
| --- | |
| ### Idea 5: TOIN-Learned Position Importance | |
| Track which positions users actually retrieve and learn from it: | |
| ```python | |
| @dataclass | |
| class PositionRetrievalPattern: | |
| """Learned position importance from retrieval data.""" | |
| tool_name: str | |
| total_compressions: int | |
| position_retrievals: dict[str, int] # "front_10%", "middle", "back_10%" | |
| def get_position_weights(self) -> dict[str, float]: | |
| """Convert retrieval counts to weights.""" | |
| total = sum(self.position_retrievals.values()) | |
| if total == 0: | |
| return {"front": 0.5, "middle": 0.0, "back": 0.5} | |
| return { | |
| position: count / total | |
| for position, count in self.position_retrievals.items() | |
| } | |
| class TOINPositionLearning: | |
| """Learn position importance from retrieval patterns.""" | |
| def record_retrieval( | |
| self, | |
| tool_name: str, | |
| original_size: int, | |
| retrieved_indices: list[int] | |
| ): | |
| """Record which positions were retrieved.""" | |
| for idx in retrieved_indices: | |
| position = self._classify_position(idx, original_size) | |
| self._increment_position_count(tool_name, position) | |
| def _classify_position(self, idx: int, size: int) -> str: | |
| """Classify index into position bucket.""" | |
| relative_pos = idx / size | |
| if relative_pos < 0.1: | |
| return "front_10%" | |
| elif relative_pos < 0.3: | |
| return "front_30%" | |
| elif relative_pos > 0.9: | |
| return "back_10%" | |
| elif relative_pos > 0.7: | |
| return "back_30%" | |
| else: | |
| return "middle" | |
| def get_anchor_recommendation(self, tool_name: str) -> AnchorWeights: | |
| """Get learned anchor weights for a tool.""" | |
| pattern = self._get_pattern(tool_name) | |
| if pattern.total_compressions < 10: | |
| return AnchorWeights.default() # Not enough data | |
| weights = pattern.get_position_weights() | |
| return AnchorWeights( | |
| front=weights.get("front_10%", 0.3) + weights.get("front_30%", 0.1), | |
| middle=weights.get("middle", 0.2), | |
| back=weights.get("back_10%", 0.3) + weights.get("back_30%", 0.1), | |
| ) | |
| ``` | |
| --- | |
| ### Idea 6: Stratified Sampling for Middle Positions | |
| For large arrays, sample strategically from middle: | |
| ```python | |
| def stratified_middle_sample( | |
| items: list[dict], | |
| num_samples: int, | |
| analysis: ArrayAnalysis | |
| ) -> list[int]: | |
| """Sample middle positions using stratified approach.""" | |
| n = len(items) | |
| front_boundary = int(n * 0.1) | |
| back_boundary = int(n * 0.9) | |
| middle_items = list(range(front_boundary, back_boundary)) | |
| if not middle_items or num_samples <= 0: | |
| return [] | |
| # Strategy 1: Cluster-based sampling | |
| if analysis.has_clusterable_field: | |
| clusters = cluster_by_field(items, analysis.cluster_field) | |
| return sample_from_clusters(clusters, num_samples, middle_items) | |
| # Strategy 2: Variance-based sampling (pick high-variance points) | |
| if analysis.numeric_fields: | |
| variance_scores = calculate_position_variance(items, analysis.numeric_fields) | |
| sorted_by_variance = sorted( | |
| middle_items, | |
| key=lambda i: variance_scores.get(i, 0), | |
| reverse=True | |
| ) | |
| return sorted(sorted_by_variance[:num_samples]) | |
| # Strategy 3: Even spacing (fallback) | |
| step = len(middle_items) // (num_samples + 1) | |
| return [middle_items[i * step] for i in range(1, num_samples + 1)] | |
| ``` | |
| --- | |
| ## Testing Strategy | |
| ### Test Category 1: Adversarial Position Tests | |
| Test cases where important data is NOT at expected positions: | |
| ```python | |
| class TestAdversarialPositions: | |
| """Test scenarios that break 'first 3 + last 2' assumption.""" | |
| def test_important_data_in_middle(self): | |
| """Critical error at position 50 of 100-item array.""" | |
| items = [{"status": "ok", "value": i} for i in range(100)] | |
| items[50] = {"status": "error", "error_code": "CRITICAL", "value": 50} | |
| result = smart_crusher.crush(items, max_items=10) | |
| # Error item MUST be preserved regardless of position | |
| assert any(item.get("error_code") == "CRITICAL" for item in result) | |
| def test_spike_not_at_boundaries(self): | |
| """Numeric spike at position 75 of 100-item array.""" | |
| items = [{"metric": 10.0 + random.random()} for _ in range(100)] | |
| items[75]["metric"] = 1000.0 # Spike | |
| result = smart_crusher.crush(items, max_items=10) | |
| # Spike MUST be preserved as anomaly | |
| assert any(item["metric"] > 500 for item in result) | |
| def test_first_items_identical(self): | |
| """First 10 items are identical - shouldn't waste slots.""" | |
| items = [{"id": "same", "value": 0}] * 10 + [ | |
| {"id": f"unique_{i}", "value": i} for i in range(90) | |
| ] | |
| result = smart_crusher.crush(items, max_items=10) | |
| # Should NOT have multiple identical items | |
| ids = [item["id"] for item in result] | |
| # At most 1-2 of the identical items, not 3 | |
| assert ids.count("same") <= 2 | |
| def test_last_items_identical(self): | |
| """Last 10 items are identical - shouldn't waste slots.""" | |
| items = [{"id": f"unique_{i}", "value": i} for i in range(90)] + [ | |
| {"id": "same", "value": 100} | |
| ] * 10 | |
| result = smart_crusher.crush(items, max_items=10) | |
| ids = [item["id"] for item in result] | |
| assert ids.count("same") <= 2 | |
| def test_relevant_item_in_middle(self): | |
| """Item matching user query is in middle of array.""" | |
| items = [{"name": f"item_{i}", "status": "active"} for i in range(100)] | |
| items[42]["name"] = "target_item" | |
| items[42]["description"] = "This is what user asked about" | |
| result = smart_crusher.crush( | |
| items, | |
| max_items=10, | |
| query="find target_item" | |
| ) | |
| # Query-matched item MUST be preserved | |
| assert any("target_item" in item.get("name", "") for item in result) | |
| ``` | |
| ### Test Category 2: Size Adaptation Tests | |
| ```python | |
| class TestSizeAdaptation: | |
| """Test that anchor allocation scales with array size.""" | |
| @pytest.mark.parametrize("size,expected_min_anchors", [ | |
| (20, 3), # Small array: at least 3 anchors | |
| (100, 4), # Medium array: at least 4 anchors | |
| (500, 5), # Large array: at least 5 anchors | |
| (2000, 6), # Very large: at least 6 anchors | |
| ]) | |
| def test_anchor_count_scales(self, size, expected_min_anchors): | |
| """Anchor count should increase with array size.""" | |
| items = [{"id": i, "value": i * 10} for i in range(size)] | |
| result = smart_crusher.crush(items, max_items=20) | |
| # Count items from first 10% and last 10% | |
| anchor_count = sum( | |
| 1 for item in result | |
| if item["id"] < size * 0.1 or item["id"] > size * 0.9 | |
| ) | |
| assert anchor_count >= expected_min_anchors | |
| def test_small_array_high_preservation(self): | |
| """Small arrays should preserve higher percentage.""" | |
| items = [{"id": i} for i in range(15)] | |
| result = smart_crusher.crush(items, max_items=20) | |
| # Should preserve most/all of small array | |
| assert len(result) >= 10 # At least 66% | |
| def test_large_array_efficient_sampling(self): | |
| """Large arrays should sample efficiently.""" | |
| items = [{"id": i, "value": i} for i in range(5000)] | |
| result = smart_crusher.crush(items, max_items=20) | |
| # Should have good distribution across positions | |
| positions = [item["id"] for item in result] | |
| has_front = any(p < 500 for p in positions) | |
| has_middle = any(500 < p < 4500 for p in positions) | |
| has_back = any(p > 4500 for p in positions) | |
| assert has_front and has_back | |
| # Middle should be represented if array is large enough | |
| assert has_middle | |
| ``` | |
| ### Test Category 3: Pattern-Specific Tests | |
| ```python | |
| class TestPatternAwareAnchoring: | |
| """Test pattern-specific anchor strategies.""" | |
| def test_search_results_front_heavy(self): | |
| """Search results should preserve more from front.""" | |
| items = [ | |
| {"title": f"Result {i}", "score": 1.0 - (i * 0.01)} | |
| for i in range(100) | |
| ] | |
| result = smart_crusher.crush(items, max_items=10) | |
| # More items should be from front (high scores) | |
| front_count = sum(1 for item in result if item["score"] > 0.9) | |
| back_count = sum(1 for item in result if item["score"] < 0.1) | |
| assert front_count > back_count | |
| def test_logs_back_heavy(self): | |
| """Logs should preserve more from back (recent).""" | |
| items = [ | |
| {"timestamp": f"2024-01-{i:02d}", "level": "INFO", "message": f"Log {i}"} | |
| for i in range(1, 31) | |
| ] | |
| result = smart_crusher.crush(items, max_items=10) | |
| # More items should be from back (recent logs) | |
| timestamps = [item["timestamp"] for item in result] | |
| recent_count = sum(1 for ts in timestamps if int(ts[-2:]) > 20) | |
| old_count = sum(1 for ts in timestamps if int(ts[-2:]) < 10) | |
| assert recent_count >= old_count | |
| def test_time_series_balanced(self): | |
| """Time series should have balanced front/back.""" | |
| items = [ | |
| {"timestamp": f"2024-01-01T{i:02d}:00:00", "value": 100 + i} | |
| for i in range(24) | |
| ] | |
| result = smart_crusher.crush(items, max_items=8) | |
| hours = [int(item["timestamp"][11:13]) for item in result] | |
| front_count = sum(1 for h in hours if h < 8) | |
| back_count = sum(1 for h in hours if h > 16) | |
| # Should be roughly balanced | |
| assert abs(front_count - back_count) <= 2 | |
| ``` | |
| ### Test Category 4: Query-Aware Tests | |
| ```python | |
| class TestQueryAwareAnchoring: | |
| """Test query-based anchor adjustment.""" | |
| def test_latest_query_shifts_to_back(self): | |
| """'Latest' in query should preserve more recent items.""" | |
| items = [{"id": i, "created": f"2024-01-{i:02d}"} for i in range(1, 31)] | |
| result = smart_crusher.crush( | |
| items, | |
| max_items=8, | |
| query="Show me the latest entries" | |
| ) | |
| ids = [item["id"] for item in result] | |
| recent_count = sum(1 for id in ids if id > 20) | |
| assert recent_count >= 3 # At least 3 recent items | |
| def test_first_query_shifts_to_front(self): | |
| """'First' in query should preserve earlier items.""" | |
| items = [{"id": i, "created": f"2024-01-{i:02d}"} for i in range(1, 31)] | |
| result = smart_crusher.crush( | |
| items, | |
| max_items=8, | |
| query="Show me the first entries" | |
| ) | |
| ids = [item["id"] for item in result] | |
| early_count = sum(1 for id in ids if id < 10) | |
| assert early_count >= 3 | |
| def test_specific_id_query_finds_item(self): | |
| """Query for specific ID should find it regardless of position.""" | |
| items = [{"id": f"item_{i:04d}", "value": i} for i in range(1000)] | |
| result = smart_crusher.crush( | |
| items, | |
| max_items=10, | |
| query="Find item_0567" | |
| ) | |
| assert any(item["id"] == "item_0567" for item in result) | |
| ``` | |
| ### Test Category 5: Coverage Metrics Tests | |
| ```python | |
| class TestCoverageMetrics: | |
| """Test that preserved items represent the full distribution.""" | |
| def test_value_range_coverage(self): | |
| """Preserved items should cover the value range.""" | |
| items = [{"value": i} for i in range(100)] | |
| result = smart_crusher.crush(items, max_items=10) | |
| values = [item["value"] for item in result] | |
| # Should cover most of the range | |
| assert min(values) < 10 # Has low values | |
| assert max(values) > 90 # Has high values | |
| # Should have some middle values too | |
| middle_count = sum(1 for v in values if 30 < v < 70) | |
| assert middle_count >= 1 | |
| def test_category_coverage(self): | |
| """Preserved items should represent all categories.""" | |
| items = [ | |
| {"category": cat, "id": i} | |
| for i, cat in enumerate(["A"] * 30 + ["B"] * 30 + ["C"] * 40) | |
| ] | |
| result = smart_crusher.crush(items, max_items=10) | |
| categories = set(item["category"] for item in result) | |
| # Should have at least 2 of 3 categories | |
| assert len(categories) >= 2 | |
| def test_temporal_coverage(self): | |
| """Preserved items should span the time range.""" | |
| items = [ | |
| {"timestamp": f"2024-{m:02d}-15", "event": f"event_{i}"} | |
| for i, m in enumerate(range(1, 13)) | |
| ] | |
| result = smart_crusher.crush(items, max_items=5) | |
| months = [int(item["timestamp"][5:7]) for item in result] | |
| # Should span at least 6 months of the year | |
| assert max(months) - min(months) >= 6 | |
| ``` | |
| ### Test Category 6: Retrieval Simulation Tests | |
| ```python | |
| class TestRetrievalSimulation: | |
| """Simulate user retrieval patterns to measure effectiveness.""" | |
| def test_retrieval_hit_rate_random_queries(self): | |
| """Measure how often preserved items satisfy random queries.""" | |
| items = [ | |
| {"id": i, "name": f"Item {i}", "category": f"cat_{i % 5}"} | |
| for i in range(100) | |
| ] | |
| compressed = smart_crusher.crush(items, max_items=15) | |
| compressed_ids = {item["id"] for item in compressed} | |
| # Simulate 100 random "queries" (random item lookups) | |
| hits = 0 | |
| for _ in range(100): | |
| target_id = random.randint(0, 99) | |
| if target_id in compressed_ids: | |
| hits += 1 | |
| # Should hit at least 15% (we keep 15 of 100) | |
| assert hits >= 15 | |
| def test_retrieval_hit_rate_weighted_queries(self): | |
| """Measure hits for queries weighted toward common patterns.""" | |
| items = [{"id": i, "value": i * 10} for i in range(100)] | |
| compressed = smart_crusher.crush(items, max_items=15) | |
| compressed_ids = {item["id"] for item in compressed} | |
| # Weight queries toward front (30%), back (30%), anomalies (40%) | |
| hits = 0 | |
| queries = ( | |
| list(range(10)) * 3 + # Front queries | |
| list(range(90, 100)) * 3 + # Back queries | |
| [50] * 4 # Middle anomaly queries | |
| ) | |
| for target_id in queries: | |
| if target_id in compressed_ids: | |
| hits += 1 | |
| # Should hit more often than random due to anchor strategy | |
| assert hits >= 20 # At least 20% hit rate | |
| ``` | |
| --- | |
| ## Implementation Phases | |
| ### Phase 1: Refactor Anchor Logic (Foundation) | |
| 1. Extract anchor selection into `AnchorSelector` class | |
| 2. Make slot counts configurable via `AnchorConfig` | |
| 3. Add size-proportional allocation | |
| 4. Maintain backward compatibility with current defaults | |
| ### Phase 2: Pattern-Aware Anchoring | |
| 1. Map `DataPattern` to `AnchorStrategy` | |
| 2. Implement front-heavy, back-heavy, balanced, middle-aware strategies | |
| 3. Add pattern-specific anchor weight configs | |
| ### Phase 3: Information-Density Selection | |
| 1. Add `calculate_information_score()` for items | |
| 2. Select from candidate region instead of fixed positions | |
| 3. Deduplicate identical items across regions | |
| ### Phase 4: Query-Aware Adjustment | |
| 1. Parse query for position intent keywords | |
| 2. Adjust anchor weights dynamically | |
| 3. Add query-position relevance scoring | |
| ### Phase 5: TOIN Position Learning | |
| 1. Track retrieval positions in TOIN | |
| 2. Learn per-tool position importance | |
| 3. Use learned weights to adjust anchor strategy | |
| ### Phase 6: Comprehensive Testing | |
| 1. Implement all adversarial tests | |
| 2. Add coverage metric tests | |
| 3. Add retrieval simulation tests | |
| 4. Performance benchmarks | |
| --- | |
| ## Configuration Schema | |
| ```python | |
| @dataclass | |
| class AnchorConfig: | |
| """Configuration for dynamic anchor allocation.""" | |
| # Base anchor budget as percentage of max_items | |
| anchor_budget_pct: float = 0.20 # 20% of slots for position anchors | |
| # Minimum and maximum anchor slots | |
| min_anchor_slots: int = 3 | |
| max_anchor_slots: int = 10 | |
| # Default distribution (overridden by pattern) | |
| default_front_weight: float = 0.5 | |
| default_back_weight: float = 0.5 | |
| default_middle_weight: float = 0.0 | |
| # Pattern-specific overrides | |
| search_front_weight: float = 0.8 | |
| logs_back_weight: float = 0.8 | |
| time_series_balance: float = 0.5 | |
| # Query keyword detection | |
| recency_keywords: list[str] = field(default_factory=lambda: [ | |
| "latest", "recent", "last", "newest", "current" | |
| ]) | |
| historical_keywords: list[str] = field(default_factory=lambda: [ | |
| "first", "oldest", "earliest", "original", "initial" | |
| ]) | |
| # Information density selection | |
| use_information_density: bool = True | |
| candidate_multiplier: int = 3 # Consider 3x candidates per slot | |
| # TOIN learning | |
| use_learned_positions: bool = True | |
| min_samples_for_learning: int = 10 | |
| ``` | |
| --- | |
| ## Success Metrics | |
| 1. **Retrieval Coverage**: % of user retrievals that hit preserved items (target: >80%) | |
| 2. **Information Density**: Unique information per preserved slot (target: no duplicate items) | |
| 3. **Distribution Coverage**: Preserved items span full value/time/category ranges | |
| 4. **Adversarial Robustness**: All adversarial tests pass | |
| 5. **Backward Compatibility**: Existing tests still pass | |
| 6. **Performance**: <5ms additional latency for anchor selection | |
| --- | |
| ## Risks and Mitigations | |
| | Risk | Mitigation | | |
| |------|------------| | |
| | Information density calculation is expensive | Cache scores, limit candidate pool | | |
| | Query keyword detection is brittle | Use as soft signal, not hard rule | | |
| | TOIN learning needs cold start | Fall back to pattern-based defaults | | |
| | Breaking existing behavior | Feature flag, A/B testing | | |
| | Middle sampling misses important items | Always include anomalies/errors regardless | | |
| --- | |
| ## Next Steps | |
| 1. Review and approve this plan | |
| 2. Write failing tests first (TDD approach) | |
| 3. Implement Phase 1 (refactor foundation) | |
| 4. Iterate through phases with test validation | |
| 5. Benchmark against current implementation | |
| 6. A/B test in production with telemetry | |