Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ BOOKSTACK_TOKEN_ID=votre_token_bookstack
BOOKSTACK_TOKEN_SECRET=votre_secret_token
BOOKSTACK_URL=http://localhost:6875

MODEL_LLM=mon-model-IA
MODEL_EMBEDDING=mon-model-embedding
OPENAI_API_KEY=myapikey
OPENAI_API_KEY=sk-proj-your-api-key
MODEL_LLM=myllmmodel
MODEL_EMBEDDING=myembeddingmodel

COLLECTION_NAME=collection_test
VECTOR_STORE_DIR=vector-store
TOP_K=4
BACKEND_MODEL_ID=chatbot-rag
SYSTEM_PROMPT=You are a helpful assistant for internal documentation.

ANYLLM_ADMIN_PASSWORD=your_anythingllm_password
JWT_SECRET=your-random-secret-key

# Network binding / ports
CHATBOT_API_BIND_IP=1.1.1.1
CHATBOT_API_HOST_PORT=CHATPORT
CHATBOT_API_INTERNAL_PORT=CHATPORT

ANYTHINGLLM_BIND_IP=1.1.1.1
ANYTHINGLLM_HOST_PORT=PORT
ANYTHINGLLM_CONTAINER_PORT=PORT
16 changes: 16 additions & 0 deletions Dockerfile.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.11-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

COPY app /app/app
COPY vector-store /app/vector-store

EXPOSE 8000

CMD ["sh", "-lc", "uvicorn app.main:app --host 0.0.0.0 --port ${CHATBOT_API_INTERNAL_PORT}"]
113 changes: 88 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,108 @@
# Chatbot

## Export BookStack pages to PDF
This project provides:
- BookStack page export to PDF
- Vector store indexing (Chroma)
- A RAG chatbot API (OpenAI-compatible endpoints)
- An Open WebUI interface connected to the RAG API

### Prerequisites
- Python 3.9+
- BookStack API token (token id + token secret)
- BookStack URL
## Prerequisites

- Python 3.11+
- Docker + Docker Compose
- OpenAI API key with active billing/quota
- BookStack API token (for export only)

## Environment setup

Create `.env` from `.env.example` and fill values:

```env
OPENAI_API_KEY=sk-proj-your-api-key
MODEL_LLM=yout_model
MODEL_EMBEDDING=your_embedding_model

BOOKSTACK_URL=your_url
BOOKSTACK_TOKEN_ID=your_bookstack_token_id
BOOKSTACK_TOKEN_SECRET=your_bookstack_token_secret

CHATBOT_API_BIND_IP=IP
CHATBOT_API_HOST_PORT=PORT
CHATBOT_API_INTERNAL_PORT=PORT
ANYTHINGLLM_BIND_IP=IP
ANYTHINGLLM_HOST_PORT=PORT
```

## Python setup

### Setup
1. Create a virtual environment
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

2. Install dependencies
## 1) Export BookStack pages as PDFs

```bash
pip install -r requirements.txt
python export_pages.py
```

3. Create a `.env` file (or update it) at the project root:
```env
BOOKSTACK_URL=your_bookstack_url
BOOKSTACK_TOKEN_ID=your_token_id
BOOKSTACK_TOKEN_SECRET=your_token_secret
```
PDF files are written to `exports/`.

## 2) Build / refresh vector store

## To run a notebook
```bash
pip install jupyter ipykernel
python -m ipykernel install --user --name chatbot-venv --display-name "Python (chatbot-venv)"
jupyter notebook
python reload_vector_store.py
```

### Run the export
This indexes documents from `exports/` into `vector-store/`.

## 3) Run chatbot API + Open WebUI

```bash
python export_pages.py
docker compose up --build
```

PDFs will be saved to `exports/`
Access:
- AnythingLLM: `http://localhost:${ANYTHINGLLM_HOST_PORT}` (default: `3001`)
- Chatbot API health: `http://localhost:${CHATBOT_API_HOST_PORT}/health` (default: `8000`)

Open WebUI is configured to call the local chatbot backend through OpenAI-compatible routes:
- `GET /v1/models`
- `POST /v1/chat/completions`

## 4) Expose AnythingLLM on internet (secure minimal setup)

Files:
- `docker-compose.anythingllm.secure.yml`
- `deploy/Caddyfile`

This stack puts Caddy in front of AnythingLLM:
- HTTPS with automatic TLS certificates
- HTTP Basic Auth at proxy level
- AnythingLLM not exposed directly (internal only)

Steps:

1. Create env file:

docker compose up

# Pricing OPenAI
Pour un rechargement de base on utilise en moyenne 67 000 tokens soit 1/7 centimes.
Une question côut 1/40 centimes (une toute simple).
Security notes:
- Keep AnythingLLM built-in auth enabled (multi-user recommended for internet exposure).
- Disable public signup inside AnythingLLM unless explicitly needed.
- Keep `OPENAI_API_KEY` only inside server env, never in frontend code.

