title: CX Bot
emoji: π€
colorFrom: purple
colorTo: blue
sdk: docker
pinned: false
Domain-Aware Multi-Agent CX Bot
A local, fully self-hosted customer experience platform that routes incoming support queries to domain-specific RAG agents (billing, returns, escalation), grounds every answer in retrieved policy documents, scores its own confidence, and logs every interaction for auditing and continuous evaluation.
Everything runs on your own machine β local LLM via Ollama, local vector index via FAISS, local cache/session store via Redis. No external API keys required.
Why this exists
Most "RAG chatbot" demos stop at retrieve β generate. This project adds the pieces that make a support bot actually trustworthy and operable in production:
- Routing instead of one giant prompt β a lightweight classifier sends each query to a specialized agent with its own system prompt and document set.
- Hybrid retrieval β BM25 keyword search and FAISS semantic search are fused, so exact policy terms (e.g. "Section 5") aren't lost to pure embedding similarity.
- Graph-based multi-hop expansion β chunks that cross-reference each other ("see Section 3", "as per the returns policy") are linked in a graph so the agent can follow that reference automatically instead of missing it.
- Confidence isn't a single number from the LLM β it's a weighted fusion of router confidence, retrieval relevance, and an LLM self-rating, so low-confidence answers can be reliably flagged for escalation.
- Every query is audited β full request/response metadata is logged to disk as JSONL, which a nightly evaluator and prompt optimizer both read from.
- PII is redacted before it touches the LLM or the logs, with a regex fallback if Presidio/spaCy aren't installed.
Stack
LangChain Β· FAISS Β· HuggingFace BGE embeddings Β· BM25 Β· NetworkX (graph RAG) Β· Ollama (Llama 3.2) Β· FastAPI Β· Redis Β· Presidio (PII redaction)
Architecture
User Query
β
βΌ
[PII Redactor] (Presidio, regex fallback)
β
βΌ
[Redis Semantic Cache] ββHITβββ Cached Response
β MISS
βΌ
[Session Memory] (Redis, last N turns)
β
βΌ
[Router Agent] (Llama 3.2 zero-shot classification)
ββββ BillingAgent
ββββ ReturnsAgent
ββββ EscalationAgent
β
βΌ
[Hybrid Retriever] BM25 + FAISS/BGE, score-fused
β
βΌ
[Graph RAG] NetworkX multi-hop expansion (cross-references)
β
βΌ
[Llama 3.2 via Ollama] grounded generation
β
βΌ
[Confidence Scorer] router + retrieval + LLM self-rating
β
βΌ
[Audit Logger] β logs/audit_<date>.jsonl
β
βΌ
Structured Response
Project structure
cx_bot/
βββ api/main.py # FastAPI app β all HTTP endpoints
βββ agents/
β βββ base_agent.py # Shared retrieve β expand β generate β score pipeline
β βββ billing_agent.py # Billing domain system prompt
β βββ returns_agent.py # Returns domain system prompt
β βββ escalation_agent.py # Escalation domain system prompt
β βββ router.py # LLM-based query classifier
βββ rag/
β βββ indexer.py # Loads docs, chunks, embeds, builds/saves FAISS indexes
β βββ retriever.py # Hybrid BM25 + FAISS retrieval with score fusion
β βββ bm25_retriever.py # BM25 keyword search per domain
β βββ graph_rag.py # Builds cross-reference graph, multi-hop expansion
β βββ embeddings.py # Embedding model loader (HuggingFace BGE)
βββ pipeline/
β βββ pii_redactor.py # Presidio-based PII redaction, regex fallback
β βββ confidence_scorer.py # Fuses router/retrieval/LLM signals into one score
βββ cache/redis_cache.py # Semantic response cache (cosine similarity over embeddings)
βββ memory/session_store.py # Per-session conversation history (Redis, TTL-bound)
βββ audit/logger.py # Append-only JSONL audit logging + read-back
βββ eval/
β βββ auto_eval.py # Nightly benchmark: samples audit logs, computes metrics
β βββ prompt_optimizer.py # Tests prompt variants against low-confidence queries
βββ documents/ # Source-of-truth policy docs, by domain
β βββ billing/
β βββ returns/
β βββ escalation/
βββ faiss_indexes/ # Generated β FAISS indexes + chunk pickles + graph cache
βββ logs/ # Generated β daily audit logs, benchmark/optimizer reports
βββ config.py # All tunables, loaded from .env
βββ docker-compose.yml # Redis service
βββ run.sh # One-shot startup script
βββ test_client.py # Manual smoke-test script
Prerequisites
- Python 3.10+
- Ollama installed and on PATH
- Docker (for Redis) β or a Redis instance you point
config.pyat - ~4GB free disk for the Llama 3.2 model + BGE embedding model on first run
Quick Start
1. Set up the environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # adjust values if needed
2. Pull and serve the Ollama model
ollama pull llama3.2
ollama serve
3. Start Redis
docker-compose up -d redis
4. Build document indexes
python rag/indexer.py --domain all
5. Start the API
uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
Or, on Linux/macOS, do steps 2β5 in one go:
chmod +x run.sh && ./run.sh
Once running, interactive API docs are available at http://localhost:8000/docs.
Configuration
All settings live in .env (see .env.example for the full template) and are loaded
through config.py:
| Variable | Default | Description |
|---|---|---|
OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama server address |
OLLAMA_MODEL |
llama3.2 |
Model used for routing and generation |
REDIS_HOST / REDIS_PORT / REDIS_DB |
localhost / 6379 / 0 |
Redis connection for cache + session memory |
EMBED_MODEL |
BAAI/bge-base-en-v1.5 |
HuggingFace embedding model for FAISS |
FAISS_INDEX_DIR |
faiss_indexes |
Where indexes/chunks/graphs are persisted |
DOCS_DIR |
documents |
Root folder for domain policy documents |
AUDIT_LOG_DIR |
logs |
Where audit logs and reports are written |
CONFIDENCE_THRESHOLD |
0.65 |
Below this, a response is flagged low_confidence |
CACHE_SIM_THRESHOLD |
0.92 |
Minimum cosine similarity for a cache hit |
CACHE_TTL_SECONDS |
3600 |
Cache entry lifetime |
SESSION_TTL_SECONDS |
1800 |
Session memory lifetime |
SESSION_MAX_TURNS |
10 |
Conversation turns retained per session |
TOP_K_RETRIEVAL |
5 |
Chunks retrieved per query |
BM25_WEIGHT / VECTOR_WEIGHT |
0.4 / 0.6 |
Fusion weights for hybrid retrieval |
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /query |
Submit a customer query, get a routed, grounded, scored response |
| GET | /audit |
View audit log records (?date=YYYY-MM-DD&limit=100) |
| POST | /index |
Re-index all domain documents (runs in the background) |
| DELETE | /session/{id} |
Clear a session's conversation memory |
| GET | /health |
Health check; lists active agents |
Example request
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"session_id": "user_001", "query": "Why was I charged twice?"}'
Example response
{
"answer": "Based on our billing policy (Section 5)...",
"agent": "billing",
"sources": ["billing_policy.txt"],
"confidence": 0.87,
"low_confidence": false,
"disclaimer": "This response is based on current billing policies...",
"audit_id": "uuid-here",
"latency_ms": 1240,
"cache_hit": false
}
Adding domain documents
Drop .txt or .md files into:
documents/billing/β invoices, billing policiesdocuments/returns/β return/exchange policiesdocuments/escalation/β SLA documents, escalation procedures
Then re-index just that domain (or all of them):
python rag/indexer.py --domain billing
# or rebuild everything via the API (clears in-memory FAISS/BM25 caches too):
curl -X POST http://localhost:8000/index
To enable cross-document reference following, phrase relationships explicitly in
your docs (e.g. "see Section 3", "as per the returns policy") β graph_rag.py
detects these patterns to link chunks for multi-hop retrieval.
Evaluation & continuous improvement
Benchmark β samples recent audit logs (across the last 7 days by default) and checks them against target thresholds:
python eval/auto_eval.py --mode benchmark --samples 100
| Metric | Target |
|---|---|
| Cache hit rate | β₯ 40% |
| Low-confidence rate | β€ 15% |
| Avg latency | < 1500ms |
| p95 latency | < 3500ms |
| Avg confidence | β₯ 0.80 |
Prompt optimizer β pulls queries that were flagged low_confidence, tests three
system-prompt variants (precise / empathetic / structured) against them, and writes
the best-performing variant to logs/prompt_optimization_latest.json:
python eval/prompt_optimizer.py
Nightly cron (optional)
0 2 * * * cd /path/to/cx_bot && python eval/auto_eval.py >> logs/eval.log 2>&1
0 3 * * * cd /path/to/cx_bot && python eval/prompt_optimizer.py >> logs/optimizer.log 2>&1
Troubleshooting
- Audit log line counts don't match a benchmark's
total_queriesβ the benchmark sums entries across the last 7 dailyaudit_<date>.jsonlfiles, not just today's. Check all files with:Get-ChildItem logs\*.jsonl | ForEach-Object { "$($_.Name): $((Get-Content $_.FullName).Count) lines" } - Presidio/spaCy not installed β PII redaction silently falls back to regex patterns (email, phone, credit card, SSN). Check logs for a "Presidio not available" warning.
- Redis unreachable β caching and session memory fail open (queries still work, just without caching/memory); check for "Redis unavailable" debug logs.
- First query after a fresh clone is slow β indexes are built lazily on first
access if missing. Run
python rag/indexer.py --domain allahead of time to avoid this on a live request.
License
MIT β see LICENSE.