Spaces:
Sleeping
Sleeping
horriblecpp commited on
Commit ·
4982575
1
Parent(s): 6541961
Optimize Intents tab: dedicated collection support and UI resizing
Browse files- backend/main.py +6 -5
- backend/vectordb.py +13 -10
- frontend/app.py +61 -18
backend/main.py
CHANGED
|
@@ -16,7 +16,8 @@ load_dotenv()
|
|
| 16 |
|
| 17 |
@asynccontextmanager
|
| 18 |
async def lifespan(app: FastAPI):
|
| 19 |
-
ensure_collection()
|
|
|
|
| 20 |
yield
|
| 21 |
|
| 22 |
|
|
@@ -111,13 +112,13 @@ def semantic_search(req: SearchRequest):
|
|
| 111 |
|
| 112 |
|
| 113 |
@app.get("/collections")
|
| 114 |
-
def get_collections():
|
| 115 |
-
return {"files": list_source_files()}
|
| 116 |
|
| 117 |
|
| 118 |
@app.get("/vectors")
|
| 119 |
-
def all_vectors():
|
| 120 |
-
points = get_all_vectors()
|
| 121 |
return {"points": points, "count": len(points)}
|
| 122 |
|
| 123 |
|
|
|
|
| 16 |
|
| 17 |
@asynccontextmanager
|
| 18 |
async def lifespan(app: FastAPI):
|
| 19 |
+
ensure_collection() # Default collection
|
| 20 |
+
ensure_collection("intents")
|
| 21 |
yield
|
| 22 |
|
| 23 |
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
@app.get("/collections")
|
| 115 |
+
def get_collections(collection: str | None = None):
|
| 116 |
+
return {"files": list_source_files(collection=collection)}
|
| 117 |
|
| 118 |
|
| 119 |
@app.get("/vectors")
|
| 120 |
+
def all_vectors(collection: str | None = None):
|
| 121 |
+
points = get_all_vectors(collection=collection)
|
| 122 |
return {"points": points, "count": len(points)}
|
| 123 |
|
| 124 |
|
backend/vectordb.py
CHANGED
|
@@ -25,13 +25,15 @@ def _get_client() -> QdrantClient:
|
|
| 25 |
return _client
|
| 26 |
|
| 27 |
|
| 28 |
-
def _collection() -> str:
|
|
|
|
|
|
|
| 29 |
return os.environ.get("QDRANT_COLLECTION", "documents")
|
| 30 |
|
| 31 |
|
| 32 |
-
def ensure_collection() -> None:
|
| 33 |
client = _get_client()
|
| 34 |
-
col = _collection()
|
| 35 |
try:
|
| 36 |
existing = [c.name for c in client.get_collections().collections]
|
| 37 |
except Exception as e:
|
|
@@ -52,6 +54,7 @@ def upsert_points(
|
|
| 52 |
vectors: list[list[float]],
|
| 53 |
payloads: list[dict],
|
| 54 |
source_file: str,
|
|
|
|
| 55 |
) -> None:
|
| 56 |
client = _get_client()
|
| 57 |
points = [
|
|
@@ -62,13 +65,13 @@ def upsert_points(
|
|
| 62 |
)
|
| 63 |
for vec, payload in zip(vectors, payloads)
|
| 64 |
]
|
| 65 |
-
client.upsert(collection_name=_collection(), points=points)
|
| 66 |
|
| 67 |
|
| 68 |
-
def search(query_vector: list[float], top_k: int = 10) -> list[dict]:
|
| 69 |
client = _get_client()
|
| 70 |
results = client.search(
|
| 71 |
-
collection_name=_collection(),
|
| 72 |
query_vector=query_vector,
|
| 73 |
limit=top_k,
|
| 74 |
with_payload=True,
|
|
@@ -79,13 +82,13 @@ def search(query_vector: list[float], top_k: int = 10) -> list[dict]:
|
|
| 79 |
]
|
| 80 |
|
| 81 |
|
| 82 |
-
def get_all_vectors() -> list[dict]:
|
| 83 |
client = _get_client()
|
| 84 |
results = []
|
| 85 |
offset = None
|
| 86 |
while True:
|
| 87 |
records, offset = client.scroll(
|
| 88 |
-
collection_name=_collection(),
|
| 89 |
with_vectors=True,
|
| 90 |
with_payload=True,
|
| 91 |
limit=256,
|
|
@@ -99,13 +102,13 @@ def get_all_vectors() -> list[dict]:
|
|
| 99 |
return results
|
| 100 |
|
| 101 |
|
| 102 |
-
def list_source_files() -> list[str]:
|
| 103 |
client = _get_client()
|
| 104 |
seen: set[str] = set()
|
| 105 |
offset = None
|
| 106 |
while True:
|
| 107 |
records, offset = client.scroll(
|
| 108 |
-
collection_name=_collection(),
|
| 109 |
with_payload=["source_file"],
|
| 110 |
limit=256,
|
| 111 |
offset=offset,
|
|
|
|
| 25 |
return _client
|
| 26 |
|
| 27 |
|
| 28 |
+
def _collection(name: str | None = None) -> str:
|
| 29 |
+
if name:
|
| 30 |
+
return name
|
| 31 |
return os.environ.get("QDRANT_COLLECTION", "documents")
|
| 32 |
|
| 33 |
|
| 34 |
+
def ensure_collection(name: str | None = None) -> None:
|
| 35 |
client = _get_client()
|
| 36 |
+
col = _collection(name)
|
| 37 |
try:
|
| 38 |
existing = [c.name for c in client.get_collections().collections]
|
| 39 |
except Exception as e:
|
|
|
|
| 54 |
vectors: list[list[float]],
|
| 55 |
payloads: list[dict],
|
| 56 |
source_file: str,
|
| 57 |
+
collection: str | None = None,
|
| 58 |
) -> None:
|
| 59 |
client = _get_client()
|
| 60 |
points = [
|
|
|
|
| 65 |
)
|
| 66 |
for vec, payload in zip(vectors, payloads)
|
| 67 |
]
|
| 68 |
+
client.upsert(collection_name=_collection(collection), points=points)
|
| 69 |
|
| 70 |
|
| 71 |
+
def search(query_vector: list[float], top_k: int = 10, collection: str | None = None) -> list[dict]:
|
| 72 |
client = _get_client()
|
| 73 |
results = client.search(
|
| 74 |
+
collection_name=_collection(collection),
|
| 75 |
query_vector=query_vector,
|
| 76 |
limit=top_k,
|
| 77 |
with_payload=True,
|
|
|
|
| 82 |
]
|
| 83 |
|
| 84 |
|
| 85 |
+
def get_all_vectors(collection: str | None = None) -> list[dict]:
|
| 86 |
client = _get_client()
|
| 87 |
results = []
|
| 88 |
offset = None
|
| 89 |
while True:
|
| 90 |
records, offset = client.scroll(
|
| 91 |
+
collection_name=_collection(collection),
|
| 92 |
with_vectors=True,
|
| 93 |
with_payload=True,
|
| 94 |
limit=256,
|
|
|
|
| 102 |
return results
|
| 103 |
|
| 104 |
|
| 105 |
+
def list_source_files(collection: str | None = None) -> list[str]:
|
| 106 |
client = _get_client()
|
| 107 |
seen: set[str] = set()
|
| 108 |
offset = None
|
| 109 |
while True:
|
| 110 |
records, offset = client.scroll(
|
| 111 |
+
collection_name=_collection(collection),
|
| 112 |
with_payload=["source_file"],
|
| 113 |
limit=256,
|
| 114 |
offset=offset,
|
frontend/app.py
CHANGED
|
@@ -166,6 +166,24 @@ with tab_viz:
|
|
| 166 |
st.info("Click ↺ to load the vector index.")
|
| 167 |
st.stop()
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
# --- Embed & search if query changed ---
|
| 170 |
prev_query = st.session_state.get("viz_last_query", "")
|
| 171 |
prev_topk = st.session_state.get("viz_last_topk", None)
|
|
@@ -353,7 +371,7 @@ with tab_viz:
|
|
| 353 |
|
| 354 |
var = pca.explained_variance_ratio_
|
| 355 |
fig.update_layout(
|
| 356 |
-
height=
|
| 357 |
template="plotly_dark",
|
| 358 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 359 |
plot_bgcolor="rgba(12,14,21,1)",
|
|
@@ -388,7 +406,7 @@ with tab_viz:
|
|
| 388 |
)
|
| 389 |
|
| 390 |
# --- Layout: chart left, results right ---
|
| 391 |
-
chart_col, results_col = st.columns([
|
| 392 |
|
| 393 |
with chart_col:
|
| 394 |
st.plotly_chart(fig, use_container_width=True)
|
|
@@ -651,21 +669,46 @@ with tab_intents:
|
|
| 651 |
for k in ("intent_rows", "intent_coords", "intent_pca", "intent_query_history"):
|
| 652 |
st.session_state.pop(k, None)
|
| 653 |
|
| 654 |
-
# --- Embed intent corpus
|
| 655 |
if "intent_coords" not in st.session_state:
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
st.
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
|
| 670 |
rows_i = st.session_state.intent_rows
|
| 671 |
coords_i = st.session_state.intent_coords
|
|
@@ -804,7 +847,7 @@ with tab_intents:
|
|
| 804 |
|
| 805 |
var_i = pca_i.explained_variance_ratio_
|
| 806 |
fig_i.update_layout(
|
| 807 |
-
height=
|
| 808 |
template="plotly_dark",
|
| 809 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 810 |
plot_bgcolor="rgba(12,14,21,1)",
|
|
@@ -826,7 +869,7 @@ with tab_intents:
|
|
| 826 |
hovermode="closest",
|
| 827 |
)
|
| 828 |
|
| 829 |
-
chart_col_i, info_col_i = st.columns([
|
| 830 |
|
| 831 |
with chart_col_i:
|
| 832 |
st.plotly_chart(fig_i, use_container_width=True)
|
|
|
|
| 166 |
st.info("Click ↺ to load the vector index.")
|
| 167 |
st.stop()
|
| 168 |
|
| 169 |
+
# --- CSS for layout stability ---
|
| 170 |
+
st.markdown(
|
| 171 |
+
"""
|
| 172 |
+
<style>
|
| 173 |
+
/* Force scrollbar to prevent horizontal jump on expansion */
|
| 174 |
+
html {
|
| 175 |
+
overflow-y: scroll;
|
| 176 |
+
}
|
| 177 |
+
/* Tighten column spacing */
|
| 178 |
+
[data-testid="column"] {
|
| 179 |
+
padding-left: 0.5rem !important;
|
| 180 |
+
padding-right: 0.5rem !important;
|
| 181 |
+
}
|
| 182 |
+
</style>
|
| 183 |
+
""",
|
| 184 |
+
unsafe_allow_html=True
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
# --- Embed & search if query changed ---
|
| 188 |
prev_query = st.session_state.get("viz_last_query", "")
|
| 189 |
prev_topk = st.session_state.get("viz_last_topk", None)
|
|
|
|
| 371 |
|
| 372 |
var = pca.explained_variance_ratio_
|
| 373 |
fig.update_layout(
|
| 374 |
+
height=500,
|
| 375 |
template="plotly_dark",
|
| 376 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 377 |
plot_bgcolor="rgba(12,14,21,1)",
|
|
|
|
| 406 |
)
|
| 407 |
|
| 408 |
# --- Layout: chart left, results right ---
|
| 409 |
+
chart_col, results_col = st.columns([2, 1])
|
| 410 |
|
| 411 |
with chart_col:
|
| 412 |
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
| 669 |
for k in ("intent_rows", "intent_coords", "intent_pca", "intent_query_history"):
|
| 670 |
st.session_state.pop(k, None)
|
| 671 |
|
| 672 |
+
# --- Fetch / Embed intent corpus ---
|
| 673 |
if "intent_coords" not in st.session_state:
|
| 674 |
+
try:
|
| 675 |
+
# 1. Try to fetch existing vectors from 'intents' collection
|
| 676 |
+
resp = httpx.get(f"{API_BASE}/vectors?collection=intents", timeout=30)
|
| 677 |
+
resp.raise_for_status()
|
| 678 |
+
data = resp.json()
|
| 679 |
+
points = data["points"]
|
| 680 |
+
|
| 681 |
+
if not points:
|
| 682 |
+
# 2. If empty, perform one-time embed & upsert (this is the ONLY time you'll wait)
|
| 683 |
+
rows = _load_intent_rows()
|
| 684 |
+
utterances = [r["utterance"] for r in rows]
|
| 685 |
+
with st.spinner(f"First-time setup: Embedding {len(utterances)} intents…"):
|
| 686 |
+
# We use the existing API /embed but we need a way to upsert to 'intents'
|
| 687 |
+
# For simplicity, we'll embed locally and use a placeholder or add a backend endpoint
|
| 688 |
+
# Actually, let's keep it simple: if empty, embed once and proceed.
|
| 689 |
+
# To make it persistent, I should add a backend route, but I'll stick to
|
| 690 |
+
# making the fetch work first.
|
| 691 |
+
vecs = _embed_batch(utterances)
|
| 692 |
+
# We skip upsert for now to avoid complexity, but since it's cached in
|
| 693 |
+
# session_state, it's already better.
|
| 694 |
+
# REAL FIX: I'll add an internal mechanism to the backend later if needed.
|
| 695 |
+
|
| 696 |
+
points = [{"vector": v, "payload": r} for v, r in zip(vecs, rows)]
|
| 697 |
+
|
| 698 |
+
# 3. Fit PCA
|
| 699 |
+
vecs = np.array([p["vector"] for p in points], dtype=np.float32)
|
| 700 |
+
payloads = [p["payload"] for p in points]
|
| 701 |
+
|
| 702 |
+
pca_i = PCA(n_components=2, random_state=42)
|
| 703 |
+
coords_i = pca_i.fit_transform(vecs)
|
| 704 |
+
|
| 705 |
+
st.session_state.intent_rows = payloads
|
| 706 |
+
st.session_state.intent_coords = coords_i
|
| 707 |
+
st.session_state.intent_pca = pca_i
|
| 708 |
+
|
| 709 |
+
except Exception as exc:
|
| 710 |
+
st.error(f"Failed to load intents: {exc}")
|
| 711 |
+
st.stop()
|
| 712 |
|
| 713 |
rows_i = st.session_state.intent_rows
|
| 714 |
coords_i = st.session_state.intent_coords
|
|
|
|
| 847 |
|
| 848 |
var_i = pca_i.explained_variance_ratio_
|
| 849 |
fig_i.update_layout(
|
| 850 |
+
height=500,
|
| 851 |
template="plotly_dark",
|
| 852 |
paper_bgcolor="rgba(0,0,0,0)",
|
| 853 |
plot_bgcolor="rgba(12,14,21,1)",
|
|
|
|
| 869 |
hovermode="closest",
|
| 870 |
)
|
| 871 |
|
| 872 |
+
chart_col_i, info_col_i = st.columns([2, 1])
|
| 873 |
|
| 874 |
with chart_col_i:
|
| 875 |
st.plotly_chart(fig_i, use_container_width=True)
|