diff --git a/.github/workflows/package-build.yml b/.github/workflows/package-build.yml
new file mode 100644
index 00000000..f6cd3296
--- /dev/null
+++ b/.github/workflows/package-build.yml
@@ -0,0 +1,88 @@
+name: Build the Python package
+
+# Builds the sdist and wheel, checks the wheel carries the data the app reads
+# at runtime and nothing from the checkout, then installs it into a clean
+# environment and runs the docsgpt command.
+
+on:
+ pull_request:
+ paths:
+ - 'pyproject.toml'
+ - 'uv.lock'
+ - 'docsgpt/**'
+ - 'README.md'
+ - 'LICENSE'
+ - '.github/workflows/package-build.yml'
+ push:
+ branches: [main]
+ paths:
+ - 'pyproject.toml'
+ - 'uv.lock'
+ - 'docsgpt/**'
+ - 'README.md'
+ - 'LICENSE'
+ - '.github/workflows/package-build.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ persist-credentials: false
+
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+
+ - name: Build the sdist and wheel
+ run: uv build
+
+ - name: Check the archives
+ run: |
+ uvx twine check --strict dist/*
+ python3 - <<'PY'
+ import glob
+ import zipfile
+
+ names = set(zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist())
+ for required in (
+ "docsgpt/cli.py",
+ "docsgpt/alembic.ini",
+ "docsgpt/alembic/env.py",
+ "docsgpt/alembic/script.py.mako",
+ "docsgpt/prompts/chat_reduce_prompt.txt",
+ "docsgpt/prompts/fragments/identity.txt",
+ "docsgpt/core/models/_defaults.yaml",
+ "docsgpt/seed/config/premade_agents.yaml",
+ ):
+ assert required in names, f"missing from the wheel: {required}"
+ for banned in (
+ "docsgpt/Dockerfile",
+ "docsgpt/requirements.txt",
+ "docsgpt/requirements-docling.txt",
+ "docsgpt/index.faiss",
+ "docsgpt/index.pkl",
+ "application/__init__.py",
+ ):
+ assert banned not in names, f"must not ship: {banned}"
+ stray = [n for n in names if n.startswith(("docsgpt/indexes/", "docsgpt/inputs/", "docsgpt/vectors/"))]
+ assert not stray, stray
+ print(f"wheel ok: {len(names)} files")
+ PY
+
+ - name: Install the wheel into a clean environment and run the command
+ run: |
+ uv venv "$RUNNER_TEMP/docsgpt-venv"
+ uv pip install --python "$RUNNER_TEMP/docsgpt-venv/bin/python" dist/*.whl
+ "$RUNNER_TEMP/docsgpt-venv/bin/docsgpt" --version
+ "$RUNNER_TEMP/docsgpt-venv/bin/docsgpt" --help
+ "$RUNNER_TEMP/docsgpt-venv/bin/docsgpt" api --help
+ "$RUNNER_TEMP/docsgpt-venv/bin/python" -c "from docsgpt.core import paths; print('home:', paths.home_dir())"
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: dist
+ path: dist/
diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
new file mode 100644
index 00000000..8e977c56
--- /dev/null
+++ b/.github/workflows/pypi-publish.yml
@@ -0,0 +1,86 @@
+name: Publish to PyPI
+
+# Trusted publishing: PyPI trusts this workflow file in arc53/DocsGPT running
+# in the `pypi` environment, so no API token is stored. A published release
+# (which the backend-release workflow creates when docsgpt/version.py changes
+# on main) publishes to PyPI; a manual run publishes to TestPyPI by default.
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ target:
+ description: Package index to publish to
+ type: choice
+ options: [testpypi, pypi]
+ default: testpypi
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ if: github.repository == 'arc53/DocsGPT'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ persist-credentials: false
+
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
+ with:
+ # A publishing workflow must not restore a cache another job could have poisoned.
+ enable-cache: false
+
+ - name: Build the sdist and wheel
+ run: uv build
+
+ - name: The archives carry the released version
+ if: github.event_name == 'release'
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ run: |
+ ls "dist/docsgpt-${TAG}.tar.gz" "dist/docsgpt-${TAG}-py3-none-any.whl"
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
+
+ publish-pypi:
+ if: github.event_name == 'release' || inputs.target == 'pypi'
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/project/docsgpt/
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: dist
+ path: dist/
+
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
+
+ publish-testpypi:
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/project/docsgpt/
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: dist
+ path: dist/
+
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
+ with:
+ repository-url: https://test.pypi.org/legacy/
diff --git a/.gitignore b/.gitignore
index d2e4019f..8693a33e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -206,3 +206,6 @@ tests/e2e/.e2e-last-run.json
docsgpt/core/models/foundry_test_internal.yaml
bench/
/benchmark/
+
+# Python package build output (uv build)
+dist/
diff --git a/AGENTS.md b/AGENTS.md
index eb4d9708..8b878d56 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,6 +37,13 @@ uv pip install -r docsgpt/requirements.txt # or: pip install -r docsgpt/require
# (the file adds the PyTorch CPU index; prefer `uv sync --extra docling`).
```
+The backend is also an installable package (`pyproject.toml`, hatchling).
+`uv sync` installs it editable and puts a `docsgpt` command on PATH:
+`docsgpt api --reload`, `docsgpt worker`, `docsgpt migrate`,
+`docsgpt prefetch-models`, `docsgpt verify-offline`. Runtime data (`.env`,
+`inputs/`, `indexes/`) lives in the checkout by default; `DOCSGPT_HOME` and
+`DOCSGPT_ENV_FILE` move it (see `docsgpt/core/paths.py`).
+
Dependencies are declared in `pyproject.toml` and locked in `uv.lock`; the
`docsgpt/requirements*.txt` files are exported from the lock. To add or
bump a package: edit `pyproject.toml`, run `uv lock`, then
diff --git a/docs/content/Deploying/Pip-Install.mdx b/docs/content/Deploying/Pip-Install.mdx
new file mode 100644
index 00000000..41586200
--- /dev/null
+++ b/docs/content/Deploying/Pip-Install.mdx
@@ -0,0 +1,86 @@
+---
+title: Install with pip
+description: Run the DocsGPT backend from the PyPI package, inside your own Python environment.
+---
+
+import { Callout } from 'nextra/components'
+
+# Install with pip
+
+The DocsGPT backend is on PyPI as [`docsgpt`](https://pypi.org/project/docsgpt/): the API server, the Celery worker and the maintenance scripts in one package, behind a single `docsgpt` command. Use it when you want DocsGPT inside your own Python environment or process manager rather than the [Docker images](/Deploying/Docker-Deploying).
+
+
+ The package is the backend only. The web UI ships in the Docker images and in the repository under `frontend/`; serving it from the package is planned.
+
+
+## Requirements
+
+- Python 3.12 or newer
+- [PostgreSQL](/Deploying/Postgres-Migration) for user data, with the `vector` extension if you set `VECTOR_STORE=pgvector`
+- Redis for the task queue and the cache
+- An LLM: an API key for a hosted provider, or a local model server
+
+## Install
+
+```bash
+python -m venv .venv && source .venv/bin/activate
+pip install docsgpt
+```
+
+Extras add the optional engines:
+
+```bash
+pip install "docsgpt[docling]" # DOC_PARSER_ENGINE=docling: OCR backend, structured output
+pip install "docsgpt[milvus]" # VECTOR_STORE=milvus
+```
+
+The `docling` extra pulls in PyTorch. On Linux, PyPI's torch wheels bring the CUDA stack with them; on a CPU-only machine add `--extra-index-url https://download.pytorch.org/whl/cpu` to the install command to get the CPU build instead. `uv pip install` and `pipx install` work the same way.
+
+## Configure
+
+DocsGPT keeps its runtime files in a **data home**: the `.env` file it reads settings from, uploaded files under `inputs/` and vector indexes under `indexes/`. The data home is the directory you run the commands from, or the directory `DOCSGPT_HOME` points to. `DOCSGPT_ENV_FILE` points at a `.env` kept somewhere else.
+
+Create a `.env` in the data home. The minimum for a hosted LLM:
+
+```ini
+# LLM provider and model: see App Configuration for the options
+LLM_PROVIDER=openai
+LLM_NAME=
+API_KEY=
+
+# User data
+POSTGRES_URI=postgresql://docsgpt:docsgpt@localhost:5432/docsgpt
+
+# Worker-to-API authentication (required for uploads)
+INTERNAL_KEY=
+```
+
+Redis defaults to `localhost:6379` (`CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND`, `CACHE_REDIS_URL`). Every setting is listed in [App Configuration](/Deploying/DocsGPT-Settings).
+
+## Run
+
+```bash
+docsgpt migrate # create the database if it is missing and apply the migrations
+docsgpt api # the API on http://localhost:7091
+docsgpt worker # in a second terminal: the Celery worker, with the scheduler
+```
+
+The API applies pending migrations when it starts (`AUTO_MIGRATE`), so `docsgpt migrate` is the explicit step for deployments that want the schema in place before the first request or that run the API with a restricted database role.
+
+The worker is not optional: query embedding runs on it, so search fails without one. `docsgpt worker --help` lists the queue, concurrency and pool options; `--no-beat` starts a worker without the scheduler when another worker already runs it.
+
+Other commands:
+
+- `docsgpt api --reload`: a development server with auto-reload.
+- `docsgpt prefetch-models`: download the embedding, tokenizer and parser models ahead of time, for machines that go offline.
+- `docsgpt verify-offline`: check that a prepared install starts with networking off.
+- `docsgpt reembed`: re-embed every index after changing `EMBEDDINGS_NAME` (see [Upgrading](/upgrading)).
+
+## Upgrade
+
+```bash
+pip install -U docsgpt
+docsgpt migrate
+```
+
+Read the [upgrade notes](/upgrading) first when moving between minor versions.
diff --git a/docs/content/Deploying/_meta.js b/docs/content/Deploying/_meta.js
index 5f414192..c14234bd 100644
--- a/docs/content/Deploying/_meta.js
+++ b/docs/content/Deploying/_meta.js
@@ -15,6 +15,10 @@ export default {
"title": "🛳️ Docker Setup",
"href": "/Deploying/Docker-Deploying"
},
+ "Pip-Install": {
+ "title": "🐍 Install with pip",
+ "href": "/Deploying/Pip-Install"
+ },
"Development-Environment": {
"title": "🛠️Development Environment",
"href": "/Deploying/Development-Environment"
diff --git a/docsgpt/api/internal/routes.py b/docsgpt/api/internal/routes.py
index 6c097207..d849c77f 100755
--- a/docsgpt/api/internal/routes.py
+++ b/docsgpt/api/internal/routes.py
@@ -5,6 +5,7 @@ from flask import Blueprint, request, send_from_directory, jsonify
from werkzeug.utils import secure_filename
import logging
+from docsgpt.core.paths import home_dir
from docsgpt.core.settings import settings
from docsgpt.storage.db.base_repository import looks_like_uuid
from docsgpt.storage.db.repositories.sources import SourcesRepository
@@ -14,9 +15,7 @@ from docsgpt.storage.storage_creator import StorageCreator
logger = logging.getLogger(__name__)
-current_dir = os.path.dirname(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-)
+current_dir = str(home_dir())
internal = Blueprint("internal", __name__)
diff --git a/docsgpt/api/user/base.py b/docsgpt/api/user/base.py
index 8cd85b8a..f6cd6f58 100644
--- a/docsgpt/api/user/base.py
+++ b/docsgpt/api/user/base.py
@@ -32,10 +32,6 @@ from docsgpt.vectorstore.vector_creator import VectorCreator
storage = StorageCreator.get_storage()
-current_dir = os.path.dirname(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-)
-
def generate_minute_range(start_date, end_date):
"""Generate a dictionary with minute-level time ranges."""
return {
diff --git a/docsgpt/app.py b/docsgpt/app.py
index 96b392aa..81c303dc 100644
--- a/docsgpt/app.py
+++ b/docsgpt/app.py
@@ -11,6 +11,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
from docsgpt.auth import handle_auth
from docsgpt.core import log_context
+from docsgpt.core.paths import env_file
from docsgpt.core.logging_config import setup_logging
setup_logging()
@@ -53,7 +54,7 @@ if platform.system() == "Windows":
import pathlib
pathlib.PosixPath = pathlib.WindowsPath
-dotenv.load_dotenv()
+dotenv.load_dotenv(env_file())
# Self-bootstrap the user-data Postgres DB. Runs before any blueprint or
# repository touches the engine, so the first request can't race the
diff --git a/docsgpt/cli.py b/docsgpt/cli.py
new file mode 100644
index 00000000..4c227f83
--- /dev/null
+++ b/docsgpt/cli.py
@@ -0,0 +1,145 @@
+"""The ``docsgpt`` command: run the API, the worker and the maintenance scripts.
+
+Every subcommand imports what it needs when it runs, so ``docsgpt --help``
+stays instant and does not touch the database.
+"""
+
+from __future__ import annotations
+
+import argparse
+import logging
+import os
+import sys
+from typing import Optional, Sequence
+
+from docsgpt.version import __version__
+
+DEFAULT_HOST = "0.0.0.0"
+DEFAULT_PORT = 7091
+DEFAULT_QUEUES = "docsgpt,parsing,embeddings"
+
+
+def _api(args: argparse.Namespace) -> int:
+ """Serve the ASGI app: gunicorn with the bounded-drain uvicorn worker, or uvicorn when reloading."""
+ if args.reload or sys.platform == "win32":
+ import uvicorn
+
+ uvicorn.run("docsgpt.asgi:asgi_app", host=args.host, port=args.port, reload=args.reload)
+ return 0
+
+ from gunicorn.app.wsgiapp import run as gunicorn_run
+
+ argv = [
+ "gunicorn",
+ "-w", str(args.workers),
+ "-k", "docsgpt.gunicorn_worker.BoundedDrainUvicornWorker",
+ "--bind", f"{args.host}:{args.port}",
+ "--timeout", "180",
+ "--graceful-timeout", "120",
+ "--keep-alive", "5",
+ "--max-requests", "5000",
+ "--max-requests-jitter", "500",
+ "--config", "python:docsgpt.gunicorn_conf",
+ ]
+ if os.path.isdir("/dev/shm"):
+ argv += ["--worker-tmp-dir", "/dev/shm"]
+ argv.append("docsgpt.asgi:asgi_app")
+ sys.argv = argv
+ gunicorn_run()
+ return 0
+
+
+def _worker(args: argparse.Namespace) -> int:
+ """Run the Celery worker (with the beat scheduler unless ``--no-beat``)."""
+ from docsgpt.app import celery
+
+ argv = ["worker", "-l", args.loglevel, "-Q", args.queues]
+ if args.concurrency:
+ argv += ["--concurrency", str(args.concurrency)]
+ if args.beat:
+ argv.append("-B")
+ pool = args.pool or ("solo" if sys.platform == "darwin" else None)
+ if pool:
+ argv += ["--pool", pool]
+ celery.worker_main(argv)
+ return 0
+
+
+def _migrate(args: argparse.Namespace) -> int:
+ """Create the Postgres database if asked and migrate it to head."""
+ from docsgpt.core.settings import settings
+ from docsgpt.storage.db.bootstrap import ensure_database_ready
+
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
+ if not settings.POSTGRES_URI:
+ print("POSTGRES_URI is not set; nothing to migrate.", file=sys.stderr)
+ return 2
+ ensure_database_ready(
+ settings.POSTGRES_URI,
+ create_db=args.create_db,
+ migrate=True,
+ logger=logging.getLogger("docsgpt.migrate"),
+ )
+ return 0
+
+
+# Maintenance scripts keep their own argument parsers; the command hands
+# everything after the script name to them untouched (argparse would try to
+# interpret the options itself).
+SCRIPTS = {
+ "prefetch-models": ("prefetch_models", "download the embedding, tokenizer and parser models"),
+ "verify-offline": ("verify_offline", "check that the install runs with networking off"),
+ "reembed": ("reembed", "re-embed every index with the configured embedding model"),
+}
+
+
+def _run_script(module: str, argv: list[str]) -> int:
+ import importlib
+
+ return int(importlib.import_module(f"docsgpt.scripts.{module}").main(argv) or 0)
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(prog="docsgpt", description="DocsGPT: private AI for agents, assistants and search.")
+ parser.add_argument("--version", action="version", version=f"docsgpt {__version__}")
+ commands = parser.add_subparsers(dest="command", metavar="")
+
+ api = commands.add_parser("api", help="serve the HTTP API")
+ api.add_argument("--host", default=DEFAULT_HOST)
+ api.add_argument("--port", type=int, default=DEFAULT_PORT)
+ api.add_argument("--workers", type=int, default=1, help="gunicorn worker processes (default: 1)")
+ api.add_argument("--reload", action="store_true", help="development mode: uvicorn with auto-reload")
+ api.set_defaults(func=_api)
+
+ worker = commands.add_parser("worker", help="run the Celery worker (and the scheduler)")
+ worker.add_argument("-Q", "--queues", default=DEFAULT_QUEUES, help=f"queues to consume (default: {DEFAULT_QUEUES})")
+ worker.add_argument("--concurrency", type=int, help="worker processes (default: one per CPU)")
+ worker.add_argument("--pool", help="celery pool (default: prefork; solo on macOS)")
+ worker.add_argument("-l", "--loglevel", default="INFO")
+ worker.add_argument("--no-beat", dest="beat", action="store_false", help="do not embed the beat scheduler")
+ worker.set_defaults(func=_worker)
+
+ migrate = commands.add_parser("migrate", help="create the database if needed and run the migrations")
+ migrate.add_argument("--no-create", dest="create_db", action="store_false", help="fail instead of creating a missing database")
+ migrate.set_defaults(func=_migrate)
+
+ for name, (module, help_text) in SCRIPTS.items():
+ commands.add_parser(name, help=f"{help_text} (docsgpt.scripts.{module})", add_help=False)
+
+ return parser
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ argv = list(sys.argv[1:] if argv is None else argv)
+ if argv and argv[0] in SCRIPTS:
+ return _run_script(SCRIPTS[argv[0]][0], argv[1:])
+ parser = build_parser()
+ args = parser.parse_args(argv)
+ if not args.command:
+ parser.print_help()
+ return 2
+ return args.func(args)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/docsgpt/core/paths.py b/docsgpt/core/paths.py
new file mode 100644
index 00000000..48296d21
--- /dev/null
+++ b/docsgpt/core/paths.py
@@ -0,0 +1,47 @@
+"""Where DocsGPT keeps its files.
+
+Three locations matter. The package directory holds code and the data that
+ships with it (prompts, model catalogs, migrations). A checkout, when the
+package is imported from one, is the directory holding ``pyproject.toml``.
+The data home is where runtime data lives: ``.env``, ``inputs``, ``indexes``.
+
+The home is ``DOCSGPT_HOME`` when set, else the checkout, else the current
+directory. That keeps a source checkout and the Docker image (which runs from
+``/app`` with the package beside it) behaving as before, and gives a
+``pip install docsgpt`` user a home that is not ``site-packages``.
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+HOME_ENV = "DOCSGPT_HOME"
+ENV_FILE_ENV = "DOCSGPT_ENV_FILE"
+
+
+def package_dir() -> Path:
+ """The installed ``docsgpt`` package directory."""
+ return Path(__file__).resolve().parent.parent
+
+
+def checkout_root() -> Path | None:
+ """The source checkout the package is imported from, or None when installed."""
+ root = package_dir().parent
+ return root if (root / "pyproject.toml").is_file() else None
+
+
+def home_dir() -> Path:
+ """Directory for runtime data: ``DOCSGPT_HOME``, else the checkout, else cwd."""
+ configured = os.environ.get(HOME_ENV)
+ if configured:
+ return Path(configured).expanduser().resolve()
+ return checkout_root() or Path.cwd()
+
+
+def env_file() -> Path:
+ """The ``.env`` file settings load: ``DOCSGPT_ENV_FILE``, else ``/.env``."""
+ configured = os.environ.get(ENV_FILE_ENV)
+ if configured:
+ return Path(configured).expanduser()
+ return home_dir() / ".env"
diff --git a/docsgpt/core/settings.py b/docsgpt/core/settings.py
index f34841ff..46f3f34b 100644
--- a/docsgpt/core/settings.py
+++ b/docsgpt/core/settings.py
@@ -1,17 +1,17 @@
import os
-from pathlib import Path
from typing import Optional
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
-current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-
-from docsgpt.core.db_uri import ( # noqa: E402
+from docsgpt.core.db_uri import (
normalize_pgvector_connection_string,
normalize_postgres_uri,
)
+from docsgpt.core.paths import env_file, home_dir
+
+# Runtime data home (DOCSGPT_HOME, the checkout, or cwd); see docsgpt.core.paths.
+current_dir = str(home_dir())
class Settings(BaseSettings):
@@ -581,6 +581,4 @@ class Settings(BaseSettings):
return stripped
-# Project root is one level above application/
-path = Path(__file__).parent.parent.parent.absolute()
-settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
+settings = Settings(_env_file=env_file(), _env_file_encoding="utf-8")
diff --git a/docsgpt/storage/local.py b/docsgpt/storage/local.py
index ab23e1fb..ba2099c7 100644
--- a/docsgpt/storage/local.py
+++ b/docsgpt/storage/local.py
@@ -4,6 +4,7 @@ import shutil
import tempfile
from typing import BinaryIO, List, Callable
+from docsgpt.core.paths import home_dir
from docsgpt.storage.base import BaseStorage
@@ -15,11 +16,10 @@ class LocalStorage(BaseStorage):
Initialize local storage.
Args:
- base_dir: Base directory for all operations. If None, uses current directory.
+ base_dir: Base directory for all operations. Defaults to the data home
+ (see ``docsgpt.core.paths.home_dir``).
"""
- self.base_dir = base_dir or os.path.dirname(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- )
+ self.base_dir = base_dir or str(home_dir())
def _get_full_path(self, path: str) -> str:
"""Get absolute path by combining base_dir and path.
diff --git a/docsgpt/worker.py b/docsgpt/worker.py
index c1c6cb78..b300f1a3 100755
--- a/docsgpt/worker.py
+++ b/docsgpt/worker.py
@@ -380,10 +380,6 @@ def generate_random_string(length):
return "".join([string.ascii_letters[i % 52] for i in range(length)])
-current_dir = os.path.dirname(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-)
-
# Zip extraction security limits. Kept as module constants for backward
# compatibility with worker callers/tests; values come from operator settings.
MAX_UNCOMPRESSED_SIZE = settings.UPLOAD_MAX_ARCHIVE_BYTES
diff --git a/pyproject.toml b/pyproject.toml
index d43c281b..3577d6c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,58 +1,80 @@
+[build-system]
+requires = ["hatchling>=1.27,<2"]
+build-backend = "hatchling.build"
+
[project]
name = "docsgpt"
-# Bump together with docsgpt/version.py and frontend/package.json.
-version = "0.19.0"
+# The version is read from docsgpt/version.py (the release workflow reads the
+# same file); bump it there.
+dynamic = ["version"]
description = "DocsGPT backend: chat with your documents, agents, and tools."
readme = "README.md"
requires-python = ">=3.12"
-license = { file = "LICENSE" }
+license = "MIT"
+license-files = ["LICENSE"]
+authors = [{ name = "Arc53" }]
+keywords = ["rag", "llm", "agents", "documents", "chat", "search"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Web Environment",
+ "Framework :: Flask",
+ "Intended Audience :: Developers",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+]
-# Direct dependencies only. Transitive pins live in uv.lock; the pip-facing
-# files under docsgpt/ (requirements*.txt) are exported from that lock by
-# scripts/export_requirements.sh and must not be edited by hand.
+# Direct dependencies only, as compatible ranges: the floor is the version
+# uv.lock pins, the ceiling the next major (next minor below 1.0), so the
+# PyPI package installs next to other packages. The pip-facing files under
+# docsgpt/ (requirements*.txt) are exported from the lock by
+# scripts/export_requirements.sh and must not be edited by hand; Docker, CI
+# and the checkout install those exact versions.
dependencies = [
- "a2wsgi==1.10.10",
+ "a2wsgi>=1.10.10,<2",
"alembic>=1.13,<2",
- "anthropic==0.121.0",
- "beautifulsoup4==4.15.0",
- "boto3==1.43.67",
- "cel-python==0.5.0",
- "celery==5.6.3",
- "celery-redbeat==2.4.2",
- "croniter==6.2.4",
- "cryptography==50.0.0",
- "dataclasses-json==0.6.7",
- "daytona==0.205.1",
- "ddgs>=8.0.0",
- "defusedxml==0.7.1",
- "docx2txt==0.9",
- "elevenlabs==2.62.0",
- "faiss-cpu==1.15.0",
- "fast-ebook",
+ "anthropic>=0.121.0,<0.122",
+ "beautifulsoup4>=4.15.0,<5",
+ "boto3>=1.43.67,<2",
+ "cel-python>=0.5.0,<0.6",
+ "celery>=5.6.3,<6",
+ "celery-redbeat>=2.4.2,<3",
+ "croniter>=6.2.4,<7",
+ "cryptography>=50.0.0,<51",
+ "dataclasses-json>=0.6.7,<0.7",
+ "daytona>=0.205.1,<0.206",
+ "ddgs>=8.0.0,<10",
+ "defusedxml>=0.7.1,<0.8",
+ "docx2txt>=0.9,<0.10",
+ "elevenlabs>=2.62.0,<3",
+ "faiss-cpu>=1.15.0,<2",
+ "fast-ebook>=0.2.0,<0.3",
# Default document converter (DOC_PARSER_ENGINE=anydoc): a Rust extension
# with no model downloads. The docling engine is the `docling` extra.
- "firecrawl-anydoc==0.2.3",
- "fastembed==0.8.0",
- "fastmcp==3.4.6",
- "Flask==3.1.3",
- "flask-restx==1.3.2",
- "google-api-python-client==2.198.0",
- "google-auth-oauthlib==1.4.0",
- "google-genai==2.17.0",
- "gTTS==2.5.4",
- "gunicorn==26.0.0",
- "jinja2==3.1.6",
- "kombu==5.6.2",
- "markdownify==1.2.3",
- "msal==1.37.0",
- "networkx==3.6.1",
- "numpy==2.5.1",
+ "firecrawl-anydoc>=0.2.3,<0.3",
+ "fastembed>=0.8.0,<0.9",
+ "fastmcp>=3.4.6,<4",
+ "Flask>=3.1.3,<4",
+ "flask-restx>=1.3.2,<2",
+ "google-api-python-client>=2.198.0,<3",
+ "google-auth-oauthlib>=1.4.0,<2",
+ "google-genai>=2.17.0,<3",
+ "gTTS>=2.5.4,<3",
+ "gunicorn>=26.0.0,<27",
+ "jinja2>=3.1.6,<4",
+ "kombu>=5.6.2,<6",
+ "markdownify>=1.2.3,<2",
+ "msal>=1.37.0,<2",
+ "networkx>=3.6.1,<4",
+ "numpy>=2.5.1,<3",
# fastembed's runtime: local embeddings execute on it.
- "onnxruntime==1.28.0",
- "openai==2.53.0",
- "openapi3-parser==1.1.22",
+ "onnxruntime>=1.28.0,<2",
+ "openai>=2.53.0,<3",
+ "openapi3-parser>=1.1.22,<2",
# pandas reads .xlsx through openpyxl but does not depend on it.
- "openpyxl==3.1.5",
+ "openpyxl>=3.1.5,<4",
"opentelemetry-distro>=0.50b0,<1",
"opentelemetry-exporter-otlp>=1.29.0,<2",
"opentelemetry-instrumentation-celery>=0.50b0,<1",
@@ -63,59 +85,71 @@ dependencies = [
"opentelemetry-instrumentation-requests>=0.50b0,<1",
"opentelemetry-instrumentation-sqlalchemy>=0.50b0,<1",
"opentelemetry-instrumentation-starlette>=0.50b0,<1",
- "pandas==3.0.5",
- "pdf2image>=1.17.0",
+ "pandas>=3.0.5,<4",
+ "pdf2image>=1.17.0,<2",
"pgvector>=0.5,<1",
- "pillow==12.3.0",
- "praw==8.0.2",
+ "pillow>=12.3.0,<13",
+ "praw>=8.0.2,<9",
"psycopg[binary,pool]>=3.1,<4",
- "pydantic",
- "pydantic-settings",
- "pypdf==6.15.0",
- "pypdfium2==5.12.1",
- "python-dateutil==2.9.0.post0",
- "python-dotenv",
- "python-jose==3.5.0",
- "python-pptx==1.0.2",
- "PyYAML",
- "qdrant-client==1.19.0",
- "redis==7.4.0",
- "requests==2.34.2",
- "retry==0.9.2",
+ "pydantic>=2.13.5,<3",
+ "pydantic-settings>=2.15.0,<3",
+ "pypdf>=6.15.0,<7",
+ "pypdfium2>=5.12.1,<6",
+ "python-dateutil>=2.9.0,<3",
+ "python-dotenv>=1.2.3,<2",
+ "python-jose>=3.5.0,<4",
+ "python-pptx>=1.0.2,<2",
+ "PyYAML>=6.0.3,<7",
+ "qdrant-client>=1.19.0,<2",
+ "redis>=7.4.0,<8",
+ "requests>=2.34.2,<3",
+ "retry>=0.9.2,<0.10",
"sqlalchemy>=2.0,<3",
"starlette>=1.0,<2",
- "tiktoken==0.13.0",
- "tldextract==5.3.2",
- "tokenizers==0.22.2",
- "tqdm==4.67.3",
+ "tiktoken>=0.13.0,<0.14",
+ "tldextract>=5.3.2,<6",
+ "tokenizers>=0.22.2,<0.23",
+ "tqdm>=4.67.3,<5",
"uvicorn[standard]>=0.30,<1",
"uvicorn-worker>=0.4,<1",
- "websocket-client==1.9.0",
- "werkzeug>=3.1.0",
+ "websocket-client>=1.9.0,<2",
+ "werkzeug>=3.1.0,<4",
]
[project.optional-dependencies]
# Docling parser engine: DOC_PARSER_ENGINE=docling, the docling OCR backend
# (layout-model hybrid OCR, ocrmac/rapidocr engines), .adoc/.vtt/.xml
# attachment parsing, and read_document's `structured` output. Pulls torch and
-# transformers; on Linux torch resolves from the CPU-only PyTorch index (see
-# [tool.uv.sources]) so the extra does not drag the CUDA stack in.
+# transformers; with uv on Linux torch resolves from the CPU-only PyTorch index
+# (see [tool.uv.sources]) so the extra does not drag the CUDA stack in. pip
+# users on Linux get the same by adding
+# --extra-index-url https://download.pytorch.org/whl/cpu.
docling = [
- "docling==2.119.0",
- "rapidocr==3.9.2",
- # docling's model stack. Declared here (not left transitive) so the pins
+ "docling>=2.119.0,<3",
+ "rapidocr>=3.9.2,<4",
+ # docling's model stack. Declared here (not left transitive) so the floors
# hold and the CPU index source below applies. transformers is capped by
# docling-core at <5.9: 5.9+ breaks the PDF layout model on Apple Silicon.
- "torch==2.11.0",
- "torchvision==0.26.0",
- "transformers==5.8.1",
+ "torch>=2.11.0,<3",
+ "torchvision>=0.26.0,<0.27",
+ "transformers>=5.8.1,<5.9",
]
# VECTOR_STORE=milvus. milvus-lite (the embedded server) pulls pyarrow.
milvus = [
- "pymilvus==3.0.1",
- "milvus-lite==3.2.0; sys_platform != 'win32'",
+ "pymilvus>=3.0.1,<4",
+ "milvus-lite>=3.2.0,<4; sys_platform != 'win32'",
]
+[project.scripts]
+docsgpt = "docsgpt.cli:main"
+
+[project.urls]
+Homepage = "https://www.docsgpt.cloud/"
+Documentation = "https://docs.docsgpt.cloud/"
+Repository = "https://github.com/arc53/DocsGPT"
+Issues = "https://github.com/arc53/DocsGPT/issues"
+Changelog = "https://github.com/arc53/DocsGPT/releases"
+
[dependency-groups]
# Mirrors tests/requirements.txt for `uv sync`; pip users install that file.
dev = [
@@ -131,10 +165,36 @@ dev = [
"ruff",
]
-[tool.uv]
-# The repo is run in place (`application.*` imported from the checkout), not
-# installed as a distribution.
-package = false
+[tool.hatch.version]
+path = "docsgpt/version.py"
+
+# The wheel is the docsgpt package with the data it reads at runtime: prompts,
+# model catalogs, the seed config, alembic.ini and the migrations. Build inputs
+# (Dockerfile, exported requirements), the sample index and local runtime data
+# stay out. The one-release `application` import alias is checkout-only.
+[tool.hatch.build.targets.wheel]
+packages = ["docsgpt"]
+exclude = [
+ "docsgpt/Dockerfile",
+ "docsgpt/requirements*.txt",
+ "docsgpt/index.faiss",
+ "docsgpt/index.pkl",
+ "docsgpt/indexes/",
+ "docsgpt/inputs/",
+ "docsgpt/vectors/",
+]
+
+[tool.hatch.build.targets.sdist]
+include = ["/docsgpt"]
+exclude = [
+ "docsgpt/Dockerfile",
+ "docsgpt/requirements*.txt",
+ "docsgpt/index.faiss",
+ "docsgpt/index.pkl",
+ "docsgpt/indexes/",
+ "docsgpt/inputs/",
+ "docsgpt/vectors/",
+]
[[tool.uv.index]]
name = "pytorch-cpu"
diff --git a/tests/core/test_paths.py b/tests/core/test_paths.py
new file mode 100644
index 00000000..54c74d19
--- /dev/null
+++ b/tests/core/test_paths.py
@@ -0,0 +1,43 @@
+"""Data home and .env discovery (docsgpt.core.paths)."""
+
+from pathlib import Path
+
+from docsgpt.core import paths
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+
+
+class TestHomeDir:
+ def test_the_checkout_wins_over_cwd(self, monkeypatch, tmp_path):
+ monkeypatch.delenv(paths.HOME_ENV, raising=False)
+ monkeypatch.chdir(tmp_path)
+ assert paths.checkout_root() == REPO_ROOT
+ assert paths.home_dir() == REPO_ROOT
+
+ def test_the_env_var_wins(self, monkeypatch, tmp_path):
+ monkeypatch.setenv(paths.HOME_ENV, str(tmp_path))
+ assert paths.home_dir() == tmp_path.resolve()
+
+ def test_an_installed_package_falls_back_to_cwd(self, monkeypatch, tmp_path):
+ monkeypatch.delenv(paths.HOME_ENV, raising=False)
+ monkeypatch.setattr(paths, "checkout_root", lambda: None)
+ monkeypatch.chdir(tmp_path)
+ assert paths.home_dir() == Path.cwd()
+
+
+class TestEnvFile:
+ def test_default_is_dot_env_in_the_home(self, monkeypatch, tmp_path):
+ monkeypatch.delenv(paths.ENV_FILE_ENV, raising=False)
+ monkeypatch.setenv(paths.HOME_ENV, str(tmp_path))
+ assert paths.env_file() == tmp_path.resolve() / ".env"
+
+ def test_the_env_var_wins(self, monkeypatch, tmp_path):
+ monkeypatch.setenv(paths.ENV_FILE_ENV, str(tmp_path / "custom.env"))
+ assert paths.env_file() == tmp_path / "custom.env"
+
+
+class TestPackageDir:
+ def test_holds_the_shipped_data(self):
+ assert paths.package_dir().name == "docsgpt"
+ assert (paths.package_dir() / "alembic.ini").is_file()
+ assert (paths.package_dir() / "prompts" / "chat_reduce_prompt.txt").is_file()
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..00d2c8bc
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,111 @@
+"""The ``docsgpt`` command dispatches to the API server, the worker and the scripts."""
+
+import subprocess
+import sys
+import types
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from docsgpt import cli
+from docsgpt.version import __version__
+
+
+class TestTopLevel:
+ def test_version(self, capsys):
+ with pytest.raises(SystemExit) as exc:
+ cli.main(["--version"])
+ assert exc.value.code == 0
+ assert capsys.readouterr().out.strip() == f"docsgpt {__version__}"
+
+ def test_no_command_prints_help(self, capsys):
+ assert cli.main([]) == 2
+ assert "worker" in capsys.readouterr().out
+
+ def test_importing_the_cli_does_not_boot_the_app(self):
+ """``docsgpt --help`` must not import the Flask app, Celery or settings."""
+ code = (
+ "import sys, docsgpt.cli; "
+ "loaded = {m for m in sys.modules if m in ('docsgpt.app', 'docsgpt.core.settings', 'celery', 'flask')}; "
+ "assert not loaded, loaded"
+ )
+ subprocess.run([sys.executable, "-c", code], cwd=Path(__file__).resolve().parents[1], check=True)
+
+
+class TestApi:
+ def test_gunicorn_argv(self, monkeypatch):
+ run = MagicMock()
+ monkeypatch.setitem(sys.modules, "gunicorn.app.wsgiapp", types.SimpleNamespace(run=run))
+ monkeypatch.setattr(sys, "platform", "linux")
+ monkeypatch.setattr(sys, "argv", ["docsgpt"])
+ assert cli.main(["api", "--port", "8000", "--workers", "2"]) == 0
+ run.assert_called_once_with()
+ argv = sys.argv
+ assert argv[0] == "gunicorn" and argv[-1] == "docsgpt.asgi:asgi_app"
+ assert argv[argv.index("--bind") + 1] == "0.0.0.0:8000"
+ assert argv[argv.index("-w") + 1] == "2"
+ assert argv[argv.index("-k") + 1] == "docsgpt.gunicorn_worker.BoundedDrainUvicornWorker"
+ assert argv[argv.index("--config") + 1] == "python:docsgpt.gunicorn_conf"
+
+ def test_reload_uses_uvicorn(self, monkeypatch):
+ uvicorn = types.SimpleNamespace(run=MagicMock())
+ monkeypatch.setitem(sys.modules, "uvicorn", uvicorn)
+ assert cli.main(["api", "--reload", "--host", "127.0.0.1"]) == 0
+ uvicorn.run.assert_called_once_with("docsgpt.asgi:asgi_app", host="127.0.0.1", port=7091, reload=True)
+
+
+class TestWorker:
+ @staticmethod
+ def _celery(monkeypatch):
+ celery = MagicMock()
+ monkeypatch.setitem(sys.modules, "docsgpt.app", types.SimpleNamespace(celery=celery))
+ return celery
+
+ def test_defaults(self, monkeypatch):
+ celery = self._celery(monkeypatch)
+ monkeypatch.setattr(sys, "platform", "linux")
+ assert cli.main(["worker"]) == 0
+ argv = celery.worker_main.call_args.args[0]
+ assert argv[:3] == ["worker", "-l", "INFO"]
+ assert argv[argv.index("-Q") + 1] == cli.DEFAULT_QUEUES
+ assert "-B" in argv
+ assert "--pool" not in argv
+
+ def test_options_and_the_macos_solo_pool(self, monkeypatch):
+ celery = self._celery(monkeypatch)
+ monkeypatch.setattr(sys, "platform", "darwin")
+ assert cli.main(["worker", "--no-beat", "-Q", "embeddings", "--concurrency", "2"]) == 0
+ argv = celery.worker_main.call_args.args[0]
+ assert argv[argv.index("--pool") + 1] == "solo"
+ assert "-B" not in argv
+ assert argv[argv.index("-Q") + 1] == "embeddings"
+ assert argv[argv.index("--concurrency") + 1] == "2"
+
+
+class TestMigrate:
+ def test_runs_the_bootstrap(self, monkeypatch):
+ ensure = MagicMock()
+ monkeypatch.setattr("docsgpt.storage.db.bootstrap.ensure_database_ready", ensure)
+ monkeypatch.setattr("docsgpt.core.settings.settings.POSTGRES_URI", "postgresql://docsgpt@localhost/docsgpt")
+ assert cli.main(["migrate", "--no-create"]) == 0
+ assert ensure.call_args.args[0] == "postgresql://docsgpt@localhost/docsgpt"
+ assert ensure.call_args.kwargs["create_db"] is False
+ assert ensure.call_args.kwargs["migrate"] is True
+
+ def test_without_a_database_uri(self, monkeypatch, capsys):
+ monkeypatch.setattr("docsgpt.core.settings.settings.POSTGRES_URI", None)
+ assert cli.main(["migrate"]) == 2
+ assert "POSTGRES_URI" in capsys.readouterr().err
+
+
+class TestScripts:
+ def test_arguments_pass_through_untouched(self, monkeypatch):
+ main = MagicMock(return_value=0)
+ monkeypatch.setattr("docsgpt.scripts.prefetch_models.main", main)
+ assert cli.main(["prefetch-models", "--embeddings", "x", "--help"]) == 0
+ main.assert_called_once_with(["--embeddings", "x", "--help"])
+
+ def test_the_script_exit_code_is_returned(self, monkeypatch):
+ monkeypatch.setattr("docsgpt.scripts.verify_offline.main", MagicMock(return_value=3))
+ assert cli.main(["verify-offline"]) == 3
diff --git a/uv.lock b/uv.lock
index f120d37d..fe9b0717 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1385,8 +1385,7 @@ standard = [
[[package]]
name = "docsgpt"
-version = "0.19.0"
-source = { virtual = "." }
+source = { editable = "." }
dependencies = [
{ name = "a2wsgi" },
{ name = "alembic" },
@@ -1498,46 +1497,46 @@ dev = [
[package.metadata]
requires-dist = [
- { name = "a2wsgi", specifier = "==1.10.10" },
+ { name = "a2wsgi", specifier = ">=1.10.10,<2" },
{ name = "alembic", specifier = ">=1.13,<2" },
- { name = "anthropic", specifier = "==0.121.0" },
- { name = "beautifulsoup4", specifier = "==4.15.0" },
- { name = "boto3", specifier = "==1.43.67" },
- { name = "cel-python", specifier = "==0.5.0" },
- { name = "celery", specifier = "==5.6.3" },
- { name = "celery-redbeat", specifier = "==2.4.2" },
- { name = "croniter", specifier = "==6.2.4" },
- { name = "cryptography", specifier = "==50.0.0" },
- { name = "dataclasses-json", specifier = "==0.6.7" },
- { name = "daytona", specifier = "==0.205.1" },
- { name = "ddgs", specifier = ">=8.0.0" },
- { name = "defusedxml", specifier = "==0.7.1" },
- { name = "docling", marker = "extra == 'docling'", specifier = "==2.119.0" },
- { name = "docx2txt", specifier = "==0.9" },
- { name = "elevenlabs", specifier = "==2.62.0" },
- { name = "faiss-cpu", specifier = "==1.15.0" },
- { name = "fast-ebook" },
- { name = "fastembed", specifier = "==0.8.0" },
- { name = "fastmcp", specifier = "==3.4.6" },
- { name = "firecrawl-anydoc", specifier = "==0.2.3" },
- { name = "flask", specifier = "==3.1.3" },
- { name = "flask-restx", specifier = "==1.3.2" },
- { name = "google-api-python-client", specifier = "==2.198.0" },
- { name = "google-auth-oauthlib", specifier = "==1.4.0" },
- { name = "google-genai", specifier = "==2.17.0" },
- { name = "gtts", specifier = "==2.5.4" },
- { name = "gunicorn", specifier = "==26.0.0" },
- { name = "jinja2", specifier = "==3.1.6" },
- { name = "kombu", specifier = "==5.6.2" },
- { name = "markdownify", specifier = "==1.2.3" },
- { name = "milvus-lite", marker = "sys_platform != 'win32' and extra == 'milvus'", specifier = "==3.2.0" },
- { name = "msal", specifier = "==1.37.0" },
- { name = "networkx", specifier = "==3.6.1" },
- { name = "numpy", specifier = "==2.5.1" },
- { name = "onnxruntime", specifier = "==1.28.0" },
- { name = "openai", specifier = "==2.53.0" },
- { name = "openapi3-parser", specifier = "==1.1.22" },
- { name = "openpyxl", specifier = "==3.1.5" },
+ { name = "anthropic", specifier = ">=0.121.0,<0.122" },
+ { name = "beautifulsoup4", specifier = ">=4.15.0,<5" },
+ { name = "boto3", specifier = ">=1.43.67,<2" },
+ { name = "cel-python", specifier = ">=0.5.0,<0.6" },
+ { name = "celery", specifier = ">=5.6.3,<6" },
+ { name = "celery-redbeat", specifier = ">=2.4.2,<3" },
+ { name = "croniter", specifier = ">=6.2.4,<7" },
+ { name = "cryptography", specifier = ">=50.0.0,<51" },
+ { name = "dataclasses-json", specifier = ">=0.6.7,<0.7" },
+ { name = "daytona", specifier = ">=0.205.1,<0.206" },
+ { name = "ddgs", specifier = ">=8.0.0,<10" },
+ { name = "defusedxml", specifier = ">=0.7.1,<0.8" },
+ { name = "docling", marker = "extra == 'docling'", specifier = ">=2.119.0,<3" },
+ { name = "docx2txt", specifier = ">=0.9,<0.10" },
+ { name = "elevenlabs", specifier = ">=2.62.0,<3" },
+ { name = "faiss-cpu", specifier = ">=1.15.0,<2" },
+ { name = "fast-ebook", specifier = ">=0.2.0,<0.3" },
+ { name = "fastembed", specifier = ">=0.8.0,<0.9" },
+ { name = "fastmcp", specifier = ">=3.4.6,<4" },
+ { name = "firecrawl-anydoc", specifier = ">=0.2.3,<0.3" },
+ { name = "flask", specifier = ">=3.1.3,<4" },
+ { name = "flask-restx", specifier = ">=1.3.2,<2" },
+ { name = "google-api-python-client", specifier = ">=2.198.0,<3" },
+ { name = "google-auth-oauthlib", specifier = ">=1.4.0,<2" },
+ { name = "google-genai", specifier = ">=2.17.0,<3" },
+ { name = "gtts", specifier = ">=2.5.4,<3" },
+ { name = "gunicorn", specifier = ">=26.0.0,<27" },
+ { name = "jinja2", specifier = ">=3.1.6,<4" },
+ { name = "kombu", specifier = ">=5.6.2,<6" },
+ { name = "markdownify", specifier = ">=1.2.3,<2" },
+ { name = "milvus-lite", marker = "sys_platform != 'win32' and extra == 'milvus'", specifier = ">=3.2.0,<4" },
+ { name = "msal", specifier = ">=1.37.0,<2" },
+ { name = "networkx", specifier = ">=3.6.1,<4" },
+ { name = "numpy", specifier = ">=2.5.1,<3" },
+ { name = "onnxruntime", specifier = ">=1.28.0,<2" },
+ { name = "openai", specifier = ">=2.53.0,<3" },
+ { name = "openapi3-parser", specifier = ">=1.1.22,<2" },
+ { name = "openpyxl", specifier = ">=3.1.5,<4" },
{ name = "opentelemetry-distro", specifier = ">=0.50b0,<1" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.29.0,<2" },
{ name = "opentelemetry-instrumentation-celery", specifier = ">=0.50b0,<1" },
@@ -1548,42 +1547,42 @@ requires-dist = [
{ name = "opentelemetry-instrumentation-requests", specifier = ">=0.50b0,<1" },
{ name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.50b0,<1" },
{ name = "opentelemetry-instrumentation-starlette", specifier = ">=0.50b0,<1" },
- { name = "pandas", specifier = "==3.0.5" },
- { name = "pdf2image", specifier = ">=1.17.0" },
+ { name = "pandas", specifier = ">=3.0.5,<4" },
+ { name = "pdf2image", specifier = ">=1.17.0,<2" },
{ name = "pgvector", specifier = ">=0.5,<1" },
- { name = "pillow", specifier = "==12.3.0" },
- { name = "praw", specifier = "==8.0.2" },
+ { name = "pillow", specifier = ">=12.3.0,<13" },
+ { name = "praw", specifier = ">=8.0.2,<9" },
{ name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.1,<4" },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pymilvus", marker = "extra == 'milvus'", specifier = "==3.0.1" },
- { name = "pypdf", specifier = "==6.15.0" },
- { name = "pypdfium2", specifier = "==5.12.1" },
- { name = "python-dateutil", specifier = "==2.9.0.post0" },
- { name = "python-dotenv" },
- { name = "python-jose", specifier = "==3.5.0" },
- { name = "python-pptx", specifier = "==1.0.2" },
- { name = "pyyaml" },
- { name = "qdrant-client", specifier = "==1.19.0" },
- { name = "rapidocr", marker = "extra == 'docling'", specifier = "==3.9.2" },
- { name = "redis", specifier = "==7.4.0" },
- { name = "requests", specifier = "==2.34.2" },
- { name = "retry", specifier = "==0.9.2" },
+ { name = "pydantic", specifier = ">=2.13.5,<3" },
+ { name = "pydantic-settings", specifier = ">=2.15.0,<3" },
+ { name = "pymilvus", marker = "extra == 'milvus'", specifier = ">=3.0.1,<4" },
+ { name = "pypdf", specifier = ">=6.15.0,<7" },
+ { name = "pypdfium2", specifier = ">=5.12.1,<6" },
+ { name = "python-dateutil", specifier = ">=2.9.0,<3" },
+ { name = "python-dotenv", specifier = ">=1.2.3,<2" },
+ { name = "python-jose", specifier = ">=3.5.0,<4" },
+ { name = "python-pptx", specifier = ">=1.0.2,<2" },
+ { name = "pyyaml", specifier = ">=6.0.3,<7" },
+ { name = "qdrant-client", specifier = ">=1.19.0,<2" },
+ { name = "rapidocr", marker = "extra == 'docling'", specifier = ">=3.9.2,<4" },
+ { name = "redis", specifier = ">=7.4.0,<8" },
+ { name = "requests", specifier = ">=2.34.2,<3" },
+ { name = "retry", specifier = ">=0.9.2,<0.10" },
{ name = "sqlalchemy", specifier = ">=2.0,<3" },
{ name = "starlette", specifier = ">=1.0,<2" },
- { name = "tiktoken", specifier = "==0.13.0" },
- { name = "tldextract", specifier = "==5.3.2" },
- { name = "tokenizers", specifier = "==0.22.2" },
- { name = "torch", marker = "sys_platform == 'linux' and extra == 'docling'", specifier = "==2.11.0", index = "https://download.pytorch.org/whl/cpu" },
- { name = "torch", marker = "sys_platform != 'linux' and extra == 'docling'", specifier = "==2.11.0" },
- { name = "torchvision", marker = "sys_platform == 'linux' and extra == 'docling'", specifier = "==0.26.0", index = "https://download.pytorch.org/whl/cpu" },
- { name = "torchvision", marker = "sys_platform != 'linux' and extra == 'docling'", specifier = "==0.26.0" },
- { name = "tqdm", specifier = "==4.67.3" },
- { name = "transformers", marker = "extra == 'docling'", specifier = "==5.8.1" },
+ { name = "tiktoken", specifier = ">=0.13.0,<0.14" },
+ { name = "tldextract", specifier = ">=5.3.2,<6" },
+ { name = "tokenizers", specifier = ">=0.22.2,<0.23" },
+ { name = "torch", marker = "sys_platform == 'linux' and extra == 'docling'", specifier = ">=2.11.0,<3", index = "https://download.pytorch.org/whl/cpu" },
+ { name = "torch", marker = "sys_platform != 'linux' and extra == 'docling'", specifier = ">=2.11.0,<3" },
+ { name = "torchvision", marker = "sys_platform == 'linux' and extra == 'docling'", specifier = ">=0.26.0,<0.27", index = "https://download.pytorch.org/whl/cpu" },
+ { name = "torchvision", marker = "sys_platform != 'linux' and extra == 'docling'", specifier = ">=0.26.0,<0.27" },
+ { name = "tqdm", specifier = ">=4.67.3,<5" },
+ { name = "transformers", marker = "extra == 'docling'", specifier = ">=5.8.1,<5.9" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30,<1" },
{ name = "uvicorn-worker", specifier = ">=0.4,<1" },
- { name = "websocket-client", specifier = "==1.9.0" },
- { name = "werkzeug", specifier = ">=3.1.0" },
+ { name = "websocket-client", specifier = ">=1.9.0,<2" },
+ { name = "werkzeug", specifier = ">=3.1.0,<4" },
]
provides-extras = ["docling", "milvus"]