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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -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";
};
};
};
});
}
168 changes: 168 additions & 0 deletions module.nix
Original file line number Diff line number Diff line change
@@ -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
'';
};
};
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,8 @@
"@scarf/scarf",
"better-sqlite3"
]
},
"bin": {
"paperless-ai": "./server.js"
}
}