devarshia5 commited on
Commit
5eb1dee
·
verified ·
1 Parent(s): 478d756

Upload 7 files

Browse files
Files changed (7) hide show
  1. .gitignore +5 -2
  2. Dockerfile +40 -62
  3. README.md +62 -5
  4. app.py +208 -0
  5. documents/sample.txt +25 -0
  6. index.html +97 -0
  7. requirements.txt +15 -0
.gitignore CHANGED
@@ -1,2 +1,5 @@
1
- # OpenMemory - IDE/Assistant specific rules
2
- .windsurf\rules\openmemory.md
 
 
 
 
1
+ # Baked into the Docker image at build time — do not commit.
2
+ models/
3
+ .cache/
4
+ __pycache__/
5
+ *.pyc
Dockerfile CHANGED
@@ -1,81 +1,59 @@
1
- # Ornith-1.0-9B on a Hugging Face Space (Docker SDK) — OpenAI-compatible API.
 
 
 
 
2
  #
3
- # This is the CPU build: it runs on the FREE tier (2 vCPU / 16 GB, no GPU).
4
- # A 9B model on CPU is usable, not fast (~5-10 tok/s). For real speed, use GPU
5
- # hardware — see the notes at the bottom of this file.
6
- #
7
- # Speed/robustness choices:
8
- # * llama.cpp OpenAI-compatible server (prebuilt CPU wheel -> fast build).
9
- # * Model GGUF baked into the image -> instant cold start (no re-download,
10
- # which matters because free Spaces have only ephemeral disk).
11
- # * --chat_format chatml -> applies a proper template, which
12
- # kills the repetition loop AND enforces <|im_end|> as a stop (no more
13
- # duplicated answers).
14
 
15
  FROM python:3.11-slim
16
 
17
- # build-essential + cmake so llama-cpp-python compiles from source if no
18
- # prebuilt wheel matches (python:slim has no compiler -> CMake "gcc not found").
19
  RUN apt-get update && apt-get install -y --no-install-recommends \
