diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e5bbead --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1757347588, + "narHash": "sha256-tLdkkC6XnsY9EOZW9TlpesTclELy8W7lL2ClL+nma8o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b599843bad24621dcaa5ab60dac98f9b0eb1cabe", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e95a664 --- /dev/null +++ b/flake.nix @@ -0,0 +1,127 @@ +{ + description = "Paperless-AI - AI-powered extension for Paperless-ngx with automatic document classification, smart tagging, and semantic search"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + }: + { + nixosModules.default = import ./module.nix; + nixosModule = self.nixosModules.default; + } + // flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + + # Python environment with all required packages for RAG service + pythonEnv = pkgs.python313.withPackages (ps: + with ps; [ + fastapi # Web framework for RAG API + uvicorn # ASGI server + python-dotenv # Environment file loading + requests # HTTP client + numpy # Numerical computing + pytorch # Machine learning framework + sentence-transformers # Text embeddings + chromadb # Vector database + rank-bm25 # BM25 ranking algorithm + nltk # Natural language processing + tqdm # Progress bars + pydantic # Data validation + ]); + + # Node.js runtime (dependencies managed via npm) + nodejs = pkgs.nodejs; + + # Main Paperless-AI package built with buildNpmPackage for proper npm dependency management + paperless-ai = pkgs.buildNpmPackage { + pname = "paperless-ai"; + version = "1.0.0"; + + src = ./.; + + npmDepsHash = "sha256-nAcI3L0fvVI/CdUxWYg8ZiPRDjF7dW+dcIKC3KlHjNQ="; + + nativeBuildInputs = with pkgs; [ + sqlite + ]; + + buildInputs = with pkgs; [ + sqlite + ]; + + # Don't run the default npm build script + dontNpmBuild = true; + + meta = with pkgs.lib; { + description = "AI-powered extension for Paperless-ngx that brings automatic document classification, smart tagging, and semantic search"; + homepage = "https://github.com/clusterzx/paperless-ai"; + license = licenses.mit; + maintainers = []; + platforms = platforms.linux ++ platforms.darwin; + }; + }; + + paperless-ai-rag = pkgs.writeShellApplication { + name = "paperless-ai-rag"; + runtimeInputs = [pythonEnv]; + text = '' + WORK_DIR="$HOME/.local/share/paperless-ai-rag" + mkdir -p "$WORK_DIR/data" + + if [ ! -f "$WORK_DIR/main.py" ]; then + echo "Setting up Paperless-AI RAG service in $WORK_DIR..." + cp ${./main.py} "$WORK_DIR/main.py" + chmod u+w "$WORK_DIR/main.py" + fi + + cd "$WORK_DIR" + exec python main.py "$@" + ''; + meta = with pkgs.lib; { + description = "RAG service for Paperless-AI - semantic search and document Q&A"; + license = licenses.mit; + platforms = platforms.linux ++ platforms.darwin; + }; + }; + in { + # Package outputs + packages = { + default = paperless-ai; + paperless-ai = paperless-ai; + paperless-ai-rag = paperless-ai-rag; + }; + + # Development environments + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs + nodePackages.npm + pythonEnv + ]; + }; + + apps = { + paperless-ai = { + type = "app"; + program = "${paperless-ai}/bin/paperless-ai"; + meta = { + description = "AI-powered extension for Paperless-ngx with automatic document classification, smart tagging, and semantic search"; + }; + }; + paperless-ai-rag = { + type = "app"; + program = "${paperless-ai-rag}/bin/paperless-ai-rag"; + meta = { + description = "RAG (Retrieval-Augmented Generation) service for Paperless-AI - semantic search and document Q&A"; + }; + }; + }; + }); +} diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..1b54a9e --- /dev/null +++ b/module.nix @@ -0,0 +1,168 @@ +{ + config, + lib, + ... +}: +with lib; let + cfg = config.services.paperless-ai; + stateDir = "/var/lib/paperless-ai"; +in { + options.services.paperless-ai = { + enable = mkEnableOption "Paperless-AI service"; + + package = mkOption { + type = types.package; + }; + + rag-package = mkOption { + type = types.package; + }; + + user = mkOption { + type = types.str; + default = "paperless-ai"; + description = "User account under which Paperless-AI runs."; + }; + + group = mkOption { + type = types.str; + default = "paperless-ai"; + description = "Group account under which Paperless-AI runs."; + }; + + webPort = mkOption { + type = types.port; + default = 3000; + description = "Port for the web service."; + }; + + ragPort = mkOption { + type = types.port; + default = 8000; + description = "Port for the RAG service."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for Paperless-AI ports."; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Environment file containing secrets like API keys."; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra environment variables for Paperless-AI."; + }; + }; + + config = mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = stateDir; + createHome = true; + }; + + users.groups.${cfg.group} = {}; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ + cfg.webPort + cfg.ragPort + ]; + + systemd.services.paperless-ai-web = { + description = "Paperless-AI Web Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${cfg.package}/bin/paperless-ai"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + NODE_ENV = "production"; + PAPERLESS_AI_PORT = toString cfg.webPort; + RAG_SERVICE_URL = "http://localhost:${toString cfg.ragPort}"; + RAG_SERVICE_ENABLED = "true"; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + # serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + + systemd.services.paperless-ai-rag = { + description = "Paperless-AI RAG Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${cfg.rag-package}/bin/paperless-ai-rag"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + PORT = toString cfg.ragPort; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + }; +} diff --git a/package.json b/package.json index c823d6a..f3cf607 100644 --- a/package.json +++ b/package.json @@ -64,5 +64,8 @@ "@scarf/scarf", "better-sqlite3" ] + }, + "bin": { + "paperless-ai": "./server.js" } }