diff --git a/.github/workflows/helm.yaml b/.github/workflows/helm.yaml new file mode 100644 index 000000000..f2b87af64 --- /dev/null +++ b/.github/workflows/helm.yaml @@ -0,0 +1,54 @@ +name: Publish Helm Chart + +on: + push: + branches: + - main + - dev + paths: + - "charts/**" +env: + HELM_REGISTRY: ghcr.io + HELM_ORG: linagora + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.4 + + - name: Set chart version + run: | + BASE_VERSION=$(yq '.version' charts/openrag-stack/Chart.yaml) + + if [[ "${GITHUB_REF_NAME}" == "main" ]]; then + # Keep version as-is for stable + CHART_VERSION="$BASE_VERSION" + else + # Append branch for dev builds + CHART_VERSION="${BASE_VERSION}-dev" + yq -i ".version = \"${CHART_VERSION}\"" charts/openrag-stack/Chart.yaml + fi + + echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_ENV + echo "Using chart version: $CHART_VERSION" + + - name: Helm dependency update + run: helm dependency update charts/openrag-stack + + - name: Package chart + run: helm package charts/openrag-stack --destination . + + - name: Login to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login $HELM_REGISTRY -u ${{ github.actor }} --password-stdin + + - name: Push chart + run: | + helm push openrag-stack-${CHART_VERSION}.tgz oci://$HELM_REGISTRY/$HELM_ORG diff --git a/.gitignore b/.gitignore index 1d15c51a1..ad803d074 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ services/* *.json *.csv *.pkl + +#helm +charts/openrag-stack/charts/*.tgz diff --git a/Dockerfile.ray b/Dockerfile.ray index 31e87d385..8f91d86c7 100644 --- a/Dockerfile.ray +++ b/Dockerfile.ray @@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y \ make \ ssh \ rsync \ + wget \ libpq-dev python3-dev \ && rm -rf /var/lib/apt/lists/* @@ -35,7 +36,7 @@ COPY pyproject.toml uv.lock ./ RUN pip3 install uv && \ uv python install 3.12.7 && \ uv python pin 3.12.7 - # && \uv sync --no-dev +# && \uv sync --no-dev COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh @@ -46,10 +47,10 @@ WORKDIR /app/openrag COPY openrag/ . # Copy assests & config -COPY public/ /app/public/ COPY prompts/ /app/prompts/ COPY .hydra_config/ /app/.hydra_config/ RUN apt install -y +RUN ln -s /app/.venv/bin/ray /usr/local/bin/ray ENV PYTHONPATH=/app/openrag/ \ No newline at end of file diff --git a/charts/openrag-stack/.helmignore b/charts/openrag-stack/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/openrag-stack/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/openrag-stack/Chart.lock b/charts/openrag-stack/Chart.lock new file mode 100644 index 000000000..9001e5e43 --- /dev/null +++ b/charts/openrag-stack/Chart.lock @@ -0,0 +1,15 @@ +dependencies: +- name: kuberay-operator + repository: https://ray-project.github.io/kuberay-helm/ + version: 1.4.0 +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 15.5.23 +- name: milvus + repository: https://zilliztech.github.io/milvus-helm/ + version: 5.0.0 +- name: vllm-stack + repository: https://vllm-project.github.io/production-stack + version: 0.1.7 +digest: sha256:eb9f465f19be0d6d6c563c4eda6dc0f7b8e9331209420f14f01587f2ca29f965 +generated: "2025-09-04T13:28:20.5556128Z" diff --git a/charts/openrag-stack/Chart.yaml b/charts/openrag-stack/Chart.yaml new file mode 100644 index 000000000..cdf3906cf --- /dev/null +++ b/charts/openrag-stack/Chart.yaml @@ -0,0 +1,39 @@ +apiVersion: v2 +name: openrag-stack +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.2.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.2.0" + +dependencies: + - name: kuberay-operator + version: "1.4.0" + repository: "https://ray-project.github.io/kuberay-helm/" + - name: postgresql + version: "15.5.23" + repository: "https://charts.bitnami.com/bitnami" + - name: milvus + version: "5.0.0" + repository: "https://zilliztech.github.io/milvus-helm/" + - name: vllm-stack + alias: vllm + version: ">=0.1.7" + repository: "https://vllm-project.github.io/production-stack" diff --git a/charts/openrag-stack/templates/_helpers.tpl b/charts/openrag-stack/templates/_helpers.tpl new file mode 100644 index 000000000..c5bd0ede7 --- /dev/null +++ b/charts/openrag-stack/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "openrag-stack.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "openrag-stack.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "openrag-stack.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "openrag-stack.labels" -}} +helm.sh/chart: {{ include "openrag-stack.chart" . }} +{{ include "openrag-stack.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "openrag-stack.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openrag-stack.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "openrag-stack.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "openrag-stack.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/openrag-stack/templates/configmap-env.yaml b/charts/openrag-stack/templates/configmap-env.yaml new file mode 100644 index 000000000..fa1a5da9e --- /dev/null +++ b/charts/openrag-stack/templates/configmap-env.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: rag-env +data: +{{- range $key, $value := .Values.env.config }} + {{ $key }}: "{{ tpl (printf "%v" $value) $ }}" +{{- end }} diff --git a/charts/openrag-stack/templates/infinity.yaml b/charts/openrag-stack/templates/infinity.yaml new file mode 100644 index 000000000..cafcfd51f --- /dev/null +++ b/charts/openrag-stack/templates/infinity.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-reranker + labels: + app.kubernetes.io/name: reranker + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.reranker.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: reranker + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: reranker + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + runtimeClassName: {{ .Values.reranker.runtimeClassName }} + containers: + - name: reranker + image: "{{ .Values.reranker.image.repository }}:{{ .Values.reranker.image.tag }}" + args: + - "v2" + - "--model-id" + - "{{ .Values.reranker.model.id }}" + - "--port" + - "{{ .Values.reranker.service.port }}" + ports: + - containerPort: {{ .Values.reranker.service.port }} + resources: + {{- toYaml .Values.reranker.resources | nindent 12 }} + volumeMounts: + - name: hf-cache + mountPath: /app/.cache/huggingface + envFrom: + - configMapRef: + name: rag-env + volumes: + - name: hf-cache + persistentVolumeClaim: + claimName: rag-model-weights +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-reranker + labels: + app.kubernetes.io/name: reranker + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + app.kubernetes.io/name: reranker + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - protocol: TCP + port: {{ .Values.reranker.service.port }} + targetPort: {{ .Values.reranker.service.port }} + type: {{ .Values.reranker.service.type }} diff --git a/charts/openrag-stack/templates/ingress.yaml b/charts/openrag-stack/templates/ingress.yaml new file mode 100644 index 000000000..255e16634 --- /dev/null +++ b/charts/openrag-stack/templates/ingress.yaml @@ -0,0 +1,26 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "openrag-stack.fullname" . }}-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + nginx.ingress.kubernetes.io/use-regex: "true" +spec: + ingressClassName: {{ .Values.ingress.className | quote }} + rules: + - http: + paths: + {{- if .Values.ingress.paths.openrag.enabled }} + - path: /(.*) + pathType: ImplementationSpecific + backend: + service: + name: raycluster-head-svc + port: + number: {{ .Values.env.config.RAY_SERVE_PORT }} + {{- end }} + {{- with .Values.ingress.host }} + host: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/openrag-stack/templates/openrag.yaml b/charts/openrag-stack/templates/openrag.yaml new file mode 100644 index 000000000..e97a0e67e --- /dev/null +++ b/charts/openrag-stack/templates/openrag.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-openrag + labels: + app.kubernetes.io/name: openrag + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.openrag.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: openrag + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: openrag + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + initContainers: + - name: init-venv + image: "{{ .Values.openrag.image.repository }}:{{ .Values.openrag.image.tag }}" + command: + - sh + - -c + - | + echo "Waiting for Python env in /app/.venv..." + while [ ! -f /app/.venv/.ready ]; do + sleep 3 + done + echo "Venv is ready." + volumeMounts: + - name: venv + mountPath: /app/.venv + containers: + - name: openrag + image: "{{ .Values.openrag.image.repository }}:{{ .Values.openrag.image.tag }}" + ports: + - containerPort: {{ .Values.openrag.service.port }} + resources: + {{- toYaml .Values.openrag.resources | nindent 12 }} + envFrom: + - configMapRef: + name: rag-env + - secretRef: + name: rag-env-secrets + volumeMounts: + - name: model-weights + mountPath: /app/model_weights + - name: data + mountPath: /app/data + - name: logs + mountPath: /app/logs + - name: venv + mountPath: /app/.venv + volumes: + - name: model-weights + persistentVolumeClaim: + claimName: rag-model-weights + - name: data + persistentVolumeClaim: + claimName: rag-data + - name: logs + persistentVolumeClaim: + claimName: rag-logs + - name: venv + persistentVolumeClaim: + claimName: rag-venv +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-openrag + labels: + app.kubernetes.io/name: openrag + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + app.kubernetes.io/name: openrag + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - protocol: TCP + port: {{ .Values.openrag.service.port }} + targetPort: {{ .Values.openrag.service.port }} + type: {{ .Values.openrag.service.type }} diff --git a/charts/openrag-stack/templates/pvc.yaml b/charts/openrag-stack/templates/pvc.yaml new file mode 100644 index 000000000..050fdfc5c --- /dev/null +++ b/charts/openrag-stack/templates/pvc.yaml @@ -0,0 +1,49 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rag-model-weights +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.volumes.modelWeights.size }} + storageClassName: {{ .Values.persistence.storageClass }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rag-data +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.volumes.data.size }} + storageClassName: {{ .Values.persistence.storageClass }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rag-logs +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.volumes.logs.size }} + storageClassName: {{ .Values.persistence.storageClass }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rag-venv +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.volumes.venv.size }} + storageClassName: {{ .Values.persistence.storageClass }} +{{- end }} diff --git a/charts/openrag-stack/templates/raycluster.yaml b/charts/openrag-stack/templates/raycluster.yaml new file mode 100644 index 000000000..2c8408c1e --- /dev/null +++ b/charts/openrag-stack/templates/raycluster.yaml @@ -0,0 +1,148 @@ +apiVersion: ray.io/v1 +kind: RayCluster +metadata: + name: raycluster + annotations: + ray.io/overwrite-container-cmd: "true" +spec: + rayVersion: "2.47.1" + enableInTreeAutoscaling: false + + headGroupSpec: + serviceType: ClusterIP + template: + spec: + initContainers: + - name: init-venv-sync + image: {{ .Values.ray.image.repository }}:{{ .Values.ray.image.tag }} + command: + - sh + - -c + - | + if [ -f /app/.venv/.ready ]; then + echo "Existing env detected, skipping install." + else + echo "No env ready, cleaning and syncing..." + rm -rf /app/.venv/* + uv sync + touch /app/.venv/.ready + fi + volumeMounts: + - name: venv + mountPath: /app/.venv + + containers: + - name: ray-head + image: {{ .Values.ray.image.repository }}:{{ .Values.ray.image.tag }} + command: ["uv"] + args: + - "run" + - "ray" + - "start" + - "--head" + - "--dashboard-host=0.0.0.0" + - "--dashboard-agent-listen-port=52365" + - "--metrics-export-port=8080" + - "--block" + ports: + - containerPort: 80 + name: serve + - containerPort: 8265 + name: dashboard + - containerPort: 10001 + name: client + - containerPort: 6379 + name: gcs + - containerPort: 8080 + name: metrics + volumeMounts: + - name: model-weights + mountPath: /app/model_weights + - name: data + mountPath: /app/data + - name: logs + mountPath: /app/logs + - name: venv + mountPath: /app/.venv + envFrom: + - configMapRef: + name: rag-env + - secretRef: + name: rag-env-secrets + resources: + limits: + nvidia.com/gpu: 1 + volumes: + - name: model-weights + persistentVolumeClaim: + claimName: rag-model-weights + - name: data + persistentVolumeClaim: + claimName: rag-data + - name: logs + persistentVolumeClaim: + claimName: rag-logs + - name: venv + persistentVolumeClaim: + claimName: rag-venv + + workerGroupSpecs: + {{- range $i, $e := until (.Values.ray.workers.count | int) }} + - groupName: worker-group-{{ add $i 1 }} + replicas: {{ $.Values.ray.workers.replicas }} + minReplicas: {{ $.Values.ray.workers.minReplicas }} + maxReplicas: {{ $.Values.ray.workers.maxReplicas }} + template: + spec: + initContainers: + - name: init-venv + image: ghcr.io/linagora/openrag:dev-latest + command: + - sh + - -c + - | + echo "Waiting for Python env in /app/.venv..." + while [ ! -f /app/.venv/.ready ]; do + sleep 3 + done + echo "Venv is ready." + volumeMounts: + - name: venv + mountPath: /app/.venv + containers: + - name: ray-worker + image: {{ $.Values.ray.image.repository }}:{{ $.Values.ray.image.tag }} + command: ["/bin/bash", "-lc", "--"] + args: + - uv run $KUBERAY_GEN_RAY_START_CMD + volumeMounts: + - name: model-weights + mountPath: /app/model_weights + - name: data + mountPath: /app/data + - name: logs + mountPath: /app/logs + - name: venv + mountPath: /app/.venv + envFrom: + - configMapRef: + name: rag-env + - secretRef: + name: rag-env-secrets + resources: + limits: + nvidia.com/gpu: 1 + volumes: + - name: model-weights + persistentVolumeClaim: + claimName: rag-model-weights + - name: data + persistentVolumeClaim: + claimName: rag-data + - name: logs + persistentVolumeClaim: + claimName: rag-logs + - name: venv + persistentVolumeClaim: + claimName: rag-venv + {{- end }} diff --git a/charts/openrag-stack/templates/secrets-env.yaml b/charts/openrag-stack/templates/secrets-env.yaml new file mode 100644 index 000000000..5bf3ee1b8 --- /dev/null +++ b/charts/openrag-stack/templates/secrets-env.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rag-env-secrets +type: Opaque +stringData: +{{- range $key, $value := .Values.env.secrets }} + {{ $key }}: "{{ $value }}" +{{- end }} diff --git a/charts/openrag-stack/values.yaml b/charts/openrag-stack/values.yaml new file mode 100644 index 000000000..50d351023 --- /dev/null +++ b/charts/openrag-stack/values.yaml @@ -0,0 +1,224 @@ +# === Global shared persistence === +persistence: + enabled: true + storageClass: &storageClass longhorn + accessMode: ReadWriteMany + volumes: + modelWeights: { size: 50Gi } + data: { size: 20Gi } + hydraConfig: { size: 10Mi } + logs: { size: 1Gi } + venv: { size: 10Gi } + +ray: + workers: + count: 2 + replicas: 1 + minReplicas: 1 + maxReplicas: 1 + image: + repository: ghcr.io/linagora/openrag-ray + tag: latest + +# === PostgreSQL (bitnami) === +postgresql: + enabled: true + auth: + username: &pgUser root + password: &pgPass root_password + primary: + persistence: + enabled: true + size: 10Gi + storageClass: *storageClass + service: + port: &pgPort 5432 + +# === Milvus === +milvus: + enabled: true + image: + all: { tag: v2.6.0 } + cluster: { enabled: true } + pulsarv3: { enabled: false } + woodpecker: { enabled: true } + streaming: { enabled: true } + indexNode: { enabled: false } + minio: + mode: distributed + replicas: 4 + persistence: + enabled: true + size: 20Gi + storageClass: *storageClass + etcd: + replicaCount: 3 + persistence: + enabled: true + size: 10Gi + storageClass: *storageClass + proxy: + service: + type: ClusterIP + port: 19530 + +# === vLLM (embedder) === +vllm: + embedderModelName: &embedderModel "Qwen/Qwen3-Embedding-0.6B" + servingEngineSpec: + enableEngine: true + modelSpec: + - name: "embedder" + repository: "vllm/vllm-openai" + tag: "latest" + modelURL: *embedderModel + replicaCount: 1 + requestCPU: 4 + requestMemory: "16Gi" + requestGPU: 1 + limitCPU: 4 + limitMemory: "16Gi" + hf_token: + secretName: rag-env-secrets + secretKey: HF_TOKEN + vllmConfig: + gpuMemoryUtilization: 0.3 + extraArgs: ["--trust-remote-code", "--task", "embed"] + servicePort: &vllmPort 8000 + +# === Infinity reranker === +reranker: + rerankerModelName: &rerankerModel "Alibaba-NLP/gte-multilingual-reranker-base" + servicePort: &rerankerPort 7997 + image: + repository: michaelf34/infinity + tag: latest + model: + id: Alibaba-NLP/gte-multilingual-reranker-base + command: + - v2 + - --model-id + - *rerankerModel + - --port + - *rerankerPort + replicas: 1 + runtimeClassName: nvidia + service: + type: ClusterIP + port: *rerankerPort + resources: + requests: + cpu: "4" + memory: "16Gi" + limits: + cpu: "4" + memory: "16Gi" +# === OpenRAG main app === +openrag: + image: + repository: ghcr.io/linagora/openrag + tag: latest + service: + type: ClusterIP + port: 8080 + resources: + requests: + cpu: "4" + memory: "16Gi" + limits: + cpu: "4" + memory: "16Gi" + replicas: 1 + +ingress: + enabled: true + className: "nginx" + host: "" + + paths: + openrag: + enabled: true + +# === Shared env (config + secrets) === +env: + config: + # LLM + BASE_URL: "https://example.com/v1/" + MODEL: "Mistral-Small-3.1-24B-Instruct-2503" + LLM_SEMAPHORE: "50" + + # VLM + VLM_BASE_URL: "https://example.com/v1/" + VLM_MODEL: "Qwen2.5-VL-7B-Instruct" + VLM_SEMAPHORE: "50" + + # App + APP_PORT: "8080" + APP_HOST: "0.0.0.0" + ENABLE_RAY_SERVE: "true" + RAY_SERVE_NUM_REPLICAS: "4" + RAY_SERVE_PORT: "80" + DISABLE_EXCEPTION_HANDLER: "true" + + WITH_CHAINLIT_UI: "false" + SAVE_UPLOADED_FILES: "false" + API_NUM_WORKERS: "8" + INDEXERUI_URL: "http://indexer-ui:3042" + + # Vector DB + VDB_HOST: "{{ .Release.Name }}-milvus" + VDB_PORT: "19530" + VDB_CONNECTOR_NAME: "milvus" + + # PostgreSQL + POSTGRES_HOST: "{{ .Release.Name }}-postgresql" + POSTGRES_PORT: *pgPort + POSTGRES_USER: *pgUser + POSTGRES_PASSWORD: *pgPass + + # Retriever + CONTEXTUAL_RETRIEVAL: "true" + RETRIEVER_TOP_K: "15" + + # Embedder + EMBEDDER_MODEL_NAME: *embedderModel + EMBEDDER_BASE_URL: "http://{{ .Release.Name }}-embedder-engine-service:{{ .Values.vllm.servingEngineSpec.servicePort }}/v1" + + # Reranker + RERANKER_MODEL: *rerankerModel + RERANKER_ENABLED: "true" + RERANKER_TOP_K: "4" + RERANKER_BASE_URL: "http://{{ .Release.Name }}-reranker:{{ .Values.reranker.servicePort }}" + RERANKER_MODEL_TYPE: "infinity" + + # Prompts + PROMPTS_DIR: "../prompts/example3" + COLBERT_LOAD_TORCH_EXTENSION_VERBOSE: "True" + + # Loaders + PDFLoader: "MarkerLoader" + XDG_CACHE_HOME: "/app/model_weights" + MARKER_MAX_TASKS_PER_CHILD: "10" + MARKER_MAX_PROCESSES: "15" + MARKER_MIN_PROCESSES: "3" + MARKER_POOL_SIZE: "3" + MARKER_NUM_GPUS: "0.6" + + # Ray + RAY_NUM_GPUS: "0.1" + RAY_POOL_SIZE: "3" + RAY_MAX_TASKS_PER_WORKER: "50" + RAY_DASHBOARD_PORT: "8265" + RAY_ADDRESS: "ray://raycluster-head-svc:10001" + RAY_task_retry_delay_ms: "3000" + RAY_ENABLE_UV_RUN_RUNTIME_ENV: "0" + + UV_LINK_MODE: "copy" + UV_CACHE_DIR: "/tmp/uv-cache" + + secrets: + API_KEY: "sk-xxxx" # LLM API KEY + VLM_API_KEY: "sk-xxxx" # VLM API KEY + EMBEDDER_API_KEY: "EMPTY" + AUTH_TOKEN: "sk-xxxx" # API KEY for OpenRAG + HF_TOKEN: "hf_xxxx" # HuggingFace token diff --git a/docs/kubernetes.md b/docs/kubernetes.md new file mode 100644 index 000000000..8d045e788 --- /dev/null +++ b/docs/kubernetes.md @@ -0,0 +1,51 @@ +# Deploying OpenRAG on Kubernetes + +This guide explains how to deploy the **OpenRAG** stack on a Kubernetes cluster using Helm. + +--- + +## Prerequisites + +- A **Kubernetes cluster** with **GPU nodes** available (NVIDIA runtime) and nvidia-gpu-operator installed. +- A **StorageClass** that supports **ReadWriteMany** (`RWX`) access mode. + This is required because the Ray cluster workers and the OpenRAG app need to access the same shared volumes (e.g. for `.venv`, model weights, logs, data). +- If using ingress, the ingress-nginx controller needs to be installed on the cluster. + +--- + +## Steps + +1. **Create a `values.yaml` file**: + + - Copy or create a new `values.yaml` at the root of your repo. + - You can see the full example file inside the chart: + [../charts/openrag-stack/values.yaml](../charts/openrag-stack/values.yaml) + - Customize the values you need (e.g., image tags, resources, ingress host, storage class, environment variables, secrets). + +2. **Set environment and secrets**: + + - Edit the `env.config` and `env.secrets` sections in your `values.yaml`. + - Secrets (API keys, tokens, Hugging Face credentials, etc.) will be mounted into the cluster as Kubernetes secrets. + +3. **Install or upgrade the release from GHCR**: + + ```bash + helm upgrade --install openrag oci://ghcr.io/linagora/openrag-stack -f ./values.yaml --version 0.1.0 + ``` + + - `openrag` is the Helm release name. + - `oci://ghcr.io/linagora/openrag-stack` is the remote chart location. + - `-f ./values.yaml` specifies your custom configuration. + - `--version 0.1.0` ensures you deploy a specific chart version. + +--- + +## Notes + +- If using a public IP instead of a hostname, you can leave `ingress.host` empty in your `values.yaml`. + The ingress will then match all hosts. + +- If you later configure a hostname + TLS (via cert-manager), just update `ingress.host` and redeploy. + +- Ensure your GPU nodes have the correct NVIDIA drivers and `nvidia` `RuntimeClass` configured. + diff --git a/openrag/api.py b/openrag/api.py index 1b3029620..5f9552519 100644 --- a/openrag/api.py +++ b/openrag/api.py @@ -63,6 +63,9 @@ def __init__(self, config): INDEXERUI_COMPOSE_FILE = os.getenv("INDEXERUI_COMPOSE_FILE", None) INDEXERUI_PORT: Optional[str] = os.getenv("INDEXERUI_PORT", "3042") +DISABLE_EXCEPTION_HANDLER: bool = ( + os.getenv("DISABLE_EXCEPTION_HANDLER", "false").lower() == "true" +) security = HTTPBearer() @@ -84,10 +87,12 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): # Exception handlers -@app.exception_handler(OpenRAGError) -async def openrag_exception_handler(request: Request, exc: OpenRAGError): - logger.error("OpenRAGError occurred", error=str(exc)) - return JSONResponse(status_code=exc.status_code, content=exc.to_dict()) +if not DISABLE_EXCEPTION_HANDLER: + + @app.exception_handler(OpenRAGError) + async def openrag_exception_handler(request: Request, exc: OpenRAGError): + logger.error("OpenRAGError occurred", error=str(exc)) + return JSONResponse(status_code=exc.status_code, content=exc.to_dict()) # Add CORS middleware diff --git a/openrag/components/indexer/loaders/pdf_loaders/marker.py b/openrag/components/indexer/loaders/pdf_loaders/marker.py index 1ad281451..b6b1555f4 100644 --- a/openrag/components/indexer/loaders/pdf_loaders/marker.py +++ b/openrag/components/indexer/loaders/pdf_loaders/marker.py @@ -160,7 +160,7 @@ def __init__(self): self.actors = [MarkerWorker.remote() for _ in range(self.pool_size)] self._queue: asyncio.Queue[ray.actor.ActorHandle] = asyncio.Queue() - for _ in range(self.pool_size): + for _ in range(self.max_processes): for actor in self.actors: self._queue.put_nowait(actor)