eoinedge commited on
Commit
8c14673
·
verified ·
1 Parent(s): 178a992

Add full Edge Impulse docs RAG assistant (prebuilt index + inference)

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
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
 
1
+ *.faiss filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  *.pkl filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: edgeimpulse/edgeimpulse-docs-qwen-0.5b
4
+ pipeline_tag: text-generation
5
+ library_name: transformers
6
+ tags:
7
+ - edge-impulse
8
+ - rag
9
+ - retrieval-augmented-generation
10
+ - faiss
11
+ - qwen
12
+ - documentation
13
+ - tinyml
14
+ - edge-ai
15
+ ---
16
+
17
+ # Edge Impulse Docs — RAG Assistant
18
+
19
+ A retrieval-augmented assistant for the [Edge Impulse](https://edgeimpulse.com)
20
+ documentation. It grounds every answer in a prebuilt vector index of the docs and
21
+ generates with the small quantized model
22
+ [`edgeimpulse/edgeimpulse-docs-qwen-0.5b`](https://huggingface.co/edgeimpulse/edgeimpulse-docs-qwen-0.5b),
23
+ so it runs comfortably on a laptop.
24
+
25
+ - **Retrieval:** FAISS (inner-product) over `data/index`, embedded with
26
+ `sentence-transformers/all-MiniLM-L6-v2` (384-dim, the same model the index was
27
+ built with).
28
+ - **Generation:** the 0.5B GGUF, served through any OpenAI-compatible endpoint
29
+ (llama.cpp `llama-server` or Ollama). No training stack required.
30
+ - **Grounded + cited:** answers are constrained to the retrieved context and each
31
+ response lists its source documents.
32
+
33
+ This repo ships only what you need to **run** the assistant — the prebuilt index
34
+ and the inference code. The raw document corpus and the index-building pipeline
35
+ are not included.
36
+
37
+ ## Contents
38
+
39
+ | File | Purpose |
40
+ | --- | --- |
41
+ | `data/index/edge_impulse_docs.faiss` | FAISS inner-product index of the docs |
42
+ | `data/index/chunks.pkl` | Chunk text + source metadata (aligned to the index) |
43
+ | `data/index/metadata.json` | Embedding model + index parameters |
44
+ | `rag.py` | Retrieval + grounded generation (CLI + importable) |
45
+ | `serve.py` | Minimal Flask HTTP API (`POST /ask`) |
46
+ | `requirements.txt` | Runtime dependencies |
47
+
48
+ ## Quickstart
49
+
50
+ **1. Install dependencies and download this repo**
51
+
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ hf download edgeimpulse/edgeimpulse-docs-rag --local-dir edgeimpulse-docs-rag
55
+ cd edgeimpulse-docs-rag
56
+ ```
57
+
58
+ **2. Start the generator** (pick one)
59
+
60
+ llama.cpp:
61
+
62
+ ```bash
63
+ hf download edgeimpulse/edgeimpulse-docs-qwen-0.5b qwen-edgeai-q4_k_m.gguf --local-dir .
64
+ llama-server -m qwen-edgeai-q4_k_m.gguf -c 4096 --port 8080 --jinja
65
+ ```
66
+
67
+ Ollama:
68
+
69
+ ```bash
70
+ ollama run hf.co/edgeimpulse/edgeimpulse-docs-qwen-0.5b
71
+ # then point rag.py at Ollama's OpenAI-compatible port:
72
+ export RAG_API_BASE=http://127.0.0.1:11434/v1
73
+ export RAG_MODEL=hf.co/edgeimpulse/edgeimpulse-docs-qwen-0.5b
74
+ ```
75
+
76
+ **3. Ask a question**
77
+
78
+ ```bash
79
+ python rag.py "How do I deploy a model to run on a Linux target as an .eim file?"
80
+ ```
81
+
82
+ Only see what was retrieved (no generation):
83
+
84
+ ```bash
85
+ python rag.py "How do I create an API key?" --no-generate
86
+ ```
87
+
88
+ Serve it over HTTP:
89
+
90
+ ```bash
91
+ python serve.py --host 0.0.0.0 --port 8000
92
+ curl -s localhost:8000/ask -H 'content-type: application/json' \
93
+ -d '{"question": "What is the data forwarder?"}'
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ `rag.py` reads these environment variables (all optional):
99
+
100
+ | Variable | Default | Meaning |
101
+ | --- | --- | --- |
102
+ | `RAG_INDEX_DIR` | `data/index` | Location of the FAISS index + chunks |
103
+ | `RAG_API_BASE` | `http://127.0.0.1:8080/v1` | OpenAI-compatible generation endpoint |
104
+ | `RAG_MODEL` | `edgeimpulse/edgeimpulse-docs-qwen-0.5b` | Model name passed to the endpoint |
105
+
106
+ ## How it works
107
+
108
+ ```
109
+ question ──▶ MiniLM embed ──▶ FAISS top-k ──▶ context + question
110
+
111
+
112
+ edgeimpulse-docs-qwen-0.5b (llama.cpp / Ollama)
113
+
114
+
115
+ grounded answer + cited sources
116
+ ```
117
+
118
+ The generator is a small model, so retrieval quality matters: the assistant is
119
+ most accurate when the right chunk is retrieved, and it may be terse or repeat
120
+ itself on out-of-scope questions. Sampling defaults (`temperature 0.3`,
121
+ `repeat_penalty 1.2`) are tuned to keep it from looping.
122
+
123
+ ## Related
124
+
125
+ - Generator model: [`edgeimpulse/edgeimpulse-docs-qwen-0.5b`](https://huggingface.co/edgeimpulse/edgeimpulse-docs-qwen-0.5b)
126
+ - API-scoped variant: [`edgeimpulse/edgeimpulse-api-docs-rag`](https://huggingface.co/edgeimpulse/edgeimpulse-api-docs-rag)
127
+
128
+ ## License
129
+
130
+ Apache-2.0. Documentation content belongs to Edge Impulse.
data/index/chunks.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7f90aa5667bc03f9743895628228e5a43d186a17d677dfce039f191c65b68a9
3
+ size 922471
data/index/edge_impulse_docs.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7a00fcd8072d979872290f1c180c0c603bd6d01bf47fa34db28399f84a35daab
3
+ size 837165
data/index/metadata.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
3
+ "docs_dir": "data/raw_docs",
4
+ "chunk_count": 545,
5
+ "embedding_dim": 384,
6
+ "chunk_chars": 1800,
7
+ "overlap_chars": 250
8
+ }
rag.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Edge Impulse docs RAG — retrieval + grounded generation.
2
+
3
+ Retrieval: FAISS (inner-product) over the prebuilt index in ``data/index`` using
4
+ the same ``all-MiniLM-L6-v2`` sentence embedder the index was built with.
5
+
6
+ Generation: the published quantized model ``edgeimpulse/edgeimpulse-docs-qwen-0.5b``
7
+ served through any OpenAI-compatible endpoint — e.g. llama.cpp's ``llama-server``
8
+ or Ollama. Only the tiny GGUF is needed for generation, so no training stack is
9
+ required to run this assistant.
10
+
11
+ The raw document corpus and the index-building pipeline are intentionally not
12
+ part of this repository; the prebuilt index is all you need at inference time.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import pickle
20
+ from functools import lru_cache
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import faiss
25
+ import requests
26
+ from sentence_transformers import SentenceTransformer
27
+
28
+ DEFAULT_INDEX_DIR = Path(os.environ.get("RAG_INDEX_DIR", "data/index"))
29
+
30
+ # OpenAI-compatible generation endpoint (llama.cpp `llama-server` or Ollama).
31
+ # llama.cpp : llama-server -m qwen-edgeai-q4_k_m.gguf --port 8080 --jinja
32
+ # ollama : ollama run hf.co/edgeimpulse/edgeimpulse-docs-qwen-0.5b
33
+ DEFAULT_API_BASE = os.environ.get("RAG_API_BASE", "http://127.0.0.1:8080/v1")
34
+ DEFAULT_MODEL = os.environ.get("RAG_MODEL", "edgeimpulse/edgeimpulse-docs-qwen-0.5b")
35
+ DEFAULT_API_KEY = os.environ.get("RAG_API_KEY", "sk-no-key-required")
36
+
37
+ SYSTEM_PROMPT = (
38
+ "You are an Edge Impulse documentation assistant. Answer only from the "
39
+ "provided context. If the context does not contain the answer, say what is "
40
+ "missing and suggest the closest relevant docs source. Be concise."
41
+ )
42
+
43
+
44
+ @lru_cache(maxsize=1)
45
+ def load_retriever(index_dir: str):
46
+ root = Path(index_dir)
47
+ metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8"))
48
+ index = faiss.read_index(str(root / "edge_impulse_docs.faiss"))
49
+ with (root / "chunks.pkl").open("rb") as f:
50
+ chunks = pickle.load(f)
51
+ embedder = SentenceTransformer(metadata["embedding_model"])
52
+ return index, chunks, embedder, metadata
53
+
54
+
55
+ def retrieve(question: str, index_dir: Path = DEFAULT_INDEX_DIR, k: int = 4) -> list[dict[str, Any]]:
56
+ index, chunks, embedder, _ = load_retriever(str(index_dir))
57
+ q_emb = embedder.encode(
58
+ [question], convert_to_numpy=True, normalize_embeddings=True
59
+ ).astype("float32")
60
+ scores, ids = index.search(q_emb, k)
61
+ results: list[dict[str, Any]] = []
62
+ for score, idx in zip(scores[0], ids[0]):
63
+ if idx < 0:
64
+ continue
65
+ record = dict(chunks[int(idx)])
66
+ record["score"] = float(score)
67
+ results.append(record)
68
+ return results
69
+
70
+
71
+ def build_messages(question: str, contexts: list[dict[str, Any]]) -> list[dict[str, str]]:
72
+ context_text = "\n\n".join(
73
+ f"Source: {item['source']}\n{item['text']}" for item in contexts
74
+ )
75
+ user = f"Context:\n{context_text}\n\nQuestion: {question}"
76
+ return [
77
+ {"role": "system", "content": SYSTEM_PROMPT},
78
+ {"role": "user", "content": user},
79
+ ]
80
+
81
+
82
+ def generate(
83
+ messages: list[dict[str, str]],
84
+ api_base: str = DEFAULT_API_BASE,
85
+ model: str = DEFAULT_MODEL,
86
+ api_key: str = DEFAULT_API_KEY,
87
+ max_new_tokens: int = 320,
88
+ ) -> str:
89
+ payload = {
90
+ "model": model,
91
+ "messages": messages,
92
+ "temperature": 0.3,
93
+ "top_p": 0.9,
94
+ "max_tokens": max_new_tokens,
95
+ # Honoured by llama.cpp's server; ignored by backends that don't support it.
96
+ "repeat_penalty": 1.2,
97
+ }
98
+ resp = requests.post(
99
+ f"{api_base.rstrip('/')}/chat/completions",
100
+ headers={"Authorization": f"Bearer {api_key}"},
101
+ json=payload,
102
+ timeout=120,
103
+ )
104
+ resp.raise_for_status()
105
+ return resp.json()["choices"][0]["message"]["content"].strip()
106
+
107
+
108
+ def ask(
109
+ question: str,
110
+ index_dir: Path = DEFAULT_INDEX_DIR,
111
+ k: int = 4,
112
+ max_new_tokens: int = 320,
113
+ no_generate: bool = False,
114
+ api_base: str = DEFAULT_API_BASE,
115
+ model: str = DEFAULT_MODEL,
116
+ ) -> str:
117
+ contexts = retrieve(question, index_dir, k)
118
+ sources = "\n".join(f"- {item['source']} ({item['score']:.3f})" for item in contexts)
119
+ if no_generate:
120
+ return "Retrieved context:\n" + sources
121
+ answer = generate(
122
+ build_messages(question, contexts),
123
+ api_base=api_base,
124
+ model=model,
125
+ max_new_tokens=max_new_tokens,
126
+ )
127
+ return f"{answer}\n\nSources:\n{sources}"
128
+
129
+
130
+ def main() -> None:
131
+ parser = argparse.ArgumentParser(description="Ask the Edge Impulse docs RAG assistant.")
132
+ parser.add_argument("question")
133
+ parser.add_argument("--index-dir", type=Path, default=DEFAULT_INDEX_DIR)
134
+ parser.add_argument("--k", type=int, default=4)
135
+ parser.add_argument("--max-new-tokens", type=int, default=320)
136
+ parser.add_argument("--api-base", default=DEFAULT_API_BASE)
137
+ parser.add_argument("--model", default=DEFAULT_MODEL)
138
+ parser.add_argument("--no-generate", action="store_true", help="Only print retrieved chunks.")
139
+ args = parser.parse_args()
140
+ print(
141
+ ask(
142
+ args.question,
143
+ index_dir=args.index_dir,
144
+ k=args.k,
145
+ max_new_tokens=args.max_new_tokens,
146
+ no_generate=args.no_generate,
147
+ api_base=args.api_base,
148
+ model=args.model,
149
+ )
150
+ )
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ faiss-cpu>=1.8
2
+ sentence-transformers>=2.2
3
+ numpy>=1.24
4
+ requests>=2.31
5
+ flask>=3.0
serve.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal HTTP server for the Edge Impulse docs RAG assistant.
2
+
3
+ python serve.py --host 0.0.0.0 --port 8000
4
+
5
+ POST /ask {"question": "...", "k": 4} -> {"answer": "..."}
6
+ GET /health -> {"ok": true}
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ from pathlib import Path
12
+
13
+ from flask import Flask, jsonify, request
14
+
15
+ from rag import DEFAULT_API_BASE, DEFAULT_INDEX_DIR, DEFAULT_MODEL, ask
16
+
17
+
18
+ def create_app(index_dir: Path, api_base: str, model: str, k: int) -> Flask:
19
+ app = Flask(__name__)
20
+
21
+ @app.get("/health")
22
+ def health():
23
+ return jsonify({"ok": True})
24
+
25
+ @app.post("/ask")
26
+ def ask_route():
27
+ payload = request.get_json(silent=True) or {}
28
+ question = str(payload.get("question", "")).strip()
29
+ if not question:
30
+ return jsonify({"error": "question is required"}), 400
31
+ try:
32
+ answer = ask(
33
+ question,
34
+ index_dir=index_dir,
35
+ k=int(payload.get("k", k)),
36
+ max_new_tokens=int(payload.get("max_new_tokens", 320)),
37
+ api_base=api_base,
38
+ model=model,
39
+ )
40
+ return jsonify({"answer": answer})
41
+ except Exception as exc: # noqa: BLE001 - surface the error to the client
42
+ return jsonify({"error": str(exc)}), 500
43
+
44
+ return app
45
+
46
+
47
+ def main() -> None:
48
+ parser = argparse.ArgumentParser(description="Serve the Edge Impulse docs RAG assistant.")
49
+ parser.add_argument("--host", default="127.0.0.1")
50
+ parser.add_argument("--port", type=int, default=8000)
51
+ parser.add_argument("--index-dir", type=Path, default=DEFAULT_INDEX_DIR)
52
+ parser.add_argument("--api-base", default=DEFAULT_API_BASE)
53
+ parser.add_argument("--model", default=DEFAULT_MODEL)
54
+ parser.add_argument("--k", type=int, default=4)
55
+ args = parser.parse_args()
56
+ app = create_app(args.index_dir, args.api_base, args.model, args.k)
57
+ app.run(host=args.host, port=args.port)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()