A RAG (Retrieval-Augmented Generation) agent that answers questions about OpenStack internals — Nova, Cinder, Neutron, and Keystone. It searches documentation, bug reports, source code, and mailing list archives simultaneously, fuses the results with Reciprocal Rank Fusion (RRF), and generates answers with a local LLM.
| Source | Projects | Format |
|---|---|---|
| RST documentation | nova, cinder, neutron, keystone | git clone → .rst |
| Specs (design docs) | same + -specs repos |
git clone → .rst |
| Release notes | all | releasenotes/notes/*.yaml |
| Launchpad bugs | all | REST API → JSON |
| Source code | all | AST parsing → functions/classes |
| Mailing list | openstack-discuss (since 2022) | .mbox.gz archives |
# 1. Install dependencies
pip install -r requirements.txt
# 2. Collect docs, specs, release notes, and bugs (git clone + Launchpad API)
# Takes ~20-40 minutes, creates openstack_data/
bash collect_openstack.sh
# 3. Split docs and bugs into chunks
python preprocess.py
# 4. Extract functions and classes from source code (AST)
python collect_code.py
# 5. Download and parse openstack-discuss mailing list archives
python collect_mailinglist.py
# 6. Build the vector index (four ChromaDB collections)
python build_index.py
# 7a. Ask questions from the CLI
python agent.py "Why does Nova raise NoValidHost?"
# 7b. Or launch the Gradio web UI at http://localhost:7860
python app.pycollect_openstack.sh ──► preprocess.py ──────────┐
collect_code.py ─────────────────────────────────┼──► build_index.py ──► agent.py / app.py
collect_mailinglist.py ──────────────────────────┘
- The query is embedded with
all-MiniLM-L6-v2(on CPU, to keep VRAM free for the LLM). - Four ChromaDB collections are searched in parallel:
openstack_docs,openstack_bugs,openstack_code,openstack_mail(top-5 from each). - Results are merged with Reciprocal Rank Fusion:
score += 1 / (60 + rank + 1)per collection. - The top-5 fused hits become the context for the LLM (
Qwen/Qwen2.5-1.5B-Instruct, 4-bit quantized on GPU — fits in 4 GB VRAM; float32 on CPU).
openstack_data/
├── nova_docs.json # RST docs + specs + release notes
├── cinder_docs.json
├── neutron_docs.json
├── keystone_docs.json
├── all_docs.json # everything combined
├── nova_bugs.json # Launchpad bugs
├── cinder_bugs.json
├── neutron_bugs.json
├── keystone_bugs.json
├── all_bugs.json
├── chunks.json # docs + bugs chunks (from preprocess.py)
├── code_chunks.json # code chunks (from collect_code.py)
├── mailinglist_chunks.json # mailing list chunks (from collect_mailinglist.py)
└── mbox/ # cached .mbox.gz archives
chroma_db/ # vector DB (after build_index.py)
openstack_repos/ # git clones (safe to delete after collection)
{
"id": "nova_documentation_000042",
"project": "nova",
"type": "documentation",
"source": "doc/source/admin/resize.rst",
"text": "Resizing an instance changes the flavor...",
"chunk_n": 2,
"char_len": 487
}Chunk types: documentation, spec, release_notes, bug_report, code_function, code_class, mailing_list.
Code chunks additionally carry module, name, line_start, line_end; mailing list chunks carry subject.
In collect_openstack.sh:
MAX_BUGS_PER_PROJECT=500— raise to 5000 for the full dataset
In preprocess.py:
CHUNK_SIZE=512— chunk size in charactersOVERLAP=64— overlap between chunks
In collect_code.py:
MIN_CHARS=100— shorter code chunks are dropped; tests and migrations are skipped
In collect_mailinglist.py:
START_YEAR=2022— how far back to download archivesKEYWORDS— only messages matching OpenStack-related keywords are kept
In build_index.py:
EMBED_MODEL— embedding model (default:sentence-transformers/all-MiniLM-L6-v2)BATCH_SIZE=256— embedding batch size
In agent.py:
LLM_GPU/LLM_CPU— LLM model id (default:Qwen/Qwen2.5-1.5B-Instruct)TOP_K=5— results per collectionTOP_FINAL=5— fused results used as LLM contextRRF_K=60— RRF constant