From 9ca2c8573ebf8c101d614054c31acff55f7366fb Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Wed, 11 Mar 2026 16:37:38 +0100 Subject: [PATCH 1/7] feat(openwebui): add ui --- .env.example | 12 ++- Dockerfile.api | 16 ++++ README.md | 101 +++++++++++++++----- app/__init__.py | 1 + app/main.py | 232 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 42 ++++++++ requirements.txt | 4 +- 7 files changed, 379 insertions(+), 29 deletions(-) create mode 100644 Dockerfile.api create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example index a79422d..035e3fa 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,12 @@ 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 \ No newline at end of file +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. diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..eff2aeb --- /dev/null +++ b/Dockerfile.api @@ -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 ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 85712bd..96a4900 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,96 @@ # 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 +``` + +## 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: +- Open WebUI: `http://localhost:3000` +- Chatbot API health: `http://localhost:8000/health` + +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: -# 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). +docker compose up + +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. + +## Optional: notebooks + +```bash +pip install jupyter ipykernel +python -m ipykernel install --user --name chatbot-venv --display-name "Python (chatbot-venv)" +jupyter notebook +``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..d23ecf7 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# Chatbot API package diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..6705040 --- /dev/null +++ b/app/main.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +import json +import os +import time +import uuid +from pathlib import Path +from typing import Any + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse +from langchain_chroma import Chroma +from langchain_openai import ChatOpenAI, OpenAIEmbeddings +from pydantic import BaseModel + +load_dotenv() + +BACKEND_MODEL_ID = os.getenv("BACKEND_MODEL_ID", "chatbot-rag") +LLM_MODEL = os.getenv("MODEL_LLM", "gpt-4o-mini") +EMB_MODEL = os.getenv("MODEL_EMBEDDING", "text-embedding-3-small") +VECTOR_STORE_DIR = os.getenv("VECTOR_STORE_DIR", "vector-store") +COLLECTION_NAME = os.getenv("COLLECTION_NAME", "collection_test") +TOP_K = int(os.getenv("TOP_K", "4")) +SYSTEM_PROMPT = os.getenv( + "SYSTEM_PROMPT", + "You are a helpful assistant for internal documentation. " + "Use the provided context first. If context is missing, say it clearly.", +) + + +class ChatMessage(BaseModel): + role: str + content: Any + + +class ChatCompletionRequest(BaseModel): + model: str | None = None + messages: list[ChatMessage] + temperature: float | None = 0.2 + stream: bool | None = False + max_tokens: int | None = None + + +app = FastAPI(title="Chatbot RAG API", version="0.1.0") + + +def _content_to_text(content: Any) -> str: + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for item in content: + if isinstance(item, dict) and item.get("type") == "text": + parts.append(str(item.get("text", ""))) + return "\n".join([p for p in parts if p]) + return str(content) + + +def _get_vector_store() -> Chroma | None: + try: + embeddings = OpenAIEmbeddings(model=EMB_MODEL) + return Chroma( + collection_name=COLLECTION_NAME, + embedding_function=embeddings, + persist_directory=VECTOR_STORE_DIR, + ) + except Exception: + return None + + +def _to_source_ref(metadata: dict[str, Any]) -> str: + raw_source = str(metadata.get("source", "unknown")) + source_name = Path(raw_source).name if raw_source else "unknown" + page = metadata.get("page") + if page is None: + return source_name + try: + page_num = int(page) + 1 + except Exception: + return f"{source_name} (page {page})" + return f"{source_name} (page {page_num})" + + +def _retrieve_context(question: str) -> tuple[str, list[str]]: + vector_store = _get_vector_store() + if vector_store is None: + return "", [] + try: + docs = vector_store.similarity_search(question, k=TOP_K) + except Exception: + return "", [] + chunks: list[str] = [] + refs: list[str] = [] + for doc in docs: + ref = _to_source_ref(doc.metadata) + if ref not in refs: + refs.append(ref) + chunks.append(f"[{ref}]\n{doc.page_content}") + return "\n\n".join(chunks), refs + + +def _build_prompt(messages: list[ChatMessage]) -> tuple[str, list[str]]: + user_messages = [m for m in messages if m.role == "user"] + question = _content_to_text(user_messages[-1].content).strip() if user_messages else "" + if not question: + raise HTTPException(status_code=400, detail="No user message provided") + + history_lines: list[str] = [] + for msg in messages[-8:]: + if msg.role not in {"user", "assistant"}: + continue + text = _content_to_text(msg.content).strip() + if text: + history_lines.append(f"{msg.role}: {text}") + + context, refs = _retrieve_context(question) + context_block = context if context else "No relevant context found in vector store." + history_block = "\n".join(history_lines) + + prompt = ( + f"{SYSTEM_PROMPT}\n\n" + f"Context:\n{context_block}\n\n" + f"Conversation:\n{history_block}\n\n" + f"User question:\n{question}\n\n" + "Answer in the same language as the question. Be concise and factual. " + "If context is used, cite in-text references like [source]." + ) + return prompt, refs + + +def _generate_answer(request: ChatCompletionRequest) -> tuple[str, int]: + prompt, refs = _build_prompt(request.messages) + llm = ChatOpenAI( + model=LLM_MODEL, + temperature=request.temperature if request.temperature is not None else 0.2, + ) + result = llm.invoke(prompt) + text = result.content if isinstance(result.content, str) else str(result.content) + sources_block = "Sources used:\n" + if refs: + sources_block += "\n".join(f"- {ref}" for ref in refs) + else: + sources_block += "- none (no document chunk retrieved)" + text = f"{text.strip()}\n\n{sources_block}" + return text, len(prompt) + + +@app.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@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") + + try: + answer, prompt_chars = _generate_answer(request) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + created = int(time.time()) + completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}" + model_id = request.model or BACKEND_MODEL_ID + + if request.stream: + 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", + } + ], + "usage": { + "prompt_tokens": prompt_chars // 4, + "completion_tokens": len(answer) // 4, + "total_tokens": (prompt_chars + len(answer)) // 4, + }, + } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8664f6a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +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: + - "127.0.0.1:8000:8000" + 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:8000/v1 + - OPEN_AI_BASE_PATH=http://chatbot-api:8000/v1 + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - anythingllm-local-storage:/app/server/storage + ports: + - "127.0.0.1:3001:3001" + restart: unless-stopped + +volumes: + anythingllm-local-storage: diff --git a/requirements.txt b/requirements.txt index 9c37cbc..3417529 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ python-dotenv>=1.0.0 +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 langchain-openai langchain_chroma langchain-text-splitters @@ -6,4 +8,4 @@ langchain-community langgraph pathlib tqdm -pypdf \ No newline at end of file +pypdf From 7853f0139c278c0a091d7cab64c588e54c15c797 Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Mon, 16 Mar 2026 17:04:40 +0100 Subject: [PATCH 2/7] feat(openweb-ui): add automatic job to relaod the vector store --- .env.example | 3 +++ README.md | 6 ++++++ docker-compose.yml | 2 ++ reload_job.sh | 23 +++++++++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100755 reload_job.sh diff --git a/.env.example b/.env.example index 035e3fa..706c4f9 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,6 @@ 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 diff --git a/README.md b/README.md index 96a4900..37adec1 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ Security notes: - 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 diff --git a/docker-compose.yml b/docker-compose.yml index 8664f6a..b6ed295 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,8 @@ services: - STORAGE_DIR=/app/server/storage - GENERIC_OPEN_AI_BASE_PATH=http://chatbot-api:8000/v1 - OPEN_AI_BASE_PATH=http://chatbot-api:8000/v1 + - AUTH_TOKEN=${ANYLLM_ADMIN_PASSWORD} + - JWT_SECRET=${JWT_SECRET} extra_hosts: - "host.docker.internal:host-gateway" volumes: diff --git a/reload_job.sh b/reload_job.sh new file mode 100755 index 0000000..552aac3 --- /dev/null +++ b/reload_job.sh @@ -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" From 8a95736eb4ee21c4679210e61e2447cd49d16c11 Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Thu, 2 Apr 2026 15:59:08 +0200 Subject: [PATCH 3/7] add fake api compatible with anythingLLM --- Dockerfile.api | 2 +- app/__init__.py | 2 +- app/main.py | 140 ++---------------------------------------------- 3 files changed, 6 insertions(+), 138 deletions(-) diff --git a/Dockerfile.api b/Dockerfile.api index eff2aeb..381f6dd 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -8,7 +8,7 @@ ENV PYTHONUNBUFFERED=1 COPY requirements.txt /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt -COPY app /app/app +COPY app /app/app COPY vector-store /app/vector-store EXPOSE 8000 diff --git a/app/__init__.py b/app/__init__.py index d23ecf7..8b13789 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1 @@ -# Chatbot API package + diff --git a/app/main.py b/app/main.py index 6705040..32a1581 100644 --- a/app/main.py +++ b/app/main.py @@ -3,29 +3,13 @@ import os import time import uuid -from pathlib import Path from typing import Any -from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse -from langchain_chroma import Chroma -from langchain_openai import ChatOpenAI, OpenAIEmbeddings from pydantic import BaseModel -load_dotenv() - BACKEND_MODEL_ID = os.getenv("BACKEND_MODEL_ID", "chatbot-rag") -LLM_MODEL = os.getenv("MODEL_LLM", "gpt-4o-mini") -EMB_MODEL = os.getenv("MODEL_EMBEDDING", "text-embedding-3-small") -VECTOR_STORE_DIR = os.getenv("VECTOR_STORE_DIR", "vector-store") -COLLECTION_NAME = os.getenv("COLLECTION_NAME", "collection_test") -TOP_K = int(os.getenv("TOP_K", "4")) -SYSTEM_PROMPT = os.getenv( - "SYSTEM_PROMPT", - "You are a helpful assistant for internal documentation. " - "Use the provided context first. If context is missing, say it clearly.", -) class ChatMessage(BaseModel): @@ -36,118 +20,12 @@ class ChatMessage(BaseModel): class ChatCompletionRequest(BaseModel): model: str | None = None messages: list[ChatMessage] - temperature: float | None = 0.2 + temperature: float | None = None stream: bool | None = False max_tokens: int | None = None -app = FastAPI(title="Chatbot RAG API", version="0.1.0") - - -def _content_to_text(content: Any) -> str: - if isinstance(content, str): - return content - if isinstance(content, list): - parts: list[str] = [] - for item in content: - if isinstance(item, dict) and item.get("type") == "text": - parts.append(str(item.get("text", ""))) - return "\n".join([p for p in parts if p]) - return str(content) - - -def _get_vector_store() -> Chroma | None: - try: - embeddings = OpenAIEmbeddings(model=EMB_MODEL) - return Chroma( - collection_name=COLLECTION_NAME, - embedding_function=embeddings, - persist_directory=VECTOR_STORE_DIR, - ) - except Exception: - return None - - -def _to_source_ref(metadata: dict[str, Any]) -> str: - raw_source = str(metadata.get("source", "unknown")) - source_name = Path(raw_source).name if raw_source else "unknown" - page = metadata.get("page") - if page is None: - return source_name - try: - page_num = int(page) + 1 - except Exception: - return f"{source_name} (page {page})" - return f"{source_name} (page {page_num})" - - -def _retrieve_context(question: str) -> tuple[str, list[str]]: - vector_store = _get_vector_store() - if vector_store is None: - return "", [] - try: - docs = vector_store.similarity_search(question, k=TOP_K) - except Exception: - return "", [] - chunks: list[str] = [] - refs: list[str] = [] - for doc in docs: - ref = _to_source_ref(doc.metadata) - if ref not in refs: - refs.append(ref) - chunks.append(f"[{ref}]\n{doc.page_content}") - return "\n\n".join(chunks), refs - - -def _build_prompt(messages: list[ChatMessage]) -> tuple[str, list[str]]: - user_messages = [m for m in messages if m.role == "user"] - question = _content_to_text(user_messages[-1].content).strip() if user_messages else "" - if not question: - raise HTTPException(status_code=400, detail="No user message provided") - - history_lines: list[str] = [] - for msg in messages[-8:]: - if msg.role not in {"user", "assistant"}: - continue - text = _content_to_text(msg.content).strip() - if text: - history_lines.append(f"{msg.role}: {text}") - - context, refs = _retrieve_context(question) - context_block = context if context else "No relevant context found in vector store." - history_block = "\n".join(history_lines) - - prompt = ( - f"{SYSTEM_PROMPT}\n\n" - f"Context:\n{context_block}\n\n" - f"Conversation:\n{history_block}\n\n" - f"User question:\n{question}\n\n" - "Answer in the same language as the question. Be concise and factual. " - "If context is used, cite in-text references like [source]." - ) - return prompt, refs - - -def _generate_answer(request: ChatCompletionRequest) -> tuple[str, int]: - prompt, refs = _build_prompt(request.messages) - llm = ChatOpenAI( - model=LLM_MODEL, - temperature=request.temperature if request.temperature is not None else 0.2, - ) - result = llm.invoke(prompt) - text = result.content if isinstance(result.content, str) else str(result.content) - sources_block = "Sources used:\n" - if refs: - sources_block += "\n".join(f"- {ref}" for ref in refs) - else: - sources_block += "- none (no document chunk retrieved)" - text = f"{text.strip()}\n\n{sources_block}" - return text, len(prompt) - - -@app.get("/health") -def health() -> dict[str, str]: - return {"status": "ok"} +app = FastAPI(title="Chatbot API", version="0.1.0") @app.get("/v1/models") @@ -169,18 +47,13 @@ def chat_completions(request: ChatCompletionRequest): if not request.messages: raise HTTPException(status_code=400, detail="messages is required") - try: - answer, prompt_chars = _generate_answer(request) - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) from e - + 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, @@ -224,9 +97,4 @@ def event_stream(): "finish_reason": "stop", } ], - "usage": { - "prompt_tokens": prompt_chars // 4, - "completion_tokens": len(answer) // 4, - "total_tokens": (prompt_chars + len(answer)) // 4, - }, } From 20d1c17addd0d7ce7972813b4f8389a73d454047 Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Thu, 2 Apr 2026 16:10:22 +0200 Subject: [PATCH 4/7] securize docker compose to avoid leak --- .env.example | 9 +++++++++ Dockerfile.api | 2 +- README.md | 10 ++++++++-- docker-compose.yml | 8 ++++---- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 706c4f9..cfb4d1d 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,12 @@ 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 diff --git a/Dockerfile.api b/Dockerfile.api index 381f6dd..9b42bc2 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -13,4 +13,4 @@ COPY vector-store /app/vector-store EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["sh", "-lc", "uvicorn app.main:app --host 0.0.0.0 --port ${CHATBOT_API_INTERNAL_PORT}"] diff --git a/README.md b/README.md index 37adec1..e9c5b81 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ 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 @@ -58,8 +64,8 @@ docker compose up --build ``` Access: -- Open WebUI: `http://localhost:3000` -- Chatbot API health: `http://localhost:8000/health` +- 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` diff --git a/docker-compose.yml b/docker-compose.yml index b6ed295..3d1c8e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: volumes: - ./vector-store:/app/vector-store ports: - - "127.0.0.1:8000:8000" + - "${CHATBOT_API_BIND_IP}:${CHATBOT_API_HOST_PORT}:${CHATBOT_API_INTERNAL_PORT}" restart: unless-stopped anythingllm: @@ -28,8 +28,8 @@ services: - .env environment: - STORAGE_DIR=/app/server/storage - - GENERIC_OPEN_AI_BASE_PATH=http://chatbot-api:8000/v1 - - OPEN_AI_BASE_PATH=http://chatbot-api:8000/v1 + - 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: @@ -37,7 +37,7 @@ services: volumes: - anythingllm-local-storage:/app/server/storage ports: - - "127.0.0.1:3001:3001" + - "${ANYTHINGLLM_BIND_IP}:${ANYTHINGLLM_HOST_PORT}:${ANYTHINGLLM_HOST_PORT}" restart: unless-stopped volumes: From 9ce36b6652b800bfc4ceb3aee449345479692258 Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Fri, 3 Apr 2026 11:59:03 +0200 Subject: [PATCH 5/7] ajout du worklow pour pousser l'image docker sur ghcr --- .github/workflows/ghcr.yaml | 31 ++++++++++++++++++++++++++ docker/docker-compose-prod.yml | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/ghcr.yaml create mode 100644 docker/docker-compose-prod.yml diff --git a/.github/workflows/ghcr.yaml b/.github/workflows/ghcr.yaml new file mode 100644 index 0000000..17bbadb --- /dev/null +++ b/.github/workflows/ghcr.yaml @@ -0,0 +1,31 @@ +name: Create and publish all Docker images + +on: + push: + branches: ["main"] + +jobs: + build-docker-image: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and publish Chabot Docker Image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.api + push: true + tags: | + ghcr.io/xpeho/chatbot-api:latest + ghcr.io/xpeho/chatbot-api:${{ github.sha }} diff --git a/docker/docker-compose-prod.yml b/docker/docker-compose-prod.yml new file mode 100644 index 0000000..9fe483a --- /dev/null +++ b/docker/docker-compose-prod.yml @@ -0,0 +1,40 @@ +services: + chatbot-api: + image: ghcr.io/xpeho/chatbot-api:latest + pull_policy: always + 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} + volumes: + - anythingllm-local-storage:/app/server/storage + ports: + - "${ANYTHINGLLM_BIND_IP}:${ANYTHINGLLM_HOST_PORT}:${ANYTHINGLLM_HOST_PORT}" + restart: unless-stopped + +volumes: + anythingllm-local-storage: From 1bffb9acb441ced14445be447d6bfdae270d7eea Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Fri, 3 Apr 2026 14:43:48 +0200 Subject: [PATCH 6/7] activate the workflow to test --- .github/workflows/ghcr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr.yaml b/.github/workflows/ghcr.yaml index 17bbadb..5f3f4a0 100644 --- a/.github/workflows/ghcr.yaml +++ b/.github/workflows/ghcr.yaml @@ -2,7 +2,7 @@ name: Create and publish all Docker images on: push: - branches: ["main"] + branches: ["main","chore/github-registry"] jobs: build-docker-image: From 1167b5d4164d70ffa7a3390805bf013df1230299 Mon Sep 17 00:00:00 2001 From: Romain Vanhee Date: Fri, 3 Apr 2026 15:22:04 +0200 Subject: [PATCH 7/7] use the latest ubuntu vresion --- .github/workflows/ghcr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr.yaml b/.github/workflows/ghcr.yaml index 5f3f4a0..6abb288 100644 --- a/.github/workflows/ghcr.yaml +++ b/.github/workflows/ghcr.yaml @@ -6,7 +6,7 @@ on: jobs: build-docker-image: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4