Spaces:
Sleeping
Sleeping
husseinelsaadi Claude Opus 4.8 commited on
Commit ·
41604f6
1
Parent(s): e0470f3
Revive Codingo: free-CPU deploy, env-driven Qdrant, SQLite fallback, CPU crash fix
Browse files- Dockerfile: CUDA base -> python:3.10-slim CPU; install CPU torch; add spaCy model
- interview_retrieval.py: read Qdrant URL/key from env instead of dead hardcoded cluster
- interview_engine.py: guard unconditional GPU calls that crashed on CPU at import
- app.py: fall back to SQLite when DATABASE_URL is unset; normalise postgres:// scheme
- requirements.txt: drop GPU-only bitsandbytes; add nltk + python-dateutil for resume parser
- add scripts/rebuild_qdrant.py + data/ to rebuild the interview-question vector DB
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .gitignore +6 -0
- Dockerfile +28 -12
- README.md +33 -1
- app.py +11 -2
- backend/services/interview_engine.py +7 -7
- backend/services/interview_retrieval.py +31 -8
- data/merged_dataset.json +0 -0
- data/shuffled_questions.json +0 -0
- requirements.txt +2 -3
- scripts/rebuild_qdrant.py +121 -0
.gitignore
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Python bytecode
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
|
|
|
| 1 |
+
# Local-only archive of old laptop files (never deploy this)
|
| 2 |
+
everything-old-in-my-laptop/
|
| 3 |
+
|
| 4 |
+
# Claude / editor working dirs
|
| 5 |
+
.claude/
|
| 6 |
+
|
| 7 |
# Python bytecode
|
| 8 |
__pycache__/
|
| 9 |
*.py[cod]
|
Dockerfile
CHANGED
|
@@ -1,22 +1,38 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
python3 python3-pip ffmpeg git libsndfile1 \
|
| 10 |
-
# Development tools required to compile native extensions such as llama-cpp-python
|
| 11 |
-
build-essential cmake libopenblas-dev \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
COPY requirements.txt .
|
| 16 |
-
RUN pip install -
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
# Copy
|
| 19 |
COPY . /app
|
| 20 |
WORKDIR /app
|
| 21 |
|
|
|
|
|
|
|
| 22 |
CMD ["python3", "app.py"]
|
|
|
|
| 1 |
+
# CPU-only image for Hugging Face Spaces (free tier).
|
| 2 |
+
# Nothing in the app requires a GPU: the LLM is the Groq API, Whisper runs on
|
| 3 |
+
# CPU, edge-tts is a cloud service, and embeddings use the small MiniLM model.
|
| 4 |
+
FROM python:3.10-slim
|
| 5 |
|
| 6 |
+
ENV OMP_NUM_THREADS=1 \
|
| 7 |
+
DEBIAN_FRONTEND=noninteractive \
|
| 8 |
+
PIP_NO_CACHE_DIR=1 \
|
| 9 |
+
PYTHONUNBUFFERED=1 \
|
| 10 |
+
# Keep all model/cache downloads inside the writable /tmp dir on Spaces.
|
| 11 |
+
HF_HOME=/tmp/huggingface \
|
| 12 |
+
TRANSFORMERS_CACHE=/tmp/huggingface/transformers \
|
| 13 |
+
HUGGINGFACE_HUB_CACHE=/tmp/huggingface/hub
|
| 14 |
|
| 15 |
+
# System libraries: ffmpeg (audio), libsndfile1 (soundfile/librosa),
|
| 16 |
+
# git (some pip installs), and build tools for any source-only wheels.
|
| 17 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 18 |
+
ffmpeg git libsndfile1 build-essential \
|
|
|
|
|
|
|
|
|
|
| 19 |
&& rm -rf /var/lib/apt/lists/*
|
| 20 |
|
| 21 |
+
# Install the CPU build of PyTorch first so the heavy CUDA wheel is never
|
| 22 |
+
# pulled in by transitive dependencies.
|
| 23 |
+
RUN pip install --upgrade pip && \
|
| 24 |
+
pip install torch==2.1.2 --index-url https://download.pytorch.org/whl/cpu
|
| 25 |
+
|
| 26 |
COPY requirements.txt .
|
| 27 |
+
RUN pip install -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Pre-download the small spaCy English model used by the resume parser.
|
| 30 |
+
RUN python -m spacy download en_core_web_sm
|
| 31 |
|
| 32 |
+
# Copy the application code.
|
| 33 |
COPY . /app
|
| 34 |
WORKDIR /app
|
| 35 |
|
| 36 |
+
EXPOSE 7860
|
| 37 |
+
|
| 38 |
CMD ["python3", "app.py"]
|
README.md
CHANGED
|
@@ -1,6 +1,38 @@
|
|
| 1 |
---
|
| 2 |
title: Codingo
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: docker
|
| 4 |
app_file: app.py
|
| 5 |
-
|
| 6 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Codingo
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
---
|
| 10 |
+
|
| 11 |
+
# Codingo — AI-Powered Smart Recruitment System
|
| 12 |
+
|
| 13 |
+
A Flask web app where companies post jobs and candidates apply, then an AI
|
| 14 |
+
interviewer ("LUNA") conducts an automated voice interview and scores the
|
| 15 |
+
candidate.
|
| 16 |
+
|
| 17 |
+
## Required Space secrets
|
| 18 |
+
|
| 19 |
+
Set these under **Settings → Variables and secrets**:
|
| 20 |
+
|
| 21 |
+
| Secret | Purpose |
|
| 22 |
+
| --- | --- |
|
| 23 |
+
| `GROQ_API_KEY` | LLM that generates interview questions and chatbot replies |
|
| 24 |
+
| `QDRANT_API_URL` | URL of the Qdrant cluster holding the interview questions |
|
| 25 |
+
| `QDRANT_API_KEY` | API key for that Qdrant cluster |
|
| 26 |
+
| `DATABASE_URL` | *(optional)* Postgres URL; if unset, the app uses SQLite in /tmp |
|
| 27 |
+
|
| 28 |
+
## Rebuilding the interview-question vector database
|
| 29 |
+
|
| 30 |
+
The `interview_questions` Qdrant collection is built from
|
| 31 |
+
`data/shuffled_questions.json` (4233 Q&A pairs, all-MiniLM-L6-v2, 384-dim,
|
| 32 |
+
cosine):
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
export QDRANT_API_URL="https://<cluster>.qdrant.io"
|
| 36 |
+
export QDRANT_API_KEY="<key>"
|
| 37 |
+
python scripts/rebuild_qdrant.py
|
| 38 |
+
```
|
app.py
CHANGED
|
@@ -64,8 +64,17 @@ app.config['SESSION_COOKIE_SECURE'] = True
|
|
| 64 |
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
|
| 65 |
app.config['REMEMBER_COOKIE_SECURE'] = True
|
| 66 |
|
| 67 |
-
# Configure the database connection
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 70 |
|
| 71 |
# Create necessary directories in writable locations
|
|
|
|
| 64 |
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
|
| 65 |
app.config['REMEMBER_COOKIE_SECURE'] = True
|
| 66 |
|
| 67 |
+
# Configure the database connection. Use DATABASE_URL when provided (e.g. a
|
| 68 |
+
# hosted Postgres set as a Space secret); otherwise fall back to a local
|
| 69 |
+
# SQLite file in the writable /tmp directory so the app runs with zero
|
| 70 |
+
# database setup. Note: on Hugging Face the /tmp filesystem is ephemeral, so
|
| 71 |
+
# SQLite data resets when the Space restarts.
|
| 72 |
+
_database_url = os.getenv("DATABASE_URL") or "sqlite:////tmp/codingo.db"
|
| 73 |
+
# SQLAlchemy expects the 'postgresql://' scheme; some providers hand out
|
| 74 |
+
# 'postgres://', which newer SQLAlchemy rejects. Normalise it.
|
| 75 |
+
if _database_url.startswith("postgres://"):
|
| 76 |
+
_database_url = _database_url.replace("postgres://", "postgresql://", 1)
|
| 77 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = _database_url
|
| 78 |
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 79 |
|
| 80 |
# Create necessary directories in writable locations
|
backend/services/interview_engine.py
CHANGED
|
@@ -30,16 +30,16 @@ try:
|
|
| 30 |
except Exception as e:
|
| 31 |
print("⚠️ Qdrant check failed:", e)
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
if torch.cuda.is_available():
|
| 34 |
print("🔥 CUDA Available")
|
| 35 |
-
print(torch.cuda.get_device_name(0))
|
| 36 |
-
print("cuDNN version:", torch.backends.cudnn.version())
|
|
|
|
| 37 |
else:
|
| 38 |
-
print("❌ CUDA Not Available")
|
| 39 |
-
print("🔥 CUDA:", torch.cuda.is_available())
|
| 40 |
-
print("🧠 GPU:", torch.cuda.get_device_name(0))
|
| 41 |
-
print("💡 cuDNN version:", torch.backends.cudnn.version())
|
| 42 |
-
print("💥 cuDNN enabled:", torch.backends.cudnn.is_available())
|
| 43 |
|
| 44 |
|
| 45 |
|
|
|
|
| 30 |
except Exception as e:
|
| 31 |
print("⚠️ Qdrant check failed:", e)
|
| 32 |
|
| 33 |
+
# Report GPU availability without assuming a GPU is present. Calling
|
| 34 |
+
# torch.cuda.get_device_name(0) on a CPU-only host raises and would crash
|
| 35 |
+
# the import (and therefore the whole app), so guard every GPU-only call.
|
| 36 |
if torch.cuda.is_available():
|
| 37 |
print("🔥 CUDA Available")
|
| 38 |
+
print("🧠 GPU:", torch.cuda.get_device_name(0))
|
| 39 |
+
print("💡 cuDNN version:", torch.backends.cudnn.version())
|
| 40 |
+
print("💥 cuDNN enabled:", torch.backends.cudnn.is_available())
|
| 41 |
else:
|
| 42 |
+
print("❌ CUDA Not Available — running on CPU")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
|
backend/services/interview_retrieval.py
CHANGED
|
@@ -67,15 +67,38 @@ SentenceTransformer = None # type: ignore
|
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
# Qdrant configuration
|
| 69 |
#
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
qdrant_client = None
|
| 80 |
|
| 81 |
# Name of the Qdrant collection containing interview Q&A pairs. Do not
|
|
|
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
# Qdrant configuration
|
| 69 |
#
|
| 70 |
+
# Connection details are read from the environment so the app can point at
|
| 71 |
+
# whichever Qdrant cluster currently holds the ``interview_questions``
|
| 72 |
+
# collection. Set QDRANT_API_URL and QDRANT_API_KEY (e.g. as Hugging Face
|
| 73 |
+
# Space secrets, or in a local .env file). Rebuild the collection from the
|
| 74 |
+
# bundled dataset with ``scripts/rebuild_qdrant.py``.
|
| 75 |
+
import os as _os
|
| 76 |
+
|
| 77 |
+
_qdrant_url = _os.getenv("QDRANT_API_URL")
|
| 78 |
+
_qdrant_key = _os.getenv("QDRANT_API_KEY")
|
| 79 |
+
|
| 80 |
+
# Qdrant's REST API listens on :6333; append it if only the bare host is given.
|
| 81 |
+
if _qdrant_url:
|
| 82 |
+
_qdrant_url = _qdrant_url.rstrip("/")
|
| 83 |
+
if ".qdrant.io" in _qdrant_url and not _qdrant_url.rsplit(":", 1)[-1].isdigit():
|
| 84 |
+
_qdrant_url = _qdrant_url + ":6333"
|
| 85 |
+
|
| 86 |
+
if QdrantClient is not None and _qdrant_url and _qdrant_key:
|
| 87 |
+
try:
|
| 88 |
+
qdrant_client: QdrantClient | None = QdrantClient(
|
| 89 |
+
url=_qdrant_url,
|
| 90 |
+
api_key=_qdrant_key,
|
| 91 |
+
check_compatibility=False,
|
| 92 |
+
)
|
| 93 |
+
except Exception as _exc: # pragma: no cover - network/config issues
|
| 94 |
+
logging.error(f"Failed to initialise Qdrant client: {_exc}")
|
| 95 |
+
qdrant_client = None
|
| 96 |
else:
|
| 97 |
+
if QdrantClient is not None and not (_qdrant_url and _qdrant_key):
|
| 98 |
+
logging.warning(
|
| 99 |
+
"QDRANT_API_URL / QDRANT_API_KEY not set; interview question "
|
| 100 |
+
"retrieval will fall back to default questions."
|
| 101 |
+
)
|
| 102 |
qdrant_client = None
|
| 103 |
|
| 104 |
# Name of the Qdrant collection containing interview Q&A pairs. Do not
|
data/merged_dataset.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/shuffled_questions.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
CHANGED
|
@@ -22,7 +22,6 @@ inputimeout==1.0.4
|
|
| 22 |
evaluate==0.4.5
|
| 23 |
accelerate==0.29.3
|
| 24 |
huggingface_hub==0.20.3
|
| 25 |
-
bitsandbytes
|
| 26 |
faster-whisper==0.10.0
|
| 27 |
edge-tts==6.1.2
|
| 28 |
gunicorn
|
|
@@ -33,9 +32,9 @@ pydub>=0.25.1
|
|
| 33 |
requests>=2.31.0
|
| 34 |
psycopg2-binary
|
| 35 |
matplotlib
|
| 36 |
-
bitsandbytes>=0.41.0
|
| 37 |
pdfminer
|
| 38 |
pdfminer.six
|
| 39 |
python-docx
|
| 40 |
spacy
|
| 41 |
-
|
|
|
|
|
|
| 22 |
evaluate==0.4.5
|
| 23 |
accelerate==0.29.3
|
| 24 |
huggingface_hub==0.20.3
|
|
|
|
| 25 |
faster-whisper==0.10.0
|
| 26 |
edge-tts==6.1.2
|
| 27 |
gunicorn
|
|
|
|
| 32 |
requests>=2.31.0
|
| 33 |
psycopg2-binary
|
| 34 |
matplotlib
|
|
|
|
| 35 |
pdfminer
|
| 36 |
pdfminer.six
|
| 37 |
python-docx
|
| 38 |
spacy
|
| 39 |
+
nltk
|
| 40 |
+
python-dateutil
|
scripts/rebuild_qdrant.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rebuild the Qdrant ``interview_questions`` collection from the local dataset.
|
| 3 |
+
|
| 4 |
+
This recreates, byte-for-byte, the vector database the original app used:
|
| 5 |
+
* collection name : interview_questions
|
| 6 |
+
* vector size : 384 (all-MiniLM-L6-v2)
|
| 7 |
+
* distance : COSINE
|
| 8 |
+
* payload : {"job_role": <lower>, "question": ..., "answer": ...}
|
| 9 |
+
|
| 10 |
+
The original cluster was deleted after inactivity, but every question is
|
| 11 |
+
preserved in ``data/shuffled_questions.json`` (4233 Q&A pairs, 26 roles),
|
| 12 |
+
which is exactly what populated Qdrant in the first place.
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
export QDRANT_API_URL="https://<your-cluster>.qdrant.io:6333"
|
| 16 |
+
export QDRANT_API_KEY="<your-key>"
|
| 17 |
+
python scripts/rebuild_qdrant.py
|
| 18 |
+
|
| 19 |
+
It is safe to re-run: the collection is recreated from scratch each time.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
|
| 27 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
| 28 |
+
|
| 29 |
+
COLLECTION_NAME = "interview_questions"
|
| 30 |
+
VECTOR_SIZE = 384
|
| 31 |
+
DATA_FILE = os.path.join(
|
| 32 |
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 33 |
+
"data",
|
| 34 |
+
"shuffled_questions.json",
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def main() -> int:
|
| 39 |
+
url = os.getenv("QDRANT_API_URL")
|
| 40 |
+
key = os.getenv("QDRANT_API_KEY")
|
| 41 |
+
if not url or not key:
|
| 42 |
+
logging.error(
|
| 43 |
+
"Set QDRANT_API_URL and QDRANT_API_KEY environment variables first."
|
| 44 |
+
)
|
| 45 |
+
return 1
|
| 46 |
+
|
| 47 |
+
# Qdrant cloud URLs need the :6333 REST port; add it if the user pasted
|
| 48 |
+
# the bare hostname from the dashboard.
|
| 49 |
+
if url.endswith("/"):
|
| 50 |
+
url = url[:-1]
|
| 51 |
+
if ".qdrant.io" in url and not url.rsplit(":", 1)[-1].isdigit():
|
| 52 |
+
url = url + ":6333"
|
| 53 |
+
|
| 54 |
+
from qdrant_client import QdrantClient
|
| 55 |
+
from qdrant_client.http.models import Distance, PointStruct, VectorParams
|
| 56 |
+
from sentence_transformers import SentenceTransformer
|
| 57 |
+
|
| 58 |
+
logging.info("Loading dataset from %s", DATA_FILE)
|
| 59 |
+
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
| 60 |
+
rows = json.load(f)
|
| 61 |
+
logging.info("Loaded %d Q&A rows", len(rows))
|
| 62 |
+
|
| 63 |
+
logging.info("Loading embedding model all-MiniLM-L6-v2 (first run downloads it)")
|
| 64 |
+
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
|
| 65 |
+
|
| 66 |
+
client = QdrantClient(url=url, api_key=key, check_compatibility=False, timeout=120)
|
| 67 |
+
|
| 68 |
+
logging.info("Recreating collection '%s' (size=%d, COSINE)", COLLECTION_NAME, VECTOR_SIZE)
|
| 69 |
+
client.recreate_collection(
|
| 70 |
+
collection_name=COLLECTION_NAME,
|
| 71 |
+
vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Build the points. We embed the QUESTION text, exactly like the original
|
| 75 |
+
# notebook, and store role/question/answer in the payload.
|
| 76 |
+
questions, payloads = [], []
|
| 77 |
+
for item in rows:
|
| 78 |
+
try:
|
| 79 |
+
role = item["Job Role"].lower().strip()
|
| 80 |
+
question = item["Questions"].strip()
|
| 81 |
+
answer = item["Answers"].strip()
|
| 82 |
+
except (KeyError, AttributeError):
|
| 83 |
+
continue
|
| 84 |
+
if not question:
|
| 85 |
+
continue
|
| 86 |
+
questions.append(question)
|
| 87 |
+
payloads.append({"job_role": role, "question": question, "answer": answer})
|
| 88 |
+
|
| 89 |
+
logging.info("Embedding %d questions...", len(questions))
|
| 90 |
+
vectors = model.encode(questions, batch_size=128, show_progress_bar=True)
|
| 91 |
+
|
| 92 |
+
batch_size = 64
|
| 93 |
+
total = len(questions)
|
| 94 |
+
for start in range(0, total, batch_size):
|
| 95 |
+
end = min(start + batch_size, total)
|
| 96 |
+
points = [
|
| 97 |
+
PointStruct(id=i, vector=vectors[i].tolist(), payload=payloads[i])
|
| 98 |
+
for i in range(start, end)
|
| 99 |
+
]
|
| 100 |
+
for attempt in range(1, 4):
|
| 101 |
+
try:
|
| 102 |
+
client.upsert(collection_name=COLLECTION_NAME, points=points, wait=True)
|
| 103 |
+
break
|
| 104 |
+
except Exception as exc:
|
| 105 |
+
logging.warning("Batch %d-%d attempt %d failed: %s", start, end, attempt, exc)
|
| 106 |
+
if attempt == 3:
|
| 107 |
+
raise
|
| 108 |
+
logging.info("Uploaded %d/%d", end, total)
|
| 109 |
+
|
| 110 |
+
info = client.get_collection(COLLECTION_NAME)
|
| 111 |
+
logging.info(
|
| 112 |
+
"Done. Collection '%s' now has %s points (distance=%s).",
|
| 113 |
+
COLLECTION_NAME,
|
| 114 |
+
info.points_count,
|
| 115 |
+
info.config.params.vectors.distance,
|
| 116 |
+
)
|
| 117 |
+
return 0
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
sys.exit(main())
|