Manisankarrr commited on
Commit
df37cdd
·
0 Parent(s):

first commit

Browse files
.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()