noman2110014 commited on
Commit
fec93c8
·
1 Parent(s): 5ef8567

initial commit

Browse files
Files changed (4) hide show
  1. app.py +118 -0
  2. prepare_stackoverflow_sample.py +35 -0
  3. requirements.txt +5 -3
  4. search_engine.py +102 -0
app.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit semantic search app for CodeSeek AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ import requests
12
+ import streamlit as st
13
+
14
+ from search_engine import SemanticSearchEngine
15
+
16
+
17
+ # ================= CONFIG =================
18
+ DEFAULT_MODEL = os.getenv("GITHUB_EMBEDDING_MODEL", "openai/text-embedding-3-small")
19
+ API_VERSION = os.getenv("GITHUB_API_VERSION", "2026-03-10")
20
+ BASE_URL = "https://models.github.ai"
21
+ DATASET_PATH = Path("data/stackoverflow_sample_3000.json")
22
+
23
+
24
+ # ================= ERRORS =================
25
+ class GitHubModelsError(RuntimeError):
26
+ pass
27
+
28
+
29
+ # ================= DATASET SETUP =================
30
+ def ensure_dataset():
31
+ if not DATASET_PATH.exists():
32
+ with st.spinner("Preparing dataset (first run only)..."):
33
+ subprocess.run(
34
+ [sys.executable, "prepare_stackoverflow_sample.py"],
35
+ check=True
36
+ )
37
+
38
+
39
+ # ================= ENGINE =================
40
+ @st.cache_resource(show_spinner=False)
41
+ def load_engine() -> SemanticSearchEngine:
42
+ return SemanticSearchEngine(DATASET_PATH)
43
+
44
+
45
+ # ================= EMBEDDING =================
46
+ def get_query_embedding(query: str) -> List[float]:
47
+ token = os.getenv("GITHUB_TOKEN")
48
+ org = os.getenv("GITHUB_ORG")
49
+
50
+ if not token:
51
+ raise GitHubModelsError("Missing GITHUB_TOKEN environment variable.")
52
+
53
+ endpoint = f"{BASE_URL}/inference/embeddings"
54
+ if org:
55
+ endpoint = f"{BASE_URL}/orgs/{org}/inference/embeddings"
56
+
57
+ headers = {
58
+ "Accept": "application/vnd.github+json",
59
+ "Authorization": f"Bearer {token}",
60
+ "X-GitHub-Api-Version": API_VERSION,
61
+ "Content-Type": "application/json",
62
+ }
63
+
64
+ payload = {
65
+ "model": DEFAULT_MODEL,
66
+ "input": query
67
+ }
68
+
69
+ response = requests.post(endpoint, headers=headers, json=payload, timeout=30)
70
+
71
+ if response.status_code >= 400:
72
+ raise GitHubModelsError(f"API Error {response.status_code}: {response.text[:300]}")
73
+
74
+ data = response.json()
75
+ return data["data"][0]["embedding"]
76
+
77
+
78
+ # ================= MAIN APP =================
79
+ def main():
80
+ st.set_page_config(page_title="CodeSeek AI", page_icon="🔎", layout="wide")
81
+
82
+ st.title("🔎 CodeSeek AI")
83
+ st.subheader("Semantic Programming Search")
84
+
85
+ # Ensure dataset exists
86
+ ensure_dataset()
87
+
88
+ query = st.text_area(
89
+ "Ask a programming question:",
90
+ placeholder="e.g. How to declare array in Python?",
91
+ height=120,
92
+ )
93
+
94
+ if not query.strip():
95
+ st.info("Enter a query to begin search.")
96
+ return
97
+
98
+ try:
99
+ with st.spinner("Searching..."):
100
+ engine = load_engine()
101
+ query_embedding = get_query_embedding(query.strip())
102
+ results = engine.search(query_embedding, top_k=5)
103
+
104
+ except Exception as e:
105
+ st.error(f"Search failed: {e}")
106
+ return
107
+
108
+ st.markdown("### Top Results")
109
+
110
+ for i, item in enumerate(results, start=1):
111
+ st.markdown(f"**{i}. {item['question']}**")
112
+ st.write(item["answer"])
113
+ st.caption(f"Similarity score: {item['score']:.4f}")
114
+ st.divider()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
prepare_stackoverflow_sample.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prepare a lightweight semantic search dataset from Hugging Face.
2
+
3
+ Usage:
4
+ python prepare_stackoverflow_sample.py
5
+ """
6
+
7
+ from pathlib import Path
8
+ import json
9
+
10
+ from datasets import load_dataset
11
+
12
+
13
+ DATASET_ID = "MartinElMolon/stackoverflow_preguntas_con_embeddings"
14
+ OUTPUT_PATH = Path("data/stackoverflow_sample_3000.json")
15
+ SAMPLE_SIZE = 3000
16
+
17
+
18
+ def main() -> None:
19
+ ds = load_dataset(DATASET_ID, split="train[:3000]")
20
+
21
+ if len(ds) < SAMPLE_SIZE:
22
+ raise ValueError(f"Dataset has only {len(ds)} rows; expected at least {SAMPLE_SIZE}.")
23
+
24
+ sampled = ds.shuffle(seed=42).select(range(SAMPLE_SIZE))
25
+ sampled = sampled.select_columns(["question", "answer", "embeddings"])
26
+
27
+ OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
28
+ with OUTPUT_PATH.open("w", encoding="utf-8") as f:
29
+ json.dump(sampled.to_list(), f, ensure_ascii=False)
30
+
31
+ print(f"Saved {len(sampled)} rows to {OUTPUT_PATH}")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
1
+ streamlit
2
+ faiss-cpu
3
+ numpy
4
+ requests
5
+ datasets
search_engine.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Semantic vector search engine backed by FAISS.
2
+
3
+ Expected dataset format (JSON array):
4
+ [
5
+ {
6
+ "question": "...",
7
+ "answer": "...",
8
+ "embeddings": [0.1, 0.2, ...]
9
+ },
10
+ ...
11
+ ]
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import List, Dict, Any
19
+
20
+ import faiss
21
+ import numpy as np
22
+
23
+
24
+ DEFAULT_DATASET_PATH = Path("data/stackoverflow_sample_3000.json")
25
+
26
+
27
+ class SemanticSearchEngine:
28
+ """FAISS-based semantic search using cosine similarity via inner product."""
29
+
30
+ def __init__(self, dataset_path: str | Path = DEFAULT_DATASET_PATH) -> None:
31
+ self.dataset_path = Path(dataset_path)
32
+ self.metadata: List[Dict[str, str]] = []
33
+ self.embeddings: np.ndarray
34
+ self.index: faiss.IndexFlatIP
35
+ self._load_and_build()
36
+
37
+ def _load_and_build(self) -> None:
38
+ with self.dataset_path.open("r", encoding="utf-8") as f:
39
+ rows: List[Dict[str, Any]] = json.load(f)
40
+
41
+ if not isinstance(rows, list):
42
+ raise ValueError("Dataset must be a JSON array of objects.")
43
+ if not rows:
44
+ raise ValueError("Dataset is empty; expected at least one row.")
45
+
46
+ self.metadata = [
47
+ {
48
+ "question": row["question"],
49
+ "answer": row["answer"],
50
+ }
51
+ for row in rows
52
+ ]
53
+
54
+ embeddings = np.asarray([row["embeddings"] for row in rows], dtype=np.float32)
55
+ if embeddings.ndim != 2:
56
+ raise ValueError("Embeddings must be a 2D matrix [num_rows, dim].")
57
+
58
+ self.embeddings = self._normalize(embeddings)
59
+
60
+ dim = self.embeddings.shape[1]
61
+ self.index = faiss.IndexFlatIP(dim)
62
+ self.index.add(self.embeddings)
63
+
64
+ @staticmethod
65
+ def _normalize(vectors: np.ndarray) -> np.ndarray:
66
+ """L2-normalize vectors for cosine similarity search via inner product."""
67
+ vectors = np.asarray(vectors, dtype=np.float32)
68
+ norms = np.linalg.norm(vectors, axis=1, keepdims=True)
69
+ norms = np.where(norms == 0.0, 1.0, norms)
70
+ return vectors / norms
71
+
72
+ def search(self, query_embedding: List[float] | np.ndarray, top_k: int = 5) -> List[Dict[str, Any]]:
73
+ """Search nearest neighbors and return question/answer plus similarity score."""
74
+ if top_k <= 0:
75
+ raise ValueError("top_k must be greater than 0.")
76
+
77
+ query = np.asarray(query_embedding, dtype=np.float32).reshape(1, -1)
78
+ if query.shape[1] != self.embeddings.shape[1]:
79
+ raise ValueError(
80
+ f"Query dimension {query.shape[1]} does not match index dimension {self.embeddings.shape[1]}."
81
+ )
82
+
83
+ query = self._normalize(query)
84
+ scores, indices = self.index.search(query, min(top_k, len(self.metadata)))
85
+
86
+ results: List[Dict[str, Any]] = []
87
+ for score, idx in zip(scores[0], indices[0]):
88
+ item = self.metadata[int(idx)]
89
+ results.append(
90
+ {
91
+ "question": item["question"],
92
+ "answer": item["answer"],
93
+ "score": float(score),
94
+ }
95
+ )
96
+ return results
97
+
98
+
99
+ def search(query_embedding: List[float] | np.ndarray, top_k: int = 5) -> List[Dict[str, Any]]:
100
+ """Module-level convenience function using the default dataset path."""
101
+ engine = SemanticSearchEngine(DEFAULT_DATASET_PATH)
102
+ return engine.search(query_embedding=query_embedding, top_k=top_k)