Spaces:
Sleeping
Sleeping
Commit ·
df37cdd
0
Parent(s):
first commit
Browse files- .gitattributes +36 -0
- .gitignore +90 -0
- README.md +12 -0
- app/__init__.py +0 -0
- app/agents/__init__.py +0 -0
- app/agents/evidence_agent.py +125 -0
- app/agents/planner_agent.py +43 -0
- app/agents/search_agent.py +115 -0
- app/pipeline/__init__.py +0 -0
- app/pipeline/main_pipeline.py +28 -0
- app/prompts/__init__.py +0 -0
- app/prompts/planner_prompt.py +45 -0
- app/rag/__init__.py +0 -0
- app/rag/build_index.py +26 -0
- app/rag/chunking.py +16 -0
- app/rag/embeddings.py +6 -0
- app/rag/pdf_loader.py +39 -0
- app/rag/vector_store.py +54 -0
- app/scoring/__init__.py +0 -0
- app/scoring/evidence_scoring.py +7 -0
- app/search/__init__.py +0 -0
- app/search/search_tool.py +13 -0
- config/settings.py +28 -0
- data/pdfs/apple_iphone_2019_2024_specs.pdf +3 -0
- data/pdfs/asus_laptops_2020_2024_specs.pdf +3 -0
- data/pdfs/dell_laptops_2020_2024_specs.pdf +3 -0
- data/pdfs/hp_laptops_2020_2024_specs.pdf +3 -0
- data/pdfs/lg_tv_2021_2024_specs.pdf +3 -0
- data/pdfs/playstation_2013_2024_specs.pdf +3 -0
- data/pdfs/samsung_galaxy_2020_2024_specs.pdf +3 -0
- data/pdfs/samsung_tv_2021_2024_specs.pdf +3 -0
- requirements.txt +7 -0
- ui/app.py +100 -0
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==============================
|
| 2 |
+
# Python
|
| 3 |
+
# ==============================
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
*.pyo
|
| 7 |
+
*.pyd
|
| 8 |
+
*.so
|
| 9 |
+
*.egg-info/
|
| 10 |
+
*.egg
|
| 11 |
+
|
| 12 |
+
# ==============================
|
| 13 |
+
# Virtual Environment
|
| 14 |
+
# ==============================
|
| 15 |
+
venv/
|
| 16 |
+
.env/
|
| 17 |
+
.env.*
|
| 18 |
+
|
| 19 |
+
# ==============================
|
| 20 |
+
# OS / Editor files
|
| 21 |
+
# ==============================
|
| 22 |
+
.DS_Store
|
| 23 |
+
Thumbs.db
|
| 24 |
+
.vscode/
|
| 25 |
+
.idea/
|
| 26 |
+
*.swp
|
| 27 |
+
*.swo
|
| 28 |
+
|
| 29 |
+
# ==============================
|
| 30 |
+
# Logs & Runtime Files
|
| 31 |
+
# ==============================
|
| 32 |
+
*.log
|
| 33 |
+
logs/
|
| 34 |
+
*.pid
|
| 35 |
+
|
| 36 |
+
# ==============================
|
| 37 |
+
# Python build artifacts
|
| 38 |
+
# ==============================
|
| 39 |
+
build/
|
| 40 |
+
dist/
|
| 41 |
+
*.whl
|
| 42 |
+
|
| 43 |
+
# ==============================
|
| 44 |
+
# Jupyter / Notebooks
|
| 45 |
+
# ==============================
|
| 46 |
+
.ipynb_checkpoints/
|
| 47 |
+
|
| 48 |
+
# ==============================
|
| 49 |
+
# FAISS / Vector Store Artifacts
|
| 50 |
+
# ==============================
|
| 51 |
+
data/faiss/
|
| 52 |
+
*.faiss
|
| 53 |
+
*.pkl
|
| 54 |
+
|
| 55 |
+
# ==============================
|
| 56 |
+
# Temporary data / caches
|
| 57 |
+
# ==============================
|
| 58 |
+
.cache/
|
| 59 |
+
.tmp/
|
| 60 |
+
.tmp_cache/
|
| 61 |
+
|
| 62 |
+
# ==============================
|
| 63 |
+
# API keys / secrets
|
| 64 |
+
# ==============================
|
| 65 |
+
.env
|
| 66 |
+
config/.env
|
| 67 |
+
config/settings.local.py
|
| 68 |
+
|
| 69 |
+
# ==============================
|
| 70 |
+
# Gradio / UI cache
|
| 71 |
+
# ==============================
|
| 72 |
+
.gradio/
|
| 73 |
+
|
| 74 |
+
# ==============================
|
| 75 |
+
# OS-specific
|
| 76 |
+
# ==============================
|
| 77 |
+
*.DS_Store
|
| 78 |
+
desktop.ini
|
| 79 |
+
|
| 80 |
+
# ==============================
|
| 81 |
+
# Test output
|
| 82 |
+
# ==============================
|
| 83 |
+
.coverage
|
| 84 |
+
htmlcov/
|
| 85 |
+
|
| 86 |
+
# ==============================
|
| 87 |
+
# Ignore downloaded PDFs (optional)
|
| 88 |
+
# Uncomment if you don’t want PDFs tracked
|
| 89 |
+
# ==============================
|
| 90 |
+
# data/pdfs/*
|
README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AccessoryIQ
|
| 3 |
+
emoji: 🏃
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.0
|
| 8 |
+
app_file: ui/app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app/__init__.py
ADDED
|
File without changes
|
app/agents/__init__.py
ADDED
|
File without changes
|
app/agents/evidence_agent.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.rag.vector_store import VectorStore
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class EvidenceAgent:
|
| 5 |
+
"""
|
| 6 |
+
RAG-FIRST Evidence Agent
|
| 7 |
+
- Uses RAG if possible
|
| 8 |
+
- Signals when search fallback is needed
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.vs = VectorStore()
|
| 13 |
+
self.vs.load()
|
| 14 |
+
|
| 15 |
+
def gather(self, category, brand, model, accessory_type):
|
| 16 |
+
rag_chunks = self.vs.search(
|
| 17 |
+
query=f"{brand} {accessory_type} compatible accessories",
|
| 18 |
+
category=category,
|
| 19 |
+
brand=brand
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
supported_models = self._extract_supported_models(rag_chunks)
|
| 23 |
+
|
| 24 |
+
# 🚨 MODEL NOT FOUND → TRIGGER SEARCH
|
| 25 |
+
if not self._model_supported(model, supported_models):
|
| 26 |
+
return {
|
| 27 |
+
"needs_search": True,
|
| 28 |
+
"reason": f"Model '{model}' not found in official documentation"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# Extract accessories from RAG
|
| 32 |
+
result = self._extract_accessories(rag_chunks, model, accessory_type)
|
| 33 |
+
|
| 34 |
+
if not result["recommended"]:
|
| 35 |
+
return {
|
| 36 |
+
"needs_search": True,
|
| 37 |
+
"reason": "No accessories found in official documentation"
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return result
|
| 41 |
+
|
| 42 |
+
# ----------------------------
|
| 43 |
+
# MODEL EXTRACTION
|
| 44 |
+
# ----------------------------
|
| 45 |
+
def _extract_supported_models(self, chunks):
|
| 46 |
+
models = set()
|
| 47 |
+
|
| 48 |
+
for chunk in chunks:
|
| 49 |
+
lines = [l.strip() for l in chunk["text"].splitlines()]
|
| 50 |
+
in_section = False
|
| 51 |
+
|
| 52 |
+
for line in lines:
|
| 53 |
+
if "supported models" in line.lower():
|
| 54 |
+
in_section = True
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
if in_section:
|
| 58 |
+
if not line.startswith(("•", "-")):
|
| 59 |
+
break
|
| 60 |
+
models.add(line.lstrip("•- ").strip().lower())
|
| 61 |
+
|
| 62 |
+
return models
|
| 63 |
+
|
| 64 |
+
def _model_supported(self, model, supported_models):
|
| 65 |
+
model = model.lower()
|
| 66 |
+
return any(model in m or m in model for m in supported_models)
|
| 67 |
+
|
| 68 |
+
# ----------------------------
|
| 69 |
+
# ACCESSORY EXTRACTION
|
| 70 |
+
# ----------------------------
|
| 71 |
+
def _extract_accessories(self, chunks, model, accessory_type):
|
| 72 |
+
SECTION_MAP = {
|
| 73 |
+
"charging": ["charging", "power"],
|
| 74 |
+
"audio": ["audio"],
|
| 75 |
+
"cooling": ["cooling"],
|
| 76 |
+
"display": ["display"],
|
| 77 |
+
"connectivity": ["connectivity"],
|
| 78 |
+
"storage": ["storage"],
|
| 79 |
+
"controller": ["controller"]
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
valid_sections = SECTION_MAP.get(accessory_type.lower(), [])
|
| 83 |
+
|
| 84 |
+
recommended = []
|
| 85 |
+
|
| 86 |
+
for chunk in chunks:
|
| 87 |
+
source = chunk["source_pdf"]
|
| 88 |
+
lines = [l.strip() for l in chunk["text"].splitlines() if l.strip()]
|
| 89 |
+
|
| 90 |
+
current_section = None
|
| 91 |
+
|
| 92 |
+
for line in lines:
|
| 93 |
+
lower = line.lower()
|
| 94 |
+
|
| 95 |
+
for sec in valid_sections:
|
| 96 |
+
if sec in lower:
|
| 97 |
+
current_section = sec
|
| 98 |
+
break
|
| 99 |
+
|
| 100 |
+
if not current_section:
|
| 101 |
+
continue
|
| 102 |
+
|
| 103 |
+
if "compatible" in lower:
|
| 104 |
+
continue
|
| 105 |
+
|
| 106 |
+
if line.startswith(("•", "-", "o")):
|
| 107 |
+
recommended.append({
|
| 108 |
+
"accessory": line.lstrip("•-o ").strip(),
|
| 109 |
+
"reason": "Listed as a compatible accessory in official specifications",
|
| 110 |
+
"sources": [source]
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
return {
|
| 114 |
+
"recommended": self._dedupe(recommended),
|
| 115 |
+
"avoid": []
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
def _dedupe(self, items):
|
| 119 |
+
seen = set()
|
| 120 |
+
out = []
|
| 121 |
+
for item in items:
|
| 122 |
+
if item["accessory"] not in seen:
|
| 123 |
+
seen.add(item["accessory"])
|
| 124 |
+
out.append(item)
|
| 125 |
+
return out
|
app/agents/planner_agent.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class PlannerAgent:
|
| 2 |
+
"""
|
| 3 |
+
PHASE 1 Planner: Deterministic Formatter (Clean Output)
|
| 4 |
+
|
| 5 |
+
- No LLM
|
| 6 |
+
- No hallucination
|
| 7 |
+
- Groups sources cleanly
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def decide(self, evidence, product, use_case):
|
| 11 |
+
if "refusal" in evidence:
|
| 12 |
+
return f"REFUSAL:\n{evidence['refusal']}"
|
| 13 |
+
|
| 14 |
+
recommended = evidence.get("recommended", [])
|
| 15 |
+
avoid = evidence.get("avoid", [])
|
| 16 |
+
|
| 17 |
+
if not recommended:
|
| 18 |
+
return "REFUSAL:\nNo compatible accessories found in official specifications."
|
| 19 |
+
|
| 20 |
+
lines = []
|
| 21 |
+
lines.append("Recommended:")
|
| 22 |
+
|
| 23 |
+
# Collect unique sources
|
| 24 |
+
all_sources = set()
|
| 25 |
+
|
| 26 |
+
for item in recommended:
|
| 27 |
+
lines.append(f"- {item['accessory']}")
|
| 28 |
+
all_sources.update(item.get("sources", []))
|
| 29 |
+
|
| 30 |
+
if avoid:
|
| 31 |
+
lines.append("\nAvoid:")
|
| 32 |
+
for item in avoid:
|
| 33 |
+
lines.append(f"- {item['accessory']}")
|
| 34 |
+
all_sources.update(item.get("sources", []))
|
| 35 |
+
|
| 36 |
+
lines.append("\nSources:")
|
| 37 |
+
for src in sorted(all_sources):
|
| 38 |
+
lines.append(f"- {src}")
|
| 39 |
+
|
| 40 |
+
confidence = min(0.9, 0.6 + 0.1 * len(recommended))
|
| 41 |
+
lines.append(f"\nConfidence: {round(confidence, 2)}")
|
| 42 |
+
|
| 43 |
+
return "\n".join(lines)
|
app/agents/search_agent.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from urllib.parse import urlparse
|
| 3 |
+
from config.settings import settings
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class SearchAgent:
|
| 8 |
+
"""
|
| 9 |
+
Tiered Search Agent with confidence scoring.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
OFFICIAL_DOMAINS = {
|
| 13 |
+
"apple.com",
|
| 14 |
+
"support.apple.com",
|
| 15 |
+
"developer.apple.com",
|
| 16 |
+
"samsung.com",
|
| 17 |
+
"sony.com",
|
| 18 |
+
"playstation.com",
|
| 19 |
+
"dell.com",
|
| 20 |
+
"hp.com",
|
| 21 |
+
"asus.com",
|
| 22 |
+
"nvidia.com",
|
| 23 |
+
"amd.com"
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
COMMUNITY_DOMAINS = {
|
| 27 |
+
"reddit.com",
|
| 28 |
+
"stackoverflow.com",
|
| 29 |
+
"superuser.com",
|
| 30 |
+
"ifixit.com"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
def search(self, product, accessory):
|
| 34 |
+
logging.info(f"[SEARCH] Triggered for: {product} | {accessory}")
|
| 35 |
+
|
| 36 |
+
query = f"{product} {accessory} compatibility"
|
| 37 |
+
|
| 38 |
+
headers = {
|
| 39 |
+
"X-API-KEY": settings.SERPER_API_KEY,
|
| 40 |
+
"Content-Type": "application/json"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
payload = {"q": query, "num": 10}
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
r = requests.post(
|
| 47 |
+
"https://google.serper.dev/search",
|
| 48 |
+
headers=headers,
|
| 49 |
+
json=payload,
|
| 50 |
+
timeout=10
|
| 51 |
+
)
|
| 52 |
+
r.raise_for_status()
|
| 53 |
+
data = r.json()
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logging.error(f"[SEARCH ERROR] {e}")
|
| 56 |
+
return self._refusal()
|
| 57 |
+
|
| 58 |
+
return self._process_results(data)
|
| 59 |
+
|
| 60 |
+
def _process_results(self, data):
|
| 61 |
+
organic = data.get("organic", [])
|
| 62 |
+
|
| 63 |
+
official = []
|
| 64 |
+
community = []
|
| 65 |
+
|
| 66 |
+
for r in organic:
|
| 67 |
+
url = r.get("link", "")
|
| 68 |
+
domain = urlparse(url).netloc.lower()
|
| 69 |
+
|
| 70 |
+
entry = {
|
| 71 |
+
"title": r.get("title", "Accessory"),
|
| 72 |
+
"snippet": r.get("snippet", ""),
|
| 73 |
+
"url": url
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
if any(d in domain for d in self.OFFICIAL_DOMAINS):
|
| 77 |
+
official.append(entry)
|
| 78 |
+
elif any(d in domain for d in self.COMMUNITY_DOMAINS):
|
| 79 |
+
community.append(entry)
|
| 80 |
+
|
| 81 |
+
if official:
|
| 82 |
+
return self._format(
|
| 83 |
+
official,
|
| 84 |
+
confidence=0.9,
|
| 85 |
+
label="Official documentation"
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
if community:
|
| 89 |
+
return self._format(
|
| 90 |
+
community,
|
| 91 |
+
confidence=0.65,
|
| 92 |
+
label="Community-verified sources"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return self._refusal()
|
| 96 |
+
|
| 97 |
+
def _format(self, results, confidence, label):
|
| 98 |
+
lines = ["Recommended:"]
|
| 99 |
+
|
| 100 |
+
for r in results[:3]:
|
| 101 |
+
lines.append(f"- {r['title']}")
|
| 102 |
+
lines.append(f" Reason: {r['snippet']}")
|
| 103 |
+
lines.append(f" Source: {r['url']}")
|
| 104 |
+
|
| 105 |
+
lines.append(f"\nConfidence: {confidence}")
|
| 106 |
+
lines.append(f"Source Type: {label}")
|
| 107 |
+
|
| 108 |
+
return "\n".join(lines)
|
| 109 |
+
|
| 110 |
+
def _refusal(self):
|
| 111 |
+
return (
|
| 112 |
+
"REFUSAL:\n"
|
| 113 |
+
"No reliable official or community evidence found.\n"
|
| 114 |
+
"Confidence: 0.0"
|
| 115 |
+
)
|
app/pipeline/__init__.py
ADDED
|
File without changes
|
app/pipeline/main_pipeline.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.evidence_agent import EvidenceAgent
|
| 2 |
+
from app.agents.search_agent import SearchAgent
|
| 3 |
+
from app.agents.planner_agent import PlannerAgent
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def run_pipeline(category, model, accessory, use_case):
|
| 8 |
+
logging.info(f"[PIPELINE] {category=} {model=} {accessory=}")
|
| 9 |
+
|
| 10 |
+
evidence_agent = EvidenceAgent()
|
| 11 |
+
search_agent = SearchAgent()
|
| 12 |
+
planner = PlannerAgent()
|
| 13 |
+
|
| 14 |
+
evidence = evidence_agent.gather(
|
| 15 |
+
category=category,
|
| 16 |
+
brand=model.split()[0],
|
| 17 |
+
model=model,
|
| 18 |
+
accessory_type=accessory
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# --- RAG FAILS → SEARCH ---
|
| 22 |
+
if evidence.get("needs_search"):
|
| 23 |
+
logging.info("[PIPELINE] RAG failed → invoking SearchAgent")
|
| 24 |
+
return search_agent.search(model, accessory)
|
| 25 |
+
|
| 26 |
+
# --- RAG SUCCESS ---
|
| 27 |
+
logging.info("[PIPELINE] RAG successful → Planner formatting")
|
| 28 |
+
return planner.decide(evidence, model, use_case)
|
app/prompts/__init__.py
ADDED
|
File without changes
|
app/prompts/planner_prompt.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PLANNER_SYSTEM_PROMPT = """
|
| 2 |
+
You are a PRODUCT ACCESSORY INTELLIGENCE PLANNER.
|
| 3 |
+
|
| 4 |
+
You are a STRICT DECISION LAYER.
|
| 5 |
+
You do NOT retrieve data.
|
| 6 |
+
You do NOT invent facts.
|
| 7 |
+
|
| 8 |
+
========================
|
| 9 |
+
MANDATORY RULES
|
| 10 |
+
========================
|
| 11 |
+
|
| 12 |
+
1. You MUST use ONLY the provided evidence.
|
| 13 |
+
2. You MUST recommend ONLY PHYSICAL ACCESSORIES.
|
| 14 |
+
- Examples: charger, cable, adapter, hub, dock, stand
|
| 15 |
+
- NOT allowed: charging, audio, connectivity, display
|
| 16 |
+
3. You MUST NOT guess or generalize.
|
| 17 |
+
4. You MUST NOT merge speculative sources with official specifications.
|
| 18 |
+
5. If official specifications (PDFs) are present, they take priority over web sources.
|
| 19 |
+
6. If evidence is inconsistent, weak, or invalid, you MUST refuse.
|
| 20 |
+
|
| 21 |
+
========================
|
| 22 |
+
OUTPUT FORMAT (ONLY)
|
| 23 |
+
========================
|
| 24 |
+
|
| 25 |
+
Recommended:
|
| 26 |
+
- <Accessory Name>
|
| 27 |
+
Reason: <Evidence-based reason>
|
| 28 |
+
Sources:
|
| 29 |
+
- <PDF name OR URL>
|
| 30 |
+
|
| 31 |
+
Avoid:
|
| 32 |
+
- <Accessory Name>
|
| 33 |
+
Reason: <Evidence-based reason>
|
| 34 |
+
Sources:
|
| 35 |
+
- <PDF name OR URL>
|
| 36 |
+
|
| 37 |
+
Confidence: <number between 0 and 1>
|
| 38 |
+
|
| 39 |
+
========================
|
| 40 |
+
REFUSAL FORMAT
|
| 41 |
+
========================
|
| 42 |
+
|
| 43 |
+
REFUSAL:
|
| 44 |
+
<Clear explanation of why evidence is insufficient or invalid>
|
| 45 |
+
"""
|
app/rag/__init__.py
ADDED
|
File without changes
|
app/rag/build_index.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from app.rag.pdf_loader import load_pdf
|
| 3 |
+
from app.rag.chunking import chunk_text
|
| 4 |
+
from app.rag.vector_store import VectorStore
|
| 5 |
+
|
| 6 |
+
PDF_DIR = "data/pdfs"
|
| 7 |
+
|
| 8 |
+
texts = []
|
| 9 |
+
metadata = []
|
| 10 |
+
|
| 11 |
+
for pdf in os.listdir(PDF_DIR):
|
| 12 |
+
if not pdf.endswith(".pdf"):
|
| 13 |
+
continue
|
| 14 |
+
|
| 15 |
+
pages = load_pdf(os.path.join(PDF_DIR, pdf))
|
| 16 |
+
chunks = chunk_text(pages)
|
| 17 |
+
|
| 18 |
+
for chunk in chunks:
|
| 19 |
+
texts.append(chunk["text"])
|
| 20 |
+
metadata.append(chunk)
|
| 21 |
+
|
| 22 |
+
vs = VectorStore()
|
| 23 |
+
vs.build(texts, metadata)
|
| 24 |
+
vs.save()
|
| 25 |
+
|
| 26 |
+
print("FAISS index built successfully.")
|
app/rag/chunking.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def chunk_text(pages, chunk_size=500, overlap=100):
|
| 2 |
+
chunks = []
|
| 3 |
+
for page in pages:
|
| 4 |
+
text = page["text"]
|
| 5 |
+
start = 0
|
| 6 |
+
while start < len(text):
|
| 7 |
+
end = start + chunk_size
|
| 8 |
+
chunks.append({
|
| 9 |
+
"text": text[start:end],
|
| 10 |
+
"page": page["page"],
|
| 11 |
+
"category": page["category"],
|
| 12 |
+
"brand": page["brand"],
|
| 13 |
+
"source_pdf": page["source_pdf"]
|
| 14 |
+
})
|
| 15 |
+
start += chunk_size - overlap
|
| 16 |
+
return chunks
|
app/rag/embeddings.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sentence_transformers import SentenceTransformer
|
| 2 |
+
|
| 3 |
+
model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 4 |
+
|
| 5 |
+
def embed_texts(texts):
|
| 6 |
+
return model.encode(texts, show_progress_bar=False)
|
app/rag/pdf_loader.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pypdf import PdfReader
|
| 3 |
+
|
| 4 |
+
def extract_metadata_from_filename(filename: str):
|
| 5 |
+
name = filename.lower()
|
| 6 |
+
|
| 7 |
+
if "iphone" in name or "galaxy" in name:
|
| 8 |
+
category = "mobile"
|
| 9 |
+
elif "tv" in name:
|
| 10 |
+
category = "tv"
|
| 11 |
+
elif "laptop" in name:
|
| 12 |
+
category = "laptop"
|
| 13 |
+
elif "playstation" in name:
|
| 14 |
+
category = "gaming"
|
| 15 |
+
else:
|
| 16 |
+
category = "unknown"
|
| 17 |
+
|
| 18 |
+
brand = name.split("_")[0]
|
| 19 |
+
|
| 20 |
+
return {
|
| 21 |
+
"category": category,
|
| 22 |
+
"brand": brand,
|
| 23 |
+
"source_pdf": filename
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_pdf(path: str):
|
| 28 |
+
reader = PdfReader(path)
|
| 29 |
+
filename = os.path.basename(path)
|
| 30 |
+
base_metadata = extract_metadata_from_filename(filename)
|
| 31 |
+
|
| 32 |
+
pages = []
|
| 33 |
+
for i, page in enumerate(reader.pages):
|
| 34 |
+
pages.append({
|
| 35 |
+
"text": page.extract_text(),
|
| 36 |
+
"page": i + 1,
|
| 37 |
+
**base_metadata
|
| 38 |
+
})
|
| 39 |
+
return pages
|
app/rag/vector_store.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import faiss
|
| 2 |
+
import os
|
| 3 |
+
import pickle
|
| 4 |
+
from app.rag.embeddings import embed_texts
|
| 5 |
+
from config.settings import settings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class VectorStore:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.index = None
|
| 11 |
+
self.metadata = []
|
| 12 |
+
|
| 13 |
+
def build(self, texts, metadata):
|
| 14 |
+
vectors = embed_texts(texts)
|
| 15 |
+
dim = vectors.shape[1]
|
| 16 |
+
self.index = faiss.IndexFlatL2(dim)
|
| 17 |
+
self.index.add(vectors)
|
| 18 |
+
self.metadata = metadata
|
| 19 |
+
|
| 20 |
+
def save(self):
|
| 21 |
+
os.makedirs(settings.VECTOR_STORE_PATH, exist_ok=True)
|
| 22 |
+
faiss.write_index(
|
| 23 |
+
self.index,
|
| 24 |
+
os.path.join(settings.VECTOR_STORE_PATH, "index.faiss")
|
| 25 |
+
)
|
| 26 |
+
with open(
|
| 27 |
+
os.path.join(settings.VECTOR_STORE_PATH, "meta.pkl"),
|
| 28 |
+
"wb"
|
| 29 |
+
) as f:
|
| 30 |
+
pickle.dump(self.metadata, f)
|
| 31 |
+
|
| 32 |
+
def load(self):
|
| 33 |
+
self.index = faiss.read_index(
|
| 34 |
+
os.path.join(settings.VECTOR_STORE_PATH, "index.faiss")
|
| 35 |
+
)
|
| 36 |
+
with open(
|
| 37 |
+
os.path.join(settings.VECTOR_STORE_PATH, "meta.pkl"),
|
| 38 |
+
"rb"
|
| 39 |
+
) as f:
|
| 40 |
+
self.metadata = pickle.load(f)
|
| 41 |
+
|
| 42 |
+
def search(self, query, category, brand, k=5):
|
| 43 |
+
vector = embed_texts([query])
|
| 44 |
+
distances, indices = self.index.search(vector, k * 3)
|
| 45 |
+
|
| 46 |
+
results = []
|
| 47 |
+
for idx in indices[0]:
|
| 48 |
+
meta = self.metadata[idx]
|
| 49 |
+
if meta["category"] == category and meta["brand"] == brand:
|
| 50 |
+
results.append(meta)
|
| 51 |
+
if len(results) >= k:
|
| 52 |
+
break
|
| 53 |
+
|
| 54 |
+
return results
|
app/scoring/__init__.py
ADDED
|
File without changes
|
app/scoring/evidence_scoring.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def score_accessory(spec_ok, review_hits, complaints):
|
| 2 |
+
if not spec_ok:
|
| 3 |
+
return 0.0
|
| 4 |
+
|
| 5 |
+
score = (review_hits * 0.3) + 0.5
|
| 6 |
+
score -= complaints * 0.4
|
| 7 |
+
return max(0.0, min(score, 1.0))
|
app/search/__init__.py
ADDED
|
File without changes
|
app/search/search_tool.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from config.settings import settings
|
| 3 |
+
|
| 4 |
+
def search_reviews(query):
|
| 5 |
+
headers = {"X-API-KEY": settings.SERPER_API_KEY}
|
| 6 |
+
payload = {"q": query, "num": 5}
|
| 7 |
+
response = requests.post(
|
| 8 |
+
"https://google.serper.dev/search",
|
| 9 |
+
headers=headers,
|
| 10 |
+
json=payload
|
| 11 |
+
)
|
| 12 |
+
response.raise_for_status()
|
| 13 |
+
return response.json().get("organic", [])
|
config/settings.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
class Settings:
|
| 7 |
+
# OpenRouter
|
| 8 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
| 9 |
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
|
| 10 |
+
|
| 11 |
+
DEFAULT_MODEL = os.getenv(
|
| 12 |
+
"OPENROUTER_MODEL",
|
| 13 |
+
"meta-llama/llama-3-8b-instruct"
|
| 14 |
+
)
|
| 15 |
+
FALLBACK_MODEL = os.getenv(
|
| 16 |
+
"OPENROUTER_FALLBACK_MODEL",
|
| 17 |
+
"mistralai/mixtral-8x7b-instruct"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
TEMPERATURE = 0.0
|
| 21 |
+
|
| 22 |
+
# Search
|
| 23 |
+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
|
| 24 |
+
|
| 25 |
+
# RAG
|
| 26 |
+
VECTOR_STORE_PATH = "data/faiss_index"
|
| 27 |
+
|
| 28 |
+
settings = Settings()
|
data/pdfs/apple_iphone_2019_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f0ac0e0abcc3da17bc9332cf7225c7911b70f866e9d743517d40eebc9ada542d
|
| 3 |
+
size 172101
|
data/pdfs/asus_laptops_2020_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f51fdea020ff5b581eac95feb12a82c452bfc861f10681bb851428f235cd96d4
|
| 3 |
+
size 183398
|
data/pdfs/dell_laptops_2020_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c2fbf5ebe0c1d46b48a4ee34308a48bc59f4d11addc41993f81f1c86cead720a
|
| 3 |
+
size 188906
|
data/pdfs/hp_laptops_2020_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6b9ca907dd8e5f739898797910cd7c1479bf62dfae24641ac2f9c7ffae486779
|
| 3 |
+
size 185209
|
data/pdfs/lg_tv_2021_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:66b5a7b1d29629ef60fe55464ca01cfe1b7acd16e395f5dcb11c8c36a48bb0c0
|
| 3 |
+
size 178286
|
data/pdfs/playstation_2013_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cb93ff8667a762bf1107a25944a61949224288debfa0da94719087d7388781ce
|
| 3 |
+
size 184034
|
data/pdfs/samsung_galaxy_2020_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:62d520a0d912b1d0185345d70cae63a93561091e20c60298c054dd873d65f0b1
|
| 3 |
+
size 172127
|
data/pdfs/samsung_tv_2021_2024_specs.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:558c89e698716d22e55f23301240598917a4f3791a96f6b246e9c8147e555e95
|
| 3 |
+
size 181958
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-dotenv
|
| 2 |
+
requests
|
| 3 |
+
faiss-cpu
|
| 4 |
+
sentence-transformers
|
| 5 |
+
pypdf
|
| 6 |
+
langchain
|
| 7 |
+
gradio
|
ui/app.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from app.pipeline.main_pipeline import run_pipeline
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
logging.basicConfig(
|
| 7 |
+
level=logging.INFO,
|
| 8 |
+
format="%(asctime)s | %(levelname)s | %(message)s"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# --------------------------------------------------
|
| 12 |
+
# Accessory options derived from PDF scope
|
| 13 |
+
# --------------------------------------------------
|
| 14 |
+
ACCESSORY_MAP = {
|
| 15 |
+
"Mobile": ["Charging", "Audio", "Protection", "Connectivity"],
|
| 16 |
+
"Laptop": ["Charging", "Connectivity", "Docking", "Display", "Cooling", "Audio"],
|
| 17 |
+
"TV": ["Display", "Audio", "Mounting", "Power", "Connectivity"],
|
| 18 |
+
"Gaming": ["Controller", "Storage", "Audio", "Display", "Power"]
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# --------------------------------------------------
|
| 23 |
+
# Correct dropdown update (FORCED)
|
| 24 |
+
# --------------------------------------------------
|
| 25 |
+
def update_accessories(category):
|
| 26 |
+
if not category:
|
| 27 |
+
return gr.update(choices=[], value=None)
|
| 28 |
+
|
| 29 |
+
return gr.update(
|
| 30 |
+
choices=ACCESSORY_MAP[category],
|
| 31 |
+
value=ACCESSORY_MAP[category][0], # force selection
|
| 32 |
+
interactive=True
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# --------------------------------------------------
|
| 37 |
+
# Run pipeline (safe)
|
| 38 |
+
# --------------------------------------------------
|
| 39 |
+
def run(category, model, accessory, use_case):
|
| 40 |
+
if not category or not model or not accessory:
|
| 41 |
+
return "REFUSAL:\nPlease select Category, Model, and Accessory Type."
|
| 42 |
+
|
| 43 |
+
return run_pipeline(
|
| 44 |
+
category=category.lower(),
|
| 45 |
+
model=model.strip(),
|
| 46 |
+
accessory=accessory.lower(),
|
| 47 |
+
use_case=use_case.strip() if use_case else ""
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# --------------------------------------------------
|
| 52 |
+
# UI Layout
|
| 53 |
+
# --------------------------------------------------
|
| 54 |
+
with gr.Blocks(title="AccessoryIQ") as demo:
|
| 55 |
+
gr.Markdown("## 🔌 AccessoryIQ – Evidence-Based Accessory Intelligence")
|
| 56 |
+
|
| 57 |
+
category = gr.Dropdown(
|
| 58 |
+
choices=["Mobile", "Laptop", "TV", "Gaming"],
|
| 59 |
+
label="Product Category",
|
| 60 |
+
interactive=True
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
accessory = gr.Dropdown(
|
| 64 |
+
choices=["Select category first"], # IMPORTANT
|
| 65 |
+
label="Accessory Type",
|
| 66 |
+
interactive=True,
|
| 67 |
+
allow_custom_value=False
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
model = gr.Textbox(
|
| 71 |
+
label="Product Model",
|
| 72 |
+
placeholder="e.g., iPhone 15, Dell XPS 13, Samsung QN90C"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
use_case = gr.Textbox(
|
| 76 |
+
label="Use Case (optional)",
|
| 77 |
+
placeholder="e.g., gaming, office work, travel"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
category.change(
|
| 81 |
+
fn=update_accessories,
|
| 82 |
+
inputs=category,
|
| 83 |
+
outputs=accessory
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
output = gr.Textbox(
|
| 87 |
+
label="Accessory Recommendation",
|
| 88 |
+
lines=22,
|
| 89 |
+
interactive=False
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
submit = gr.Button("Get Recommendation")
|
| 93 |
+
|
| 94 |
+
submit.click(
|
| 95 |
+
fn=run,
|
| 96 |
+
inputs=[category, model, accessory, use_case],
|
| 97 |
+
outputs=output
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
demo.launch()
|