20
- build-essential cmake git curl ca-certificates \
21
- && rm -rf /var/lib/apt/lists/*
22
 
23
- # Hugging Face Spaces run the container as a non-root user with UID 1000.
24
  RUN useradd -m -u 1000 user
25
  USER user
26
  ENV HOME=/home/user \
27
  PATH=/home/user/.local/bin:$PATH
28
-
29
- # --- Python deps. GGML_NATIVE=OFF -> portable CPU binary (no -march=native),
30
- # so it can't crash with "illegal instruction" if build CPU != runtime CPU. ---
 
 
 
 
 
31
  ENV CMAKE_ARGS="-DGGML_NATIVE=OFF" FORCE_CMAKE=1
32
- RUN pip install --no-cache-dir --user "llama-cpp-python[server]" huggingface_hub
33
 
34
- # --- Model: which GGUF to serve ---------------------------------------------
35
- # Q4_K_M = good quality/size. For a bit more CPU speed swap to ornith-1.0-9b-Q4_0.gguf
36
- # (simpler kernels, faster on CPU) or ornith-1.0-9b-Q3_K_M.gguf (smaller, faster).
37
- # Confirm exact filenames on the repo's "Files" tab before changing.
38
- ENV MODEL_REPO=deepreinforce-ai/Ornith-1.0-9B-GGUF \
39
- MODEL_FILE=ornith-1.0-9b-Q4_K_M.gguf \
40
- MODEL_DIR=/home/user/models
41
 
42
- # Bake the weights into the image so the Space starts instantly.
43
  RUN python -c "from huggingface_hub import hf_hub_download; \
44
- hf_hub_download(repo_id='${MODEL_REPO}', filename='${MODEL_FILE}', local_dir='${MODEL_DIR}')"
 
 
 
 
45
 
46
- # --- Serving params ----------------------------------------------------------
47
- # Free tier = 2 vCPU, so 2 threads. n_ctx kept modest to save CPU RAM.
 
 
48
  ENV N_CTX=8192 \
49
  N_THREADS=2 \
50
- OMP_NUM_THREADS=2
 
51
 
52
- # HF routes external traffic to this single port.
53
  EXPOSE 7860
54
-
55
- # OpenAI-compatible API at http://<space>.hf.space/v1 (chat/completions, models).
56
- CMD python -m llama_cpp.server \
57
- --model ${MODEL_DIR}/${MODEL_FILE} \
58
- --host 0.0.0.0 --port 7860 \
59
- --n_ctx ${N_CTX} \
60
- --n_threads ${N_THREADS} \
61
- --n_batch 512 \
62
- --chat_format chatml
63
-
64
- # -----------------------------------------------------------------------------
65
- # WANT IT ACTUALLY FAST? Free HF has no GPU. Two options:
66
- #
67
- # 1) HF GPU hardware (paid, e.g. "T4 small"): change the base image to a CUDA
68
- # one and install the CUDA wheel, then set --n_gpu_layers -1:
69
- #
70
- # FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04
71
- # ... install python3 ...
72
- # RUN pip install "llama-cpp-python[server]" \
73
- # --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
74
- # CMD python3 -m llama_cpp.server --model ... --n_gpu_layers -1 \
75
- # --host 0.0.0.0 --port 7860 --n_ctx 16384 --chat_format chatml
76
- #
77
- # On a T4 this jumps to ~40-70 tok/s.
78
- #
79
- # 2) ZeroGPU Space (free-ish, needs PRO): that path uses the Gradio SDK +
80
- # @spaces.GPU with transformers/vLLM — NOT this llama.cpp Dockerfile.
81
- # -----------------------------------------------------------------------------
 
1
+ # CPU + FREE-tier RAG Space (Docker SDK).
2
+ # Embeddings : BAAI/bge-small-en-v1.5 (fastembed / ONNX, no torch)
3
+ # Vector DB : FAISS (in-memory)
4
+ # LLM : Qwen2.5-1.5B-Instruct (llama.cpp GGUF, fast on CPU)
5
+ # API : OpenAI-compatible -> /v1/chat/completions (+ web UI at /)
6
  #
7
+ # Everything is CPU-only. No GPU, no paid hardware required.
8
+ # Models are baked into the image so the Space cold-starts instantly.
 
 
 
 
 
 
 
 
 
9
 
10
  FROM python:3.11-slim
11
 
12
+ # build-essential + cmake so llama-cpp-python can compile if no prebuilt wheel
13
+ # matches (python:slim has no compiler otherwise -> CMake "gcc not found").
14
  RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ build-essential cmake git curl ca-certificates && rm -rf /var/lib/apt/lists/*
 
16
 
17
+ # Hugging Face Spaces run the container as non-root UID 1000.
18
  RUN useradd -m -u 1000 user
19
  USER user
20
  ENV HOME=/home/user \
21
  PATH=/home/user/.local/bin:$PATH
22
+ WORKDIR /home/user/app
23
+
24
+ # --- Python deps ---
25
+ # --prefer-binary is the key: it makes pip pick a PREBUILT llama-cpp-python
26
+ # wheel from the CPU index (a ~10s download) instead of compiling the newer
27
+ # source release for minutes. build-essential/cmake stay only as a fallback
28
+ # in the rare case no wheel matches (then CMAKE_ARGS keeps the build portable).
29
+ COPY --chown=user requirements.txt .
30
  ENV CMAKE_ARGS="-DGGML_NATIVE=OFF" FORCE_CMAKE=1
31
+ RUN pip install --no-cache-dir --user --prefer-binary -r requirements.txt
32
 
33
+ # --- Model config ------------------------------------------------------------
34
+ ENV LLM_REPO=Qwen/Qwen2.5-1.5B-Instruct-GGUF \
35
+ LLM_FILE=qwen2.5-1.5b-instruct-q4_k_m.gguf \
36
+ MODEL_DIR=/home/user/models \
37
+ EMBED_MODEL=BAAI/bge-small-en-v1.5 \
38
+ FASTEMBED_CACHE=/home/user/.cache/fastembed \
39
+ HF_HOME=/home/user/.cache/huggingface
40
 
41
+ # Bake the LLM (~1 GB) into the image -> instant cold start.
42
  RUN python -c "from huggingface_hub import hf_hub_download; \
43
+ hf_hub_download(repo_id='${LLM_REPO}', filename='${LLM_FILE}', local_dir='${MODEL_DIR}')"
44
+
45
+ # Bake the embedding model (ONNX ~130 MB) into the image too.
46
+ RUN python -c "import os; from fastembed import TextEmbedding; \
47
+ TextEmbedding(os.environ['EMBED_MODEL'], cache_dir=os.environ['FASTEMBED_CACHE'])"
48
 
49
+ # --- App ---------------------------------------------------------------------
50
+ COPY --chown=user . .
51
+
52
+ # Runtime knobs (free tier = 2 vCPU).
53
  ENV N_CTX=8192 \
54
  N_THREADS=2 \
55
+ TOP_K=4 \
56
+ DOCS_DIR=documents
57
 
 
58
  EXPOSE 7860
59
+ CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,10 +1,67 @@
1
  ---
2
- title: Ornith
3
- emoji: 👀
4
- colorFrom: green
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: CPU RAG Space
3
+ emoji: 🦅
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # CPU RAG Space — Qwen2.5-1.5B + FAISS (free tier, no GPU)
13
+
14
+ A self-contained Retrieval-Augmented Generation service that runs **entirely on
15
+ CPU** and fits the Hugging Face **free tier** (2 vCPU / 16 GB).
16
+
17
+ | Component | What | Why |
18
+ |-----------|------|-----|
19
+ | Embeddings | `BAAI/bge-small-en-v1.5` via `fastembed` (ONNX) | fast on CPU, no PyTorch, ~130 MB |
20
+ | Vector DB | **FAISS** (in-memory) | tiny, instant search |
21
+ | LLM | `Qwen2.5-1.5B-Instruct` GGUF Q4_K_M via llama.cpp | fast on CPU (~12–20 tok/s), ~1 GB |
22
+ | API | OpenAI-compatible `/v1/chat/completions` + web UI | drop-in for any client |
23
+
24
+ Total footprint ≈ 1.5–2 GB RAM. Retrieval adds ~20 ms; the LLM is the only
25
+ real latency.
26
+
27
+ ## Deploy (drag & drop)
28
+
29
+ 1. Create a new Space → **Docker** (blank template).
30
+ 2. Drag **all files in this folder** into the Space repo (keep the structure —
31
+ `documents/` included).
32
+ 3. Push. First build takes a few minutes (it bakes the ~1 GB LLM and the
33
+ embedder into the image so cold starts are instant).
34
+
35
+ ## Use it
36
+
37
+ Web UI: open the Space URL. Upload `.txt`/`.md` files and ask questions.
38
+
39
+ API (OpenAI-compatible):
40
+
41
+ ```python
42
+ from openai import OpenAI
43
+ client = OpenAI(base_url="https://<user>-<space>.hf.space/v1", api_key="x")
44
+ r = client.chat.completions.create(
45
+ model="cpu-rag",
46
+ messages=[{"role": "user", "content": "How does retrieval work here?"}],
47
+ )
48
+ print(r.choices[0].message.content)
49
+ ```
50
+
51
+ Extra endpoints: `POST /ingest` (upload a doc), `GET /stats`.
52
+
53
+ ## Add your own knowledge
54
+
55
+ - Put `.txt`/`.md` files in `documents/` before pushing (indexed at startup), or
56
+ - Upload them at runtime via the UI / `POST /ingest`.
57
+
58
+ > Note: the free tier has ephemeral storage, so runtime-uploaded docs are lost
59
+ > on restart. For a permanent corpus, commit files into `documents/`.
60
+
61
+ ## Swap the model
62
+
63
+ Change these in the `Dockerfile` (confirm exact filenames on the repo's *Files*
64
+ tab):
65
+
66
+ - Faster / smaller: `Qwen/Qwen2.5-0.5B-Instruct-GGUF`
67
+ - Coding-focused: `Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF`
app.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CPU RAG Space — bge-small (fastembed) + FAISS + Qwen2.5-1.5B (llama.cpp),
3
+ served as an OpenAI-compatible API with a small web UI.
4
+
5
+ Everything runs on CPU and fits the Hugging Face free tier (2 vCPU / 16 GB).
6
+ """
7
+
8
+ import glob
9
+ import json
10
+ import os
11
+
12
+ import faiss
13
+ import numpy as np
14
+ from fastapi import FastAPI, File, UploadFile
15
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
16
+ from fastembed import TextEmbedding
17
+ from llama_cpp import Llama
18
+ from pydantic import BaseModel
19
+
20
+ # --------------------------------------------------------------------------- #
21
+ # Config (all overridable via Space "Variables")
22
+ # --------------------------------------------------------------------------- #
23
+ MODEL_DIR = os.environ.get("MODEL_DIR", "models")
24
+ LLM_FILE = os.environ.get("LLM_FILE", "qwen2.5-1.5b-instruct-q4_k_m.gguf")
25
+ LLM_PATH = os.path.join(MODEL_DIR, LLM_FILE)
26
+ EMBED_MODEL = os.environ.get("EMBED_MODEL", "BAAI/bge-small-en-v1.5")
27
+ FASTEMBED_CACHE = os.environ.get("FASTEMBED_CACHE")
28
+ N_CTX = int(os.environ.get("N_CTX", "8192"))
29
+ N_THREADS = int(os.environ.get("N_THREADS", str(os.cpu_count() or 2)))
30
+ TOP_K = int(os.environ.get("TOP_K", "4"))
31
+ DOCS_DIR = os.environ.get("DOCS_DIR", "documents")
32
+
33
+ CHUNK_SIZE = 800 # characters per chunk
34
+ CHUNK_OVERLAP = 120
35
+
36
+ RAG_SYSTEM = (
37
+ "You are a helpful assistant. Answer the user's question using ONLY the "
38
+ "context below. If the answer is not in the context, say you don't know. "
39
+ "Cite the source of each fact in square brackets like [filename].\n\n"
40
+ "Context:\n{context}"
41
+ )
42
+
43
+ # --------------------------------------------------------------------------- #
44
+ # Lazily-initialised singletons
45
+ # --------------------------------------------------------------------------- #
46
+ app = FastAPI(title="CPU RAG Space")
47
+
48
+ _embedder = None
49
+ _llm = None
50
+ _index = None # faiss.IndexFlatIP
51
+ _chunks = [] # list[{"text": str, "source": str}]
52
+
53
+
54
+ def embedder():
55
+ global _embedder
56
+ if _embedder is None:
57
+ _embedder = TextEmbedding(EMBED_MODEL, cache_dir=FASTEMBED_CACHE)
58
+ return _embedder
59
+
60
+
61
+ def embed(texts):
62
+ vecs = np.array(list(embedder().embed(list(texts))), dtype="float32")
63
+ faiss.normalize_L2(vecs) # cosine similarity via inner product
64
+ return vecs
65
+
66
+
67
+ def llm():
68
+ global _llm
69
+ if _llm is None:
70
+ _llm = Llama(model_path=LLM_PATH, n_ctx=N_CTX, n_threads=N_THREADS,
71
+ n_batch=512, verbose=False)
72
+ return _llm
73
+
74
+
75
+ # --------------------------------------------------------------------------- #
76
+ # Indexing / retrieval
77
+ # --------------------------------------------------------------------------- #
78
+ def chunk_text(text, source):
79
+ out, i, n = [], 0, len(text)
80
+ step = max(CHUNK_SIZE - CHUNK_OVERLAP, 1)
81
+ while i < n:
82
+ piece = text[i:i + CHUNK_SIZE].strip()
83
+ if piece:
84
+ out.append({"text": piece, "source": source})
85
+ i += step
86
+ return out
87
+
88
+
89
+ def add_chunks(new_chunks):
90
+ global _index, _chunks
91
+ if not new_chunks:
92
+ return 0
93
+ vecs = embed([c["text"] for c in new_chunks])
94
+ if _index is None:
95
+ _index = faiss.IndexFlatIP(vecs.shape[1])
96
+ _index.add(vecs)
97
+ _chunks.extend(new_chunks)
98
+ return len(new_chunks)
99
+
100
+
101
+ def build_index():
102
+ patterns = ("*.txt", "*.md")
103
+ files = []
104
+ for p in patterns:
105
+ files += glob.glob(os.path.join(DOCS_DIR, "**", p), recursive=True)
106
+ all_chunks = []
107
+ for f in files:
108
+ try:
109
+ with open(f, encoding="utf-8") as fh:
110
+ all_chunks += chunk_text(fh.read(), os.path.basename(f))
111
+ except Exception as exc:
112
+ print(f"[rag] skip {f}: {exc}")
113
+ add_chunks(all_chunks)
114
+
115
+
116
+ def retrieve(query, k=TOP_K):
117
+ if _index is None or _index.ntotal == 0:
118
+ return []
119
+ scores, ids = _index.search(embed([query]), min(k, _index.ntotal))
120
+ hits = []
121
+ for score, idx in zip(scores[0], ids[0]):
122
+ if idx < 0:
123
+ continue
124
+ c = _chunks[idx]
125
+ hits.append({"text": c["text"], "source": c["source"], "score": float(score)})
126
+ return hits
127
+
128
+
129
+ # --------------------------------------------------------------------------- #
130
+ # Startup
131
+ # --------------------------------------------------------------------------- #
132
+ @app.on_event("startup")
133
+ def _startup():
134
+ print("[rag] loading embedder + llm ...")
135
+ embedder()
136
+ llm()
137
+ build_index()
138
+ print(f"[rag] ready. indexed_chunks={len(_chunks)}")
139
+
140
+
141
+ # --------------------------------------------------------------------------- #
142
+ # OpenAI-compatible chat endpoint (with RAG)
143
+ # --------------------------------------------------------------------------- #
144
+ class ChatMessage(BaseModel):
145
+ role: str
146
+ content: str
147
+
148
+
149
+ class ChatRequest(BaseModel):
150
+ model: str = "cpu-rag"
151
+ messages: list[ChatMessage]
152
+ temperature: float = 0.3
153
+ top_p: float = 0.9
154
+ max_tokens: int = 512
155
+ stream: bool = False
156
+ use_rag: bool = True
157
+
158
+
159
+ def _augment(req: ChatRequest):
160
+ msgs = [m.model_dump() for m in req.messages]
161
+ users = [m for m in msgs if m["role"] == "user"]
162
+ query = users[-1]["content"] if users else ""
163
+ ctxs = retrieve(query) if req.use_rag else []
164
+ if ctxs:
165
+ context = "\n\n".join(f"[{c['source']}] {c['text']}" for c in ctxs)
166
+ system = {"role": "system", "content": RAG_SYSTEM.format(context=context)}
167
+ msgs = [system] + [m for m in msgs if m["role"] != "system"]
168
+ return msgs, ctxs
169
+
170
+
171
+ @app.post("/v1/chat/completions")
172
+ def chat_completions(req: ChatRequest):
173
+ messages, ctxs = _augment(req)
174
+ params = dict(messages=messages, temperature=req.temperature, top_p=req.top_p,
175
+ max_tokens=req.max_tokens, stop=["<|im_end|>", "<|endoftext|>"])
176
+
177
+ if req.stream:
178
+ def gen():
179
+ for chunk in llm().create_chat_completion(**params, stream=True):
180
+ yield f"data: {json.dumps(chunk)}\n\n"
181
+ yield "data: [DONE]\n\n"
182
+ return StreamingResponse(gen(), media_type="text/event-stream")
183
+
184
+ resp = llm().create_chat_completion(**params)
185
+ resp["sources"] = [{"source": c["source"], "score": round(c["score"], 3)} for c in ctxs]
186
+ return JSONResponse(resp)
187
+
188
+
189
+ # --------------------------------------------------------------------------- #
190
+ # Ingest / stats / UI
191
+ # --------------------------------------------------------------------------- #
192
+ @app.post("/ingest")
193
+ async def ingest(file: UploadFile = File(...)):
194
+ text = (await file.read()).decode("utf-8", "ignore")
195
+ added = add_chunks(chunk_text(text, file.filename))
196
+ return {"file": file.filename, "added_chunks": added, "total_chunks": len(_chunks)}
197
+
198
+
199
+ @app.get("/stats")
200
+ def stats():
201
+ return {"indexed_chunks": len(_chunks), "embed_model": EMBED_MODEL,
202
+ "llm": LLM_FILE, "n_ctx": N_CTX, "threads": N_THREADS, "top_k": TOP_K}
203
+
204
+
205
+ @app.get("/", response_class=HTMLResponse)
206
+ def home():
207
+ with open(os.path.join(os.path.dirname(__file__), "index.html"), encoding="utf-8") as f:
208
+ return f.read()
documents/sample.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CPU RAG Space — sample knowledge document.
2
+
3
+ This Space is a fully CPU, free-tier Retrieval-Augmented Generation service.
4
+
5
+ Architecture:
6
+ - Embedding model: BAAI/bge-small-en-v1.5, run via fastembed (ONNX). It turns
7
+ text into 384-dimensional vectors and needs no GPU or PyTorch.
8
+ - Vector store: FAISS (IndexFlatIP) holds the document vectors in memory and
9
+ returns the most similar chunks for a query using cosine similarity.
10
+ - Language model: Qwen2.5-1.5B-Instruct in GGUF Q4_K_M form, served by
11
+ llama.cpp. It reads the retrieved chunks and writes a grounded answer.
12
+
13
+ How retrieval works:
14
+ 1. Your question is embedded into a vector.
15
+ 2. FAISS finds the top-K most similar document chunks (default K = 4).
16
+ 3. Those chunks are inserted into the model's system prompt as context.
17
+ 4. The model answers using only that context and cites the source file.
18
+
19
+ Why a small model is fine here:
20
+ RAG moves knowledge out of the model's weights and into the retriever, so the
21
+ model only needs to read and summarise the provided context rather than
22
+ memorise facts. That makes a fast 1.5B model a good fit for CPU serving.
23
+
24
+ Replace this file with your own .txt or .md documents, or upload files at
25
+ runtime through the web UI, and the Space will answer questions about them.
index.html ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>CPU RAG · Qwen2.5-1.5B + FAISS</title>
7
+ <style>
8
+ :root { color-scheme: light dark; --bg:#0f1220; --panel:#1a1e33; --acc:#7c8cff; --mut:#8b90a8; }
9
+ * { box-sizing: border-box; }
10
+ body { margin:0; font-family: system-ui, sans-serif; background:var(--bg); color:#e7e9f3; }
11
+ header { padding:14px 18px; border-bottom:1px solid #2a2f4a; display:flex; gap:12px; align-items:center; }
12
+ header h1 { font-size:15px; margin:0; font-weight:600; }
13
+ header .tag { font-size:11px; color:var(--mut); background:var(--panel); padding:3px 8px; border-radius:20px; }
14
+ main { max-width:820px; margin:0 auto; padding:18px; }
15
+ #chat { display:flex; flex-direction:column; gap:12px; min-height:50vh; }
16
+ .msg { padding:11px 14px; border-radius:12px; max-width:88%; white-space:pre-wrap; line-height:1.45; font-size:14px; }
17
+ .user { align-self:flex-end; background:var(--acc); color:#fff; }
18
+ .bot { align-self:flex-start; background:var(--panel); }
19
+ .src { align-self:flex-start; font-size:11px; color:var(--mut); margin-top:-6px; }
20
+ form { display:flex; gap:8px; margin-top:16px; }
21
+ textarea { flex:1; resize:none; padding:11px; border-radius:10px; border:1px solid #2a2f4a; background:#12152a; color:#e7e9f3; font-size:14px; }
22
+ button { padding:0 16px; border:0; border-radius:10px; background:var(--acc); color:#fff; font-weight:600; cursor:pointer; }
23
+ button:disabled { opacity:.5; cursor:default; }
24
+ .bar { display:flex; gap:10px; align-items:center; margin-bottom:14px; font-size:12px; color:var(--mut); }
25
+ .bar input[type=file] { font-size:12px; color:var(--mut); }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <header>
30
+ <h1>🦅 CPU RAG</h1>
31
+ <span class="tag">Qwen2.5-1.5B · bge-small · FAISS</span>
32
+ <span class="tag" id="stat">…</span>
33
+ </header>
34
+ <main>
35
+ <div class="bar">
36
+ <label>Add a document (.txt/.md):</label>
37
+ <input type="file" id="file" accept=".txt,.md" />
38
+ <button id="up" type="button">Upload</button>
39
+ <span id="upmsg"></span>
40
+ </div>
41
+ <div id="chat"></div>
42
+ <form id="form">
43
+ <textarea id="q" rows="2" placeholder="Ask about your documents…"></textarea>
44
+ <button id="send" type="submit">Send</button>
45
+ </form>
46
+ </main>
47
+ <script>
48
+ const chat = document.getElementById('chat');
49
+ const stat = document.getElementById('stat');
50
+
51
+ function add(cls, text) {
52
+ const d = document.createElement('div');
53
+ d.className = 'msg ' + cls; d.textContent = text;
54
+ chat.appendChild(d); chat.scrollIntoView({block:'end'}); return d;
55
+ }
56
+ async function refreshStats(){
57
+ try { const s = await (await fetch('/stats')).json();
58
+ stat.textContent = s.indexed_chunks + ' chunks indexed'; } catch(e){}
59
+ }
60
+ refreshStats();
61
+
62
+ document.getElementById('up').onclick = async () => {
63
+ const f = document.getElementById('file').files[0];
64
+ if (!f) return;
65
+ document.getElementById('upmsg').textContent = 'uploading…';
66
+ const fd = new FormData(); fd.append('file', f);
67
+ const r = await (await fetch('/ingest', {method:'POST', body:fd})).json();
68
+ document.getElementById('upmsg').textContent = '+' + r.added_chunks + ' chunks';
69
+ refreshStats();
70
+ };
71
+
72
+ document.getElementById('form').onsubmit = async (e) => {
73
+ e.preventDefault();
74
+ const q = document.getElementById('q').value.trim();
75
+ if (!q) return;
76
+ document.getElementById('q').value = '';
77
+ document.getElementById('send').disabled = true;
78
+ add('user', q);
79
+ const thinking = add('bot', '…');
80
+ try {
81
+ const r = await fetch('/v1/chat/completions', {
82
+ method:'POST', headers:{'Content-Type':'application/json'},
83
+ body: JSON.stringify({ messages:[{role:'user', content:q}], temperature:0.3 })
84
+ });
85
+ const data = await r.json();
86
+ thinking.textContent = data.choices?.[0]?.message?.content ?? '(no answer)';
87
+ if (data.sources && data.sources.length) {
88
+ add('src', 'sources: ' + data.sources.map(s => s.source + ' (' + s.score + ')').join(', '));
89
+ }
90
+ } catch (err) {
91
+ thinking.textContent = 'Error: ' + err;
92
+ }
93
+ document.getElementById('send').disabled = false;
94
+ };
95
+ </script>
96
+ </body>
97
+ </html>
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Try a prebuilt CPU wheel first (fast); if none matches, the Dockerfile's
2
+ # build-essential/cmake lets it compile from source instead of failing.
3
+ --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
4
+ llama-cpp-python
5
+
6
+ # Lightweight CPU embeddings via ONNX (no PyTorch -> small image, fast).
7
+ fastembed
8
+ faiss-cpu
9
+
10
+ # API + serving
11
+ fastapi
12
+ uvicorn[standard]
13
+ python-multipart
14
+ huggingface_hub
15
+ numpy