## 5) Automatize the vector store reload :
Create a job to automatically update the vectore store weekly :
CRON_TZ=Europe/Paris
0 7 * * 1 chatbot/reload_job.sh >> chatbot/reindex.log 2>&1


## Optional: notebooks

```bash
pip install jupyter ipykernel
python -m ipykernel install --user --name chatbot-venv --display-name "Python (chatbot-venv)"
jupyter notebook
```
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il sert à quoi ce fichier ? ☺️

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est pour dire que le dossier est un package et pouvoir ensuite l'exporter depuis d'autre fichier en dehors de ce dossier

100 changes: 100 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
import json
import os
import time
import uuid
from typing import Any

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

BACKEND_MODEL_ID = os.getenv("BACKEND_MODEL_ID", "chatbot-rag")


class ChatMessage(BaseModel):
role: str
content: Any


class ChatCompletionRequest(BaseModel):
model: str | None = None
messages: list[ChatMessage]
temperature: float | None = None
stream: bool | None = False
max_tokens: int | None = None


app = FastAPI(title="Chatbot API", version="0.1.0")


@app.get("/v1/models")
def list_models() -> dict[str, Any]:
return {
"object": "list",
"data": [
{
"id": BACKEND_MODEL_ID,
"object": "model",
"owned_by": "chatbot",
}
],
}


@app.post("/v1/chat/completions")
def chat_completions(request: ChatCompletionRequest):
if not request.messages:
raise HTTPException(status_code=400, detail="messages is required")

answer = f"Reponse bidon API."
created = int(time.time())
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
model_id = request.model or BACKEND_MODEL_ID

if request.stream:
##réponse compatible ANYTHINGLLM
def event_stream():
chunk_1 = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model_id,
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
}
yield f"data: {json.dumps(chunk_1)}\n\n"

chunk_2 = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model_id,
"choices": [{"index": 0, "delta": {"content": answer}, "finish_reason": None}],
}
yield f"data: {json.dumps(chunk_2)}\n\n"

chunk_3 = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model_id,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}
yield f"data: {json.dumps(chunk_3)}\n\n"
yield "data: [DONE]\n\n"

return StreamingResponse(event_stream(), media_type="text/event-stream")

return {
"id": completion_id,
"object": "chat.completion",
"created": created,
"model": model_id,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": answer},
"finish_reason": "stop",
}
],
}
44 changes: 44 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
services:
chatbot-api:
build:
context: .
dockerfile: Dockerfile.api
container_name: chatbot-api-local
env_file:
- .env
environment:
- VECTOR_STORE_DIR=/app/vector-store
- COLLECTION_NAME=collection_test
- TOP_K=4
- BACKEND_MODEL_ID=chatbot-rag
volumes:
- ./vector-store:/app/vector-store
ports:
- "${CHATBOT_API_BIND_IP}:${CHATBOT_API_HOST_PORT}:${CHATBOT_API_INTERNAL_PORT}"
restart: unless-stopped

anythingllm:
image: mintplexlabs/anythingllm:latest
container_name: anythingllm-local
depends_on:
- chatbot-api
cap_add:
- SYS_ADMIN
env_file:
- .env
environment:
- STORAGE_DIR=/app/server/storage
- GENERIC_OPEN_AI_BASE_PATH=http://chatbot-api:${CHATBOT_API_INTERNAL_PORT}/v1
- OPEN_AI_BASE_PATH=http://chatbot-api:${CHATBOT_API_INTERNAL_PORT}/v1
- AUTH_TOKEN=${ANYLLM_ADMIN_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- anythingllm-local-storage:/app/server/storage
ports:
- "${ANYTHINGLLM_BIND_IP}:${ANYTHINGLLM_HOST_PORT}:${ANYTHINGLLM_HOST_PORT}"
restart: unless-stopped

volumes:
anythingllm-local-storage:
23 changes: 23 additions & 0 deletions reload_job.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml"
LOCK_DIR="/tmp/chatbot-reindex-lock"

# Prevent overlapping runs.
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "[reindex] another run is already in progress"
exit 0
fi
trap 'rmdir "$LOCK_DIR"' EXIT

cd "$PROJECT_DIR"

echo "[reindex] starting export + vector store reload"
docker compose -f "$COMPOSE_FILE" run --rm --no-deps --build \
-v "$PROJECT_DIR:/work" \
-w /work \
chatbot-api \
sh -lc "python export_pages.py && python reload_vector_store.py"
echo "[reindex] done"
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
python-dotenv>=1.0.0
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
langchain-openai
langchain_chroma
langchain-text-splitters
langchain-community
langgraph
pathlib
tqdm
pypdf
pypdf