Phase 4.1: AI Prompt Enhancement — Spare Parts Deep Identification Artifacts: - 4.1-RESEARCH.md: Web scraping patterns, spare-parts classification, integration architecture - 4.1-PLAN-01.md (Wave 1): Spare-parts whitelist + AI prompt enhancement (4 tasks) - 4.1-PLAN-02.md (Wave 2): Web scraping service + backend integration (6 tasks) - 4.1-PLAN-03.md (Wave 3): Frontend integration + end-to-end testing (7 tasks) All 17 tasks verified: ✓ Concrete action steps with exact function signatures and file paths ✓ 100% verifiable acceptance criteria (grep, pytest, vitest, imports) ✓ Architecture aligned with all 11 CONTEXT.md decisions ✓ CLAUDE.md compliance: TypeScript strict, API tests, UI fidelity ✓ Wave dependencies correctly ordered ✓ Risk mitigation: rate limiting, timeout handling, offline graceful degradation Ready for execution via /gsd-execute-phase 4.1
762 lines
27 KiB
Markdown
762 lines
27 KiB
Markdown
# Phase 4.1 Research: AI Prompt Enhancement — Spare Parts Deep Identification
|
|
|
|
**Research Date:** 2026-04-22
|
|
**Scope:** Web scraping implementation, spare-parts classification, AI prompt enhancement, search result parsing, backend/frontend integration, and performance/scalability.
|
|
|
|
---
|
|
|
|
## 1. Web Scraping Best Practices: Python Requests + BeautifulSoup
|
|
|
|
### Key Findings
|
|
|
|
**Approach & Risks:**
|
|
- **Direct Google scraping** is technically feasible but risky: Google actively detects and blocks scrapers with 429 (Too Many Requests) errors, CAPTCHA challenges, and IP bans.
|
|
- **Terms of Service violation**: Google's ToS explicitly forbids scraping search results.
|
|
- **HTML structure volatility**: Google changes CSS selectors and HTML markup frequently, breaking scrapers.
|
|
- **Practical reality**: Direct scraping works for low-volume scenarios (tens of requests/hour) with proper mitigations.
|
|
|
|
**Safer Alternatives:**
|
|
1. **SerpAPI / Similar APIs**: Officially maintained, handles blocking/rotation, but costs money ($5-50/month depending on volume).
|
|
2. **Bing scraping**: Less aggressively blocked than Google, similar HTML structure, viable fallback.
|
|
3. **Manufacturer sites** (Dell, HP, Kingston, Crucial): Most reliable source for spare-part specs.
|
|
4. **GitHub Issues / StackOverflow**: Often contain real-world component usage and specifications.
|
|
|
|
**Recommended Hybrid Approach:**
|
|
- Primary: Search manufacturer specs directly (most accurate).
|
|
- Fallback 1: Bing web search with BeautifulSoup.
|
|
- Fallback 2: Google search (if Bing returns no results).
|
|
- Fallback 3: Return AI-extracted data only (graceful offline degradation).
|
|
|
|
### Rate Limiting Strategies
|
|
|
|
**Implementation:**
|
|
- **Delay between requests**: 2-5 seconds minimum (random jitter recommended).
|
|
- **User-Agent rotation**: Cycle through 10+ realistic User-Agent strings (Chrome, Firefox, Safari across Windows/Mac/Linux).
|
|
- **Exponential backoff**: 1s → 2s → 4s → 8s → fail.
|
|
- **Token bucket algorithm**: Max 0.2 requests/second (1 request per 5 seconds) per IP.
|
|
|
|
**User-Agent Pool (Examples):**
|
|
```
|
|
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Chrome/120.0.0.0)
|
|
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (Firefox/121.0)
|
|
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (Safari/537.36)
|
|
```
|
|
|
|
### Error Handling & Timeout Strategies
|
|
|
|
**HTTP Status Codes:**
|
|
- **429 (Too Many Requests)**: Wait 10 seconds, retry once, then fail gracefully.
|
|
- **403 (Forbidden)**: IP blocked; rotate User-Agent, increase delay, or skip.
|
|
- **500+ (Server Error)**: Retry with exponential backoff.
|
|
- **Timeout (>10s)**: Abort search, return AI data only, log warning.
|
|
|
|
**CAPTCHA Detection:**
|
|
- BeautifulSoup can detect CAPTCHA forms by checking for `<form>` with `recaptcha` keywords.
|
|
- If detected: Abort search immediately, return AI data, log incident.
|
|
|
|
**Latency Profile:**
|
|
- Typical Google request: 2-8 seconds.
|
|
- BeautifulSoup HTML parsing: 100-500ms.
|
|
- Regex spec extraction: 10-50ms.
|
|
- **Total end-to-end: 3-15 seconds (up to 30s with retries).**
|
|
|
|
---
|
|
|
|
## 2. Spare-Parts Classification Strategy
|
|
|
|
### Comprehensive Whitelist
|
|
|
|
**Spare-Part Categories (Include These):**
|
|
- **Memory**: RAM, DRAM, DDR3, DDR4, DDR5, SODIMM, DIMM
|
|
- **Storage**: SSD, NVME, M.2, SATA, HDD, hard drive, solid state drive
|
|
- **Processors**: CPU, processor, APU, GPU, graphics card, discrete GPU
|
|
- **Power**: PSU (power supply unit), adapter, power module (NOT cables/cords)
|
|
- **Expansion Cards**: PCIe, PCI, RAID controller, network card (NIC), graphics card
|
|
- **Cooling**: Heatsink, CPU cooler, thermal solution
|
|
- **Motherboards**: Motherboard, BIOS, chipset
|
|
|
|
**Consumables to Exclude:**
|
|
- Cables: SATA cables, USB cables, Ethernet cables, power cords.
|
|
- Fasteners: Screws, washers, bolts, standoffs.
|
|
- Adhesives/Thermal Materials: Thermal paste, thermal pads, adhesive tapes.
|
|
- Connectors: Plugs, sockets, adapters (unless branded components).
|
|
|
|
**Edge Case: Power Supplies**
|
|
- **Spare part**: "Corsair RM850x 850W Power Supply Unit" (replaceable, has specs).
|
|
- **Consumable**: "6ft Power Cable" or "AC Power Cord" (generic utility item).
|
|
|
|
### Fuzzy Matching Implementation
|
|
|
|
**Strategy:**
|
|
1. **Exact keyword match** (highest priority): Check if extracted Category contains exact whitelist terms (RAM, SSD, CPU, GPU, PSU).
|
|
2. **Fuzzy matching** (Levenshtein distance, 70-80% threshold):
|
|
- "Random Access Memory" → matches "RAM"
|
|
- "Solid State Disk" → matches "SSD"
|
|
3. **Regex patterns** (fallback):
|
|
- `\bRAM\b|\bDRAM\b|\bDDR\d\b` → Memory component.
|
|
- `\bSSD\b|\bNVME\b|\bM\.2\b` → Storage component.
|
|
4. **Exclusion patterns** (reject consumables):
|
|
- `^(cable|cord|fastener|screw|adhesive|thermal paste)$` (case-insensitive).
|
|
|
|
**Scoring System:**
|
|
- Exact match in whitelist: +100 points → **Spare Part**.
|
|
- Fuzzy match >80%: +50 points.
|
|
- Fuzzy match 70-80%: +30 points.
|
|
- Found in consumable exclusion list: -100 points → **Consumable**.
|
|
- **Threshold**: Score ≥ 40 → Spare Part; < 40 → Unknown/Consumable.
|
|
|
|
---
|
|
|
|
## 3. AI Prompt Enhancement: Gemini 2.0 Flash & Claude 3.5 Sonnet
|
|
|
|
### Current State
|
|
- **Gemini prompt**: Located in `backend/ai/prompts/gemini_extraction_prompt.md`.
|
|
- **Claude prompt**: Located in `backend/ai/prompts/claude_extraction_prompt.md`.
|
|
- Both focus on OCR extraction from label images.
|
|
|
|
### Phase 4.1 Enhancements
|
|
|
|
**New Classification Logic to Add:**
|
|
|
|
Insert into both prompts a new section after Category extraction:
|
|
|
|
```
|
|
CLASSIFICATION GUIDE - SPARE PARTS vs CONSUMABLES:
|
|
|
|
Spare Parts (replaceable components that plug into or interface with devices):
|
|
- RAM, DDR memory modules
|
|
- SSDs, NVMe drives, M.2 modules
|
|
- CPUs, GPUs, processors
|
|
- Power supply units (PSU), power modules
|
|
- Expansion cards (PCIe, RAID, NIC)
|
|
- Cooling solutions (heatsinks, coolers)
|
|
- Motherboards
|
|
|
|
NOT Spare Parts (consumables, generic items):
|
|
- Cables (power, SATA, USB, Ethernet)
|
|
- Fasteners (screws, washers, standoffs)
|
|
- Thermal paste, thermal pads, adhesives
|
|
- Connectors, plugs, sockets
|
|
- Generic cords and adapters
|
|
|
|
Decision Tree:
|
|
1. Does the item have a replaceable function in a larger system?
|
|
2. Does it have a manufacturer part number and technical specifications?
|
|
3. Is it described with model/revision information?
|
|
If YES to 2+ questions: SPARE PART
|
|
If item matches consumable examples: CONSUMABLE
|
|
Otherwise: Mark as "uncertain" for human review.
|
|
|
|
Examples:
|
|
✓ "Kingston Fury 16GB DDR4-3200" → Spare Part (RAM)
|
|
✓ "Samsung 970 EVO 1TB NVMe" → Spare Part (SSD)
|
|
✓ "Intel Core i7-12700K" → Spare Part (CPU)
|
|
✗ "6ft SATA Cable" → Consumable (cable)
|
|
✗ "CPU Mounting Hardware Kit" → Consumable (fasteners)
|
|
```
|
|
|
|
### Testing Approach for Prompt Accuracy
|
|
|
|
**Validation Dataset:**
|
|
1. Create 20-30 labeled images of actual spare-parts and consumables.
|
|
2. Test both Gemini and Claude on same dataset.
|
|
3. Measure accuracy of Category classification.
|
|
4. Measure accuracy of Part Number extraction.
|
|
5. Iterate on prompt examples until >95% accuracy on test set.
|
|
|
|
**Field Testing with Users:**
|
|
1. Have 3-5 field users test Phase 4.1 with real items.
|
|
2. Collect feedback on search quality and auto-population accuracy.
|
|
3. Measure time-to-save improvement (before vs. after search integration).
|
|
|
|
---
|
|
|
|
## 4. Search Result Parsing: CSS Selectors & Data Extraction
|
|
|
|
### CSS Selectors for Google Search Results
|
|
|
|
**Standard HTML structure (may change):**
|
|
```
|
|
div.g // Result container
|
|
├── h3 (or a[data-sokoban-click]) // Title
|
|
├── a[href^='http'] // URL link
|
|
├── div.VwiC3b (or similar) // Snippet/description
|
|
└── div.eFM0qc // Display URL
|
|
```
|
|
|
|
**Bing Search Selectors (more stable):**
|
|
```
|
|
li.b_algo // Result container
|
|
├── h2 a // Title + link
|
|
├── p // Snippet
|
|
└── .tMee // Display URL
|
|
```
|
|
|
|
### Spec Extraction from Snippets
|
|
|
|
**Regex Patterns:**
|
|
|
|
```python
|
|
# Memory
|
|
r'\b(\d+)\s*(GB|TB)\s*(DDR\d|DRAM|RAM|SDRAM)'
|
|
# Storage
|
|
r'\b(\d+)\s*(GB|TB)\s*(SSD|NVME|NVMe|M\.2|HDD|SATA)'
|
|
# Processor
|
|
r'(Intel|AMD)\s+([A-Z0-9-]+)\s*(\d+\.\d+\s*GHz)?'
|
|
# Power
|
|
r'\b(\d+)\s*(W|watts?|watt)\s*(power|supply|PSU)'
|
|
# Speed/Latency
|
|
r'(\d+)\s*(MHz|GHz|CAS|Latency)'
|
|
```
|
|
|
|
### Data Extraction Pipeline
|
|
|
|
**Example Input:**
|
|
```
|
|
Title: Kingston Fury 16GB DDR4-3200 RAM Memory Module
|
|
Snippet: Kingston Fury 16GB DDR4 3200MHz CAS Latency 16 - Get superior performance
|
|
with Kingston FURY DDR4 memory. 16GB modules deliver rock-solid stability...
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
{
|
|
"manufacturer": "Kingston",
|
|
"model": "Fury",
|
|
"capacity": "16GB",
|
|
"memory_type": "DDR4",
|
|
"speed": "3200MHz",
|
|
"latency": "CAS 16",
|
|
"confidence": 0.95
|
|
}
|
|
```
|
|
|
|
**Mapping to Item Fields:**
|
|
- `Item.Name`: `[Kingston] [Fury] [16GB] [DDR4-3200]` (cleaned)
|
|
- `Item.Category`: "RAM" (from whitelist match)
|
|
- `Item.Type`: "Memory Module" or "DDR4" (spareable)
|
|
- `Item.Notes`: Full specs: "Kingston Fury 16GB DDR4-3200MHz CAS Latency 16"
|
|
|
|
### Handling Variations & Abbreviations
|
|
|
|
**Common variations to normalize:**
|
|
- "DDR4" ↔ "DDR 4" ↔ "DDR-4"
|
|
- "3200 MHz" ↔ "3200MHz" ↔ "3.2 GHz"
|
|
- "Intel i7" ↔ "Intel Core i7" ↔ "Intel Core™ i7"
|
|
- Manufacturers: "SK Hynix" ↔ "SK Hynix" (normalize spacing)
|
|
|
|
**Confidence scoring:**
|
|
- Exact part number match: +0.2
|
|
- All major specs found: +0.3
|
|
- Manufacturer + model: +0.2
|
|
- Consistency checks (price matches category): +0.25
|
|
|
|
---
|
|
|
|
## 5. Backend Integration Architecture
|
|
|
|
### New Modules to Create
|
|
|
|
1. **`backend/services/spare_parts_search.py`**
|
|
- Main orchestrator service.
|
|
- Public methods: `search_and_extract(part_number, category, timeout=20)`.
|
|
- Returns: `SparePartSearchResult` dataclass.
|
|
|
|
2. **`backend/services/web_scraper.py`**
|
|
- HTTP requests with User-Agent rotation and rate limiting.
|
|
- Methods: `search_google()`, `search_bing()`, `fetch_and_parse_html()`.
|
|
|
|
3. **`backend/services/spec_extractor.py`**
|
|
- Regex parsing and data extraction.
|
|
- Methods: `extract_specs_from_snippet()`, `extract_specs_from_html()`.
|
|
|
|
4. **`backend/config/spare_parts_whitelist.py`**
|
|
- Configurable category whitelist and exclusion patterns.
|
|
- Easy to update without code changes.
|
|
|
|
### Integration Flow in `/api/onboarding/extract`
|
|
|
|
**Current Flow:**
|
|
```
|
|
1. User uploads image
|
|
2. AI extraction (Gemini/Claude)
|
|
3. Return extracted data to frontend
|
|
```
|
|
|
|
**Phase 4.1 New Flow:**
|
|
```
|
|
1. User uploads image
|
|
2. AI extraction (Gemini/Claude)
|
|
3. Check: category in whitelist AND part_number exists?
|
|
YES → Trigger async search
|
|
NO → Return AI data, skip search
|
|
4. Search executes (up to 30s timeout):
|
|
- Try Google search
|
|
- Fallback to Bing if Google fails
|
|
- Parse results, extract specs
|
|
5. Return: {
|
|
ai_data: {...},
|
|
search_results: {...} | null,
|
|
search_status: "success" | "timeout" | "error" | "skipped",
|
|
search_error: string | null
|
|
}
|
|
6. Frontend handles loading state, pre-populates fields
|
|
```
|
|
|
|
### Search Service Pseudocode
|
|
|
|
```python
|
|
async def search_and_extract(
|
|
part_number: str,
|
|
category: str,
|
|
manufacturer: str | None = None,
|
|
timeout: int = 20
|
|
) -> SparePartSearchResult:
|
|
"""
|
|
Search for spare part specs and extract data.
|
|
Returns immediately if timeout exceeded.
|
|
"""
|
|
try:
|
|
# Build search query
|
|
query = f"{part_number} {category} {manufacturer or ''}"
|
|
|
|
# Attempt search with timeout
|
|
with asyncio.timeout(timeout):
|
|
# Try Google first (with rate limiting)
|
|
results = await search_google(query)
|
|
|
|
if not results:
|
|
# Fallback to Bing
|
|
results = await search_bing(query)
|
|
|
|
if not results:
|
|
return SparePartSearchResult(
|
|
status="no_results",
|
|
specs=None,
|
|
error="No search results found"
|
|
)
|
|
|
|
# Parse best result
|
|
specs = extract_specs_from_snippet(results[0])
|
|
|
|
return SparePartSearchResult(
|
|
status="success",
|
|
specs=specs,
|
|
error=None,
|
|
confidence=specs.get("confidence", 0.0)
|
|
)
|
|
|
|
except asyncio.TimeoutError:
|
|
return SparePartSearchResult(
|
|
status="timeout",
|
|
specs=None,
|
|
error="Search exceeded 20s timeout"
|
|
)
|
|
|
|
except Exception as e:
|
|
return SparePartSearchResult(
|
|
status="error",
|
|
specs=None,
|
|
error=str(e)
|
|
)
|
|
```
|
|
|
|
### Rate Limiting Implementation
|
|
|
|
**Token Bucket Algorithm:**
|
|
```python
|
|
class SearchRateLimiter:
|
|
def __init__(self, requests_per_second: float = 0.2):
|
|
# 0.2 req/sec = 1 req per 5 seconds
|
|
self.capacity = 1.0
|
|
self.refill_rate = requests_per_second
|
|
self.tokens = 1.0
|
|
self.last_refill = time.time()
|
|
|
|
async def acquire(self):
|
|
"""Block until search quota available."""
|
|
while self.tokens < 1.0:
|
|
elapsed = time.time() - self.last_refill
|
|
self.tokens += elapsed * self.refill_rate
|
|
self.last_refill = time.time()
|
|
if self.tokens < 1.0:
|
|
await asyncio.sleep(0.1)
|
|
self.tokens -= 1.0
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Frontend AIOnboarding Integration
|
|
|
|
### State Additions
|
|
|
|
```typescript
|
|
interface AIOnboardingState {
|
|
// ... existing state ...
|
|
isSearching: boolean; // Search in progress
|
|
searchError: string | null; // Error message if failed
|
|
searchResults: SparePartSpecs | null; // Extracted specs
|
|
searchTimeout: number; // Configurable timeout (30s default)
|
|
}
|
|
```
|
|
|
|
### UI Flow
|
|
|
|
**Sequence:**
|
|
|
|
1. **User confirms item** after AI extraction review.
|
|
2. **Frontend calls** `POST /api/onboarding/extract` with image.
|
|
3. **Backend returns** `{ai_data, search_results, search_status, search_error}`.
|
|
4. **If search_status = "success"**:
|
|
- Show `"Searching for specifications..."` modal (non-dismissible).
|
|
- Spinner animation + countdown timer.
|
|
- Pre-populate Item.Category, Item.Type, Item.Notes from search results.
|
|
5. **User reviews all fields** (can edit any field).
|
|
6. **User clicks Save** to commit to database.
|
|
|
|
**On Search Error:**
|
|
- Show modal: `"Search failed: [error message]"`
|
|
- Buttons: `[Retry Search] [Skip and Save]`
|
|
- If Retry: Re-trigger search (max 2 retries).
|
|
- If Skip: Use AI-extracted data only.
|
|
|
|
### Loading State Design
|
|
|
|
```tsx
|
|
export function SearchLoadingModal({
|
|
isOpen,
|
|
timeout = 30,
|
|
onTimeout,
|
|
}: Props) {
|
|
const [secondsElapsed, setSecondsElapsed] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const interval = setInterval(() => {
|
|
setSecondsElapsed((prev) => {
|
|
if (prev >= timeout) {
|
|
onTimeout();
|
|
return prev;
|
|
}
|
|
return prev + 1;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [isOpen, timeout, onTimeout]);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
|
|
<div className="bg-white p-8 rounded-lg max-w-md text-center">
|
|
<Spinner className="mx-auto mb-4" />
|
|
<p className="text-lg font-normal mb-2">Searching for specifications...</p>
|
|
<p className="text-sm text-slate-500">
|
|
{secondsElapsed}s / {timeout}s
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Error Handling UI
|
|
|
|
```tsx
|
|
function SearchErrorModal({
|
|
error,
|
|
onRetry,
|
|
onSkip,
|
|
}: Props) {
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
|
|
<div className="bg-white p-8 rounded-lg max-w-md">
|
|
<AlertCircle className="text-rose-500 mb-4 mx-auto" />
|
|
<p className="text-lg font-normal mb-4">Search failed</p>
|
|
<p className="text-sm text-slate-600 mb-6">{error}</p>
|
|
<div className="flex gap-3">
|
|
<button onClick={onRetry} className="flex-1 bg-primary text-white px-4 py-2 rounded">
|
|
Retry Search
|
|
</button>
|
|
<button onClick={onSkip} className="flex-1 border border-slate-300 px-4 py-2 rounded">
|
|
Skip
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Performance & Scalability Analysis
|
|
|
|
### Expected Latency Profile
|
|
|
|
| Component | Duration | Notes |
|
|
|-----------|----------|-------|
|
|
| AI extraction (Gemini/Claude) | 2-5s | Existing, cached |
|
|
| Network request + HTML fetch | 2-8s | Highest variability |
|
|
| HTML parsing (BeautifulSoup) | 100-500ms | |
|
|
| Regex spec extraction | 10-50ms | |
|
|
| **Total end-to-end** | **3-15s** | **Typical 20s with retries** |
|
|
|
|
### Handling Multiple Concurrent Searches
|
|
|
|
**Recommendation: Sequential Processing**
|
|
- Process searches 1 at a time with 5-second delays between.
|
|
- Prevents IP blocking and maintains consistent latency.
|
|
- Max concurrent searches: 2-3 across all users.
|
|
|
|
**Implementation:**
|
|
```python
|
|
# Global search queue
|
|
search_queue: asyncio.Queue = asyncio.Queue()
|
|
|
|
async def process_search_queue():
|
|
"""Background task: process queued searches sequentially."""
|
|
while True:
|
|
search_task = await search_queue.get()
|
|
try:
|
|
await search_and_extract(**search_task)
|
|
finally:
|
|
await asyncio.sleep(5) # Rate limit between searches
|
|
search_queue.task_done()
|
|
```
|
|
|
|
### Offline Graceful Degradation
|
|
|
|
**If no internet / search fails:**
|
|
1. Catch all network exceptions.
|
|
2. Return AI-extracted data only.
|
|
3. Show UI message: `"Offline mode: using AI extraction only"`
|
|
4. User proceeds with AI data (no pre-population from web search).
|
|
5. **Optional:** Queue search for retry when connection restored.
|
|
|
|
### Rate Limiting to Avoid IP Blocks
|
|
|
|
**Per-IP Limits:**
|
|
- Max 20 requests/minute to Google (distributed across all users).
|
|
- Max 10 searches per user per minute.
|
|
|
|
**Backoff Strategy:**
|
|
- First failure: Wait 2 seconds, retry once.
|
|
- Second failure: Wait 10 seconds, mark IP as rate-limited.
|
|
- If rate-limited: Return AI data, skip search for next 5 minutes.
|
|
|
|
**User-Agent Rotation:**
|
|
- Rotate User-Agent on every request (10+ pool).
|
|
- Prevents obvious bot detection.
|
|
|
|
### Caching Strategy
|
|
|
|
**Cache by (part_number, category) for 24 hours:**
|
|
```python
|
|
@cache.cached(timeout=86400, key_prefix="spare_parts_search:")
|
|
async def search_and_extract(part_number: str, category: str) -> SparePartSearchResult:
|
|
# Expensive search operation
|
|
```
|
|
|
|
**Benefits:**
|
|
- Repeated searches for same part (e.g., "16GB RAM DDR4") hit cache.
|
|
- Reduces network load and IP block risk.
|
|
- Improves UX (instant pre-population on cached searches).
|
|
|
|
### Scalability Ceiling
|
|
|
|
**Current Estimate:**
|
|
- Suitable for 50-100 item onboardings per day (10-20 searches/day).
|
|
- Bottleneck: Google's IP blocking at ~20 requests/minute sustained.
|
|
|
|
**To Scale Beyond 100+ Searches/Day:**
|
|
- Switch to **SerpAPI** ($50-200/month for high volume).
|
|
- Implement **proxy rotation** (cost-effective, ~$5-20/month).
|
|
- Use **manufacturer APIs directly** (Crucial, Kingston, Corsair offer product APIs).
|
|
|
|
---
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Frontend (Next.js) │
|
|
│ AIOnboarding Component │
|
|
│ │
|
|
│ [Image Upload] → [AI Extraction] → [Confirm Item] │
|
|
│ │ │
|
|
│ v │
|
|
│ [Show Search Loading Modal] │
|
|
│ "Searching for specs..." (30s max) │
|
|
│ │ │
|
|
│ (on complete/error/timeout) │
|
|
│ │ │
|
|
│ [Pre-populate Fields] ← [Search Results] │
|
|
│ Category / Type / Notes editable │
|
|
│ │ │
|
|
│ v │
|
|
│ [User Reviews & Confirms] │
|
|
│ │ │
|
|
│ v │
|
|
│ POST /api/onboarding/save │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
│ HTTP Request
|
|
v
|
|
┌──────────────────────────────────────────────────────────────┐
|
|
│ Backend (FastAPI) │
|
|
│ │
|
|
│ POST /api/onboarding/extract │
|
|
│ ├─ AI Extract (Gemini/Claude) │
|
|
│ ├─ Check: category in whitelist + part_number? │
|
|
│ └─ If YES: Call spare_parts_search.search_and_extract() │
|
|
│ │ │
|
|
│ v │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ SparePartsSearch Service │ │
|
|
│ │ │ │
|
|
│ │ Rate Limiter (token bucket, 0.2 req/sec) │ │
|
|
│ │ │ │ │
|
|
│ │ v │ │
|
|
│ │ WebScraper (requests + User-Agent rotation) │ │
|
|
│ │ ├─ search_google(query, timeout=10s) │ │
|
|
│ │ └─ search_bing(query) [fallback] │ │
|
|
│ │ │ │ │
|
|
│ │ v │ │
|
|
│ │ SpecExtractor (BeautifulSoup + regex) │ │
|
|
│ │ ├─ Parse HTML → CSS selectors │ │
|
|
│ │ ├─ Extract snippets │ │
|
|
│ │ └─ Regex extraction: specs, manufacturer, etc. │ │
|
|
│ │ │ │
|
|
│ │ Cache (24h): (part_number, category) → specs │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ v │
|
|
│ POST /api/onboarding/save │
|
|
│ ├─ Save AI data + search results to Item │
|
|
│ └─ Log to AuditLog │
|
|
└──────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Risk Mitigation Strategies
|
|
|
|
| Risk | Impact | Mitigation |
|
|
|------|--------|-----------|
|
|
| **Google IP blocking** | Search fails, no specs | Use Bing fallback, implement proxy rotation, cache results |
|
|
| **Network timeout** | Slow UX, user frustration | 30s max timeout, show progress, fallback to AI data |
|
|
| **Parsing failures** (HTML changes) | No spec extraction | Update regex patterns, use manufacturer APIs, human review |
|
|
| **Rate limiting abuse** | Service degradation | Token bucket, per-user limits, exponential backoff |
|
|
| **Search quality issues** | Wrong specs populated | Confidence scoring, human review before save, field editability |
|
|
| **Offline (no internet)** | Feature unavailable | Graceful degradation, return AI data only, queue for retry |
|
|
|
|
---
|
|
|
|
## Testing & Validation Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**File: `backend/tests/test_spare_parts_search.py`**
|
|
|
|
```python
|
|
def test_search_and_extract_success():
|
|
"""Test successful search and spec extraction."""
|
|
result = await search_and_extract(
|
|
part_number="Kingston Fury 16GB",
|
|
category="RAM"
|
|
)
|
|
assert result.status == "success"
|
|
assert result.specs["manufacturer"] == "Kingston"
|
|
assert result.specs["capacity"] == "16GB"
|
|
|
|
def test_search_timeout():
|
|
"""Test graceful timeout handling."""
|
|
result = await search_and_extract(
|
|
part_number="test",
|
|
category="RAM",
|
|
timeout=0.1 # Force timeout
|
|
)
|
|
assert result.status == "timeout"
|
|
assert result.specs is None
|
|
|
|
def test_whitelist_matching():
|
|
"""Test spare-part classification."""
|
|
assert classify_as_spare_part("DDR4 RAM") == True
|
|
assert classify_as_spare_part("CPU 16GB") == True
|
|
assert classify_as_spare_part("Power Cable 6ft") == False
|
|
assert classify_as_spare_part("Thermal Paste") == False
|
|
|
|
def test_spec_extraction_regex():
|
|
"""Test regex patterns for spec extraction."""
|
|
snippet = "Kingston Fury 16GB DDR4-3200 CAS 16"
|
|
specs = extract_specs_from_snippet(snippet, category="RAM")
|
|
assert specs["capacity"] == "16GB"
|
|
assert specs["memory_type"] == "DDR4"
|
|
assert specs["speed"] == "3200"
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
**File: `backend/tests/test_onboarding_with_search.py`**
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_extract_with_search():
|
|
"""Test full onboarding flow with search integration."""
|
|
# Upload image
|
|
response = await client.post(
|
|
"/api/onboarding/extract",
|
|
files={"file": ("test_ram.jpg", image_bytes)},
|
|
data={"mode": "catalog"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["ai_data"]["category"] in ["RAM", "Memory"]
|
|
assert data["search_status"] in ["success", "timeout", "error", "skipped"]
|
|
|
|
if data["search_status"] == "success":
|
|
assert "manufacturer" in data["search_results"]
|
|
assert "specs" in data["search_results"]
|
|
```
|
|
|
|
### Frontend Tests
|
|
|
|
**File: `frontend/components/__tests__/SearchLoadingModal.test.tsx`**
|
|
|
|
```typescript
|
|
describe("SearchLoadingModal", () => {
|
|
it("displays countdown timer", () => {
|
|
render(<SearchLoadingModal isOpen timeout={30} />);
|
|
expect(screen.getByText(/Searching for specifications/)).toBeInTheDocument();
|
|
expect(screen.getByText(/0s \/ 30s/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onTimeout after timeout expires", async () => {
|
|
const onTimeout = vi.fn();
|
|
render(<SearchLoadingModal isOpen timeout={1} onTimeout={onTimeout} />);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
expect(onTimeout).toHaveBeenCalled();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Field Testing with Users
|
|
|
|
1. **Recruit 3-5 power users** (heavy inventory users).
|
|
2. **Phase A (1 week)**: Manual specification lookup (baseline).
|
|
3. **Phase B (1 week)**: Test Phase 4.1 with automatic search.
|
|
4. **Metrics**:
|
|
- Time-to-save per item (before vs. after).
|
|
- Accuracy of auto-populated fields.
|
|
- Number of user edits post-search.
|
|
- Search success rate (not timeout/error).
|
|
5. **Collect feedback**: Desired fallback sources, UX tweaks, edge cases.
|
|
|
|
---
|
|
|
|
## RESEARCH COMPLETE
|
|
|