@felipenmoura/laya-node 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ venv
3
+ .env
4
+ .daemon.pid
5
+ .git
6
+ .gitignore
7
+ *.log
package/Dockerfile ADDED
@@ -0,0 +1,40 @@
1
+ # Use a Python base image because AI models require heavy Python dependencies
2
+ FROM python:3.10-slim
3
+
4
+ # Install Node.js (required for the wrapper)
5
+ RUN apt-get update && apt-get install -y curl && \
6
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
7
+ apt-get install -y nodejs && \
8
+ npm install -g pnpm && \
9
+ rm -rf /var/lib/apt/lists/*
10
+
11
+ # Set the working directory
12
+ WORKDIR /app
13
+
14
+ # Copy package configurations
15
+ COPY package.json pnpm-lock.yaml* ./
16
+
17
+ # Install node dependencies
18
+ # (The postinstall script automatically skips the interactive python setup in Docker)
19
+ RUN pnpm install
20
+
21
+ # Copy application files
22
+ COPY . .
23
+
24
+ # Explicitly setup the python virtual environment inside the container
25
+ RUN python -m venv venv && \
26
+ ./venv/bin/pip install --no-cache-dir fastapi uvicorn laya
27
+
28
+ # Pre-download the Laya model weights during the docker build to save time on container startup
29
+ RUN ./venv/bin/python -c "import warnings; warnings.filterwarnings('ignore'); from laya import Router; Router(preload=True)"
30
+
31
+ # Set environment variables
32
+ ENV PORT=4000
33
+ ENV MAX_LEN=8192
34
+ ENV SECRET_API_KEY=""
35
+
36
+ # Expose the API port
37
+ EXPOSE 4000
38
+
39
+ # Start the Node wrapper
40
+ CMD ["pnpm", "start"]
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Laya Node Wrapper
2
2
 
3
- The Node.js wrapper for running the [Laya Text Classification Model](https://huggingface.co/convaiinnovations/laya).
3
+ The Node.js wrapper for running the [Laya Text Classification Model](https://huggingface.co/convaiinnovations/laya), the multilingual, non-autoregressive System 1 decision model for AI (for artificial inteligence).
4
4
 
5
5
  Laya is a non-autoregressive decision model designed for text classification, email triage, and moderation. This package exposes Laya's API through an Express HTTP server, allowing you to easily integrate it into your Node.js applications while matching the exact Python API structure natively.
6
6
 
@@ -66,6 +66,20 @@ pnpm stop
66
66
  npm run stop
67
67
  ```
68
68
 
69
+ ### Running with Docker
70
+
71
+ You can easily containerize this wrapper and run it anywhere using Docker. The included `Dockerfile` will automatically fetch the model weights during the build phase so container startups remain fast.
72
+
73
+ To build the image:
74
+ ```bash
75
+ docker build -t laya-node-wrapper .
76
+ ```
77
+
78
+ To run the container (exposing port 4000):
79
+ ```bash
80
+ docker run -p 4000:4000 -e SECRET_API_KEY="my-secure-key" laya-node-wrapper
81
+ ```
82
+
69
83
  ## Making Predictions
70
84
 
71
85
  Once running, send a `POST` request to `/predict`. Be sure to include your Bearer token if you enabled API Key protection during setup.
package/index.js CHANGED
@@ -81,16 +81,27 @@ const uvicornExec = process.platform === 'win32'
81
81
  : path.join(venvPath, 'bin', 'uvicorn');
82
82
 
83
83
  // Check if venv exists, otherwise fallback to system python
84
- const useVenv = fs.existsSync(uvicornExec);
85
- const command = useVenv ? uvicornExec : 'uvicorn';
84
+ const useVenv = fs.existsSync(pythonExec);
85
+ const command = useVenv ? pythonExec : 'python3';
86
+ const args = ['-m', 'uvicorn', 'server:app', '--port', PYTHON_PORT.toString()];
86
87
 
87
- console.log(`Starting python server via ${command}...`);
88
+ console.log(`Starting python server via ${command} ${args.join(' ')}...`);
88
89
 
89
- const pythonProcess = spawn(command, ['server:app', '--port', PYTHON_PORT.toString()], {
90
+ const pythonProcess = spawn(command, args, {
90
91
  cwd: __dirname,
91
92
  stdio: ['ignore', 'pipe', 'pipe']
92
93
  });
93
94
 
95
+ pythonProcess.on('error', (err) => {
96
+ if (err.code === 'ENOENT') {
97
+ console.error(`\n\x1b[31m✖ Error: Could not find Python executable or uvicorn.\x1b[0m`);
98
+ console.error(`\x1b[33mPlease ensure you have run the setup or installed dependencies in the virtual environment.\x1b[0m\n`);
99
+ } else {
100
+ console.error(`\n\x1b[31m✖ Failed to start Python server:\x1b[0m ${err.message}\n`);
101
+ }
102
+ process.exit(1);
103
+ });
104
+
94
105
  let warmedUp = false;
95
106
  async function warmupModel() {
96
107
  if (warmedUp) return;
@@ -175,7 +186,7 @@ app.use('/', createProxyMiddleware({
175
186
  }
176
187
  }));
177
188
 
178
- app.listen(PORT, () => {
189
+ const server = app.listen(PORT, () => {
179
190
  console.log(`Laya Node wrapper is running on http://localhost:${PORT}`);
180
191
  if (SECRET_API_KEY) {
181
192
  console.log(`API Key protection is ENABLED. Please send "Authorization: Bearer <your-key>"`);
@@ -203,6 +214,16 @@ app.listen(PORT, () => {
203
214
  console.log(`\x1b[1mExample Request:\x1b[0m\n${curlExample}`);
204
215
  });
205
216
 
217
+ server.on('error', (err) => {
218
+ if (err.code === 'EADDRINUSE') {
219
+ console.error(`\n\x1b[31m✖ Error: Port ${PORT} is already in use.\x1b[0m`);
220
+ console.error(`\x1b[33mPlease ensure no other process is using this port, or specify a different PORT environment variable.\x1b[0m\n`);
221
+ process.exit(1);
222
+ } else {
223
+ console.error(`\n\x1b[31m✖ Server error:\x1b[0m`, err.message, '\n');
224
+ }
225
+ });
226
+
206
227
  // Clean up child process on exit
207
228
  process.on('SIGINT', () => {
208
229
  pythonProcess.kill('SIGINT');
package/install.js CHANGED
@@ -117,11 +117,11 @@ async function runSetup() {
117
117
  }
118
118
 
119
119
  spinner.text = 'Installing dependencies (laya, fastapi, uvicorn)...';
120
- const pipCommand = process.platform === 'win32'
121
- ? path.join(venvPath, 'Scripts', 'pip')
122
- : path.join(venvPath, 'bin', 'pip');
120
+ const pythonCommand = process.platform === 'win32'
121
+ ? path.join(venvPath, 'Scripts', 'python')
122
+ : path.join(venvPath, 'bin', 'python');
123
123
 
124
- await execAsync(`${pipCommand} install fastapi uvicorn laya`, { cwd: __dirname });
124
+ await execAsync(`"${pythonCommand}" -m pip install fastapi uvicorn laya`, { cwd: __dirname });
125
125
  spinner.succeed('Setup complete!');
126
126
  } catch (error) {
127
127
  spinner.fail('Setup failed during python environment creation or pip install.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@felipenmoura/laya-node",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Node wrapper for laya text classification",
5
5
  "license": "MIT",
6
6
  "author": "Felipe N. Moura",
package/server.py CHANGED
@@ -24,9 +24,23 @@ def predict(req: PredictRequest):
24
24
  kwargs["model"] = req.model
25
25
 
26
26
  max_len = req.max_len if req.max_len is not None else int(os.getenv("MAX_LEN", 8192))
27
- kwargs["max_len"] = max_len
28
-
29
- result = router.predict(req.state, req.questions, **kwargs)
27
+
28
+ # Manually route to support max_len since Router.predict doesn't accept it
29
+ decision = router.route(req.state, req.questions, **kwargs)
30
+ agent = router.load(decision["model"])
31
+
32
+ original_max_len = agent.cfg.get("max_len")
33
+ agent.cfg["max_len"] = max_len
34
+
35
+ try:
36
+ result = agent.system_one(req.state, req.questions)
37
+ result["routing"] = dict(decision)
38
+ finally:
39
+ if original_max_len is not None:
40
+ agent.cfg["max_len"] = original_max_len
41
+ elif "max_len" in agent.cfg:
42
+ del agent.cfg["max_len"]
43
+
30
44
  return result
31
45
 
32
46
  @app.get("/health")