@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 +7 -0
- package/Dockerfile +40 -0
- package/README.md +15 -1
- package/index.js +26 -5
- package/install.js +4 -4
- package/package.json +1 -1
- package/server.py +17 -3
package/.dockerignore
ADDED
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(
|
|
85
|
-
const command = useVenv ?
|
|
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,
|
|
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
|
|
121
|
-
? path.join(venvPath, 'Scripts', '
|
|
122
|
-
: path.join(venvPath, 'bin', '
|
|
120
|
+
const pythonCommand = process.platform === 'win32'
|
|
121
|
+
? path.join(venvPath, 'Scripts', 'python')
|
|
122
|
+
: path.join(venvPath, 'bin', 'python');
|
|
123
123
|
|
|
124
|
-
await execAsync(
|
|
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
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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")
|