A RAG-powered chatbot for OpenAPI / Swagger documentation. Ingest any API spec by URL, then ask natural-language questions about it. Supports multiple indexes, persistent chat history, and an embeddable widget for third-party sites.
┌─────────────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ React Frontend │ ───► │ FastAPI (api.py) │ ───► │ Elasticsearch│
│ Vite dev server │ │ port 8000 │ │ port 9200 │
│ port 5173 │ │ │ └──────────────┘
└─────────────────────┘ │ SQLite (chat_history │
│ .db) │ ┌──────────────┐
│ │ ───► │ OpenAI API │
└──────────────────────┘ │ gpt-4.1-mini│
└──────────────┘
Ingest flow: URL → fetch spec → parse YAML/JSON → extract endpoints → embed + store in Elasticsearch index (dense vector field)
Query flow: user message → embed query → Elasticsearch kNN vector search (top-k hits) → inject context into LLM prompt → stream answer
| Requirement | Version |
|---|---|
| Python | 3.11+ |
| Node.js | 18+ |
| Elasticsearch | 8.10.x |
| OpenAI API key | — |
# Docker Compose (recommended)
docker compose up -d elasticsearch
# Or run directly
docker run -d --name elasticsearch \
-p 9200:9200 \
-e discovery.type=single-node \
-e xpack.security.enabled=false \
-e xpack.security.http.ssl.enabled=false \
docker.elastic.co/elasticsearch/elasticsearch:8.10.2# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Set required environment variables
export OPENAI_API_KEY=sk-...
export ES_URL=http://localhost:9200 # default
export SQLITE_PATH=chat_history.db # default
export CORS_ORIGINS=http://localhost:5173 # comma-separated allow-list
# Start the API server
uvicorn api:app --reload --port 8000cd frontend
npm install
npm run dev # starts at http://localhost:5173Open http://localhost:5173 in a browser.
Go to the Ingest tab, paste a Swagger / OpenAPI URL (YAML or JSON), and click Ingest. Each spec gets its own Elasticsearch index. Ingestion runs in the background; the status bar and index table update automatically.
Select an index from the dropdown in the Chat tab and ask questions in plain English. Answers include code examples, endpoint paths, request/response shapes, and HTTP methods drawn from the real spec.
Conversations are persisted in SQLite (chat_history.db) and listed in the sidebar. Click any past conversation to resume it; hover to reveal the delete button.
Drop a single <script> tag onto any web page to add a floating chat button:
<script
src="https://your-app.com/embed.js"
data-api-url="https://your-app.com"
data-index="my-api"
data-primary-color="#6B21E8"
data-position="bottom-right">
</script>| Attribute | Default | Description |
|---|---|---|
data-api-url |
current origin | Base URL of the deployed Primzel app |
data-index |
— | Pre-select an Elasticsearch index; omit to show a picker inside the widget |
data-primary-color |
#6B21E8 |
Accent colour for the button and bubbles |
data-position |
bottom-right |
bottom-right or bottom-left |
The widget renders in a sandboxed <iframe> (widget.html) so its styles never conflict with the host page. The iframe loads lazily — not until the user first opens it.
Local development: use data-api-url="http://localhost:5173" so the iframe loads through the Vite dev server (which proxies API calls to FastAPI).
All endpoints are served by FastAPI. Interactive docs at http://localhost:8000/docs.
| Method | Path | Description |
|---|---|---|
POST |
/ingest |
Start background ingestion of an OpenAPI spec URL |
GET |
/ingest/status/{index_name} |
Poll ingestion status |
GET |
/indexes |
List all Elasticsearch indexes with status and doc count |
POST /ingest body
{ "url": "https://example.com/api.yaml", "index_name": "my-api" }| Method | Path | Description |
|---|---|---|
POST |
/chat |
Send a message; creates a session if session_id is omitted |
GET |
/chat/sessions |
List all persisted sessions |
GET |
/chat/{session_id} |
Retrieve full history for a session |
DELETE |
/chat/{session_id} |
Delete a session and its messages |
POST /chat body
{
"message": "How do I create a purchase order?",
"index_name": "my-api",
"session_id": "optional-uuid",
"k": 3
}| Method | Path | Description |
|---|---|---|
POST |
/query |
Single-turn RAG query, no session tracking |
├── api.py # FastAPI app — ingest, chat, query endpoints
├── api_swagger_to_es.py # OpenAPI → Elasticsearch ingestion helpers
├── db.py # SQLite layer for chat session persistence
├── requirements.txt
└── frontend/
├── index.html # Main app entry
├── widget.html # Embeddable widget entry
├── public/
│ └── embed.js # Widget loader script (vanilla JS, no deps)
├── vite.config.js
└── src/
├── main.jsx # Main app mount
├── App.jsx # Main app — Chat + Ingest tabs
├── App.css
├── CompanyLogo.jsx # SVG logo component
├── widget-main.jsx # Widget app mount
├── WidgetApp.jsx # Embeddable chat widget
└── WidgetApp.css
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | Required. OpenAI secret key |
ES_URL |
http://localhost:9200 |
Elasticsearch base URL |
SQLITE_PATH |
chat_history.db |
Path to the SQLite database file |
CORS_ORIGINS |
http://localhost:5173,http://localhost:3000 |
Comma-separated list of allowed origins |
cd frontend
npm run build # outputs to frontend/dist/Serve frontend/dist/ as static files behind a reverse proxy (nginx, Caddy, etc.) and proxy /chat, /query, /ingest, /indexes, /health to the FastAPI process.
Minimal nginx snippet:
server {
listen 443 ssl;
server_name your-app.com;
root /path/to/frontend/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~ ^/(chat|query|ingest|indexes|health) {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
}
}Set CORS_ORIGINS to the list of domains that are allowed to embed the widget.