@felipenmoura/laya-node 1.0.0
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/LICENSE +21 -0
- package/README.md +136 -0
- package/index.js +217 -0
- package/install.js +155 -0
- package/package.json +21 -0
- package/server.py +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felipe N. Moura @DEx01
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Laya Node Wrapper
|
|
2
|
+
|
|
3
|
+
The Node.js wrapper for running the [Laya Text Classification Model](https://huggingface.co/convaiinnovations/laya).
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Features
|
|
8
|
+
- **Zero-Config Installation**: Just follow the instructions during the install process.
|
|
9
|
+
- **API Key Protection**: Built-in authorization token support to secure your endpoint.
|
|
10
|
+
- **Daemon Mode**: Easily run the inference server in the background as a daemon.
|
|
11
|
+
- **Automated Warmup**: Pre-loads the model weights upon startup so your first HTTP request is lightning fast.
|
|
12
|
+
- **Clean CLI UX**: Smooth cli experience.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
# or
|
|
19
|
+
pnpm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
During installation, an interactive prompt will ask you:
|
|
23
|
+
1. Which **Port** the Express server should bind to (default: `4000`).
|
|
24
|
+
2. Whether to generate or set a custom **Secret API Key** for route protection.
|
|
25
|
+
3. Whether to automatically set up the Python `venv` environment.
|
|
26
|
+
4. Whether to download the Laya model weights immediately.
|
|
27
|
+
|
|
28
|
+
Your preferences will be saved securely to a `.env` file (which is automatically git-ignored).
|
|
29
|
+
|
|
30
|
+
### Additional Environment Variables
|
|
31
|
+
You can manually edit your `.env` file to configure additional parameters:
|
|
32
|
+
- `MAX_LEN`: Controls the maximum token length passed to the Laya model during predictions. Defaults to `8192` if not provided.
|
|
33
|
+
- `PORT`: The port for the Express server. Defaults to `4000` if not provided.
|
|
34
|
+
- `SECRET_API_KEY`: The SECRET API key required to access the API.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Running the Server
|
|
39
|
+
|
|
40
|
+
To start the server:
|
|
41
|
+
```bash
|
|
42
|
+
npm start
|
|
43
|
+
# or
|
|
44
|
+
pnpm start
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The server will initialize, bind to the configured port, require the correct API Secret for any incomming message, automatically wake up the Laya model in the background (warm up).
|
|
48
|
+
|
|
49
|
+
It will show you a `curl` sample on how to call it, too.
|
|
50
|
+
|
|
51
|
+
### Running in the Background (Daemon Mode)
|
|
52
|
+
|
|
53
|
+
To start the server as a background daemon process:
|
|
54
|
+
```bash
|
|
55
|
+
pnpm start -D
|
|
56
|
+
# or
|
|
57
|
+
npm run start -- -D
|
|
58
|
+
```
|
|
59
|
+
The wrapper will boot up, perform the startup warmup routines, print the success logs to your terminal, and detach.
|
|
60
|
+
|
|
61
|
+
To shut down the background daemon:
|
|
62
|
+
```bash
|
|
63
|
+
pnpm stop
|
|
64
|
+
# or
|
|
65
|
+
npm run stop
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Making Predictions
|
|
69
|
+
|
|
70
|
+
Once running, send a `POST` request to `/predict`. Be sure to include your Bearer token if you enabled API Key protection during setup.
|
|
71
|
+
|
|
72
|
+
### Example Request
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -X POST http://localhost:4000/predict \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
-H "Authorization: Bearer <your-secret-api-key>" \
|
|
78
|
+
-d '{
|
|
79
|
+
"state": "The app crashes on startup",
|
|
80
|
+
"questions": {
|
|
81
|
+
"issue": {
|
|
82
|
+
"type": "choice",
|
|
83
|
+
"instructions": "What is the issue?",
|
|
84
|
+
"criteria": {
|
|
85
|
+
"bug": "crash or error",
|
|
86
|
+
"feature": "new request"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Example Response
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"model": "laya-rl-agent",
|
|
98
|
+
"answers": {
|
|
99
|
+
"issue": {
|
|
100
|
+
"type": "choice",
|
|
101
|
+
"choice": "bug",
|
|
102
|
+
"probabilities": {
|
|
103
|
+
"bug": 0.9764,
|
|
104
|
+
"feature": 0.0236
|
|
105
|
+
},
|
|
106
|
+
"confidence": 0.8388,
|
|
107
|
+
"action": {
|
|
108
|
+
"act_probability": 1.0
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"usage": {
|
|
113
|
+
"input_tokens": 28,
|
|
114
|
+
"output_tokens": 0
|
|
115
|
+
},
|
|
116
|
+
"routing": {
|
|
117
|
+
"model": "english",
|
|
118
|
+
"repo": "convaiinnovations/laya",
|
|
119
|
+
"reason": "English Latin text",
|
|
120
|
+
"detection": {
|
|
121
|
+
"script": "latin",
|
|
122
|
+
"script_profile": {
|
|
123
|
+
"latin": 1.0
|
|
124
|
+
},
|
|
125
|
+
"language": "en",
|
|
126
|
+
"is_english": true,
|
|
127
|
+
"non_latin_fraction": 0.0
|
|
128
|
+
},
|
|
129
|
+
"workflow": null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
This project is licensed under the [MIT License](LICENSE).
|
package/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const pidFile = path.join(__dirname, '.daemon.pid');
|
|
12
|
+
|
|
13
|
+
if (process.argv.includes('--stop')) {
|
|
14
|
+
if (fs.existsSync(pidFile)) {
|
|
15
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 'SIGTERM');
|
|
18
|
+
console.log(`\n\x1b[32m✔ Successfully stopped Laya daemon (PID ${pid}).\x1b[0m\n`);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
if (e.code === 'ESRCH') {
|
|
21
|
+
console.log(`\n\x1b[33m⚠ Daemon process (PID ${pid}) was not running.\x1b[0m\n`);
|
|
22
|
+
} else {
|
|
23
|
+
console.error(`\n\x1b[31m✖ Failed to stop daemon:\x1b[0m`, e.message, '\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
fs.unlinkSync(pidFile);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// it was probably removed by the processs that finished
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
console.log(`\n\x1b[33m⚠ No daemon PID file found. Is the server running in the background?\x1b[0m\n`);
|
|
34
|
+
}
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isDaemonArg = process.argv.includes('-D');
|
|
39
|
+
const isDaemonChild = process.argv.includes('--is-daemon-child');
|
|
40
|
+
|
|
41
|
+
if (isDaemonArg) {
|
|
42
|
+
const childArgs = process.argv.slice(2).filter(a => a !== '-D');
|
|
43
|
+
childArgs.push('--is-daemon-child');
|
|
44
|
+
|
|
45
|
+
const child = spawn(process.execPath, [__filename, ...childArgs], {
|
|
46
|
+
detached: true,
|
|
47
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc']
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
fs.writeFileSync(pidFile, child.pid.toString());
|
|
51
|
+
|
|
52
|
+
child.on('message', (msg) => {
|
|
53
|
+
if (msg === 'WARMUP_COMPLETE') {
|
|
54
|
+
console.log('\n\x1b[35m[Daemon] Disconnecting from terminal. Laya is now running in the background.\x1b[0m\n');
|
|
55
|
+
child.disconnect();
|
|
56
|
+
child.unref();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.on('exit', (code) => {
|
|
62
|
+
process.exit(code || 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Suspend the parent process from running the actual server logic
|
|
66
|
+
await new Promise(() => {});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const PORT = process.env.PORT || 4000;
|
|
70
|
+
const PYTHON_PORT = parseInt(PORT, 10) + 1; // dynamically assign python port based on node port
|
|
71
|
+
const SECRET_API_KEY = process.env.SECRET_API_KEY;
|
|
72
|
+
|
|
73
|
+
// Path to the python executable
|
|
74
|
+
const venvPath = path.join(__dirname, 'venv');
|
|
75
|
+
const pythonExec = process.platform === 'win32'
|
|
76
|
+
? path.join(venvPath, 'Scripts', 'python')
|
|
77
|
+
: path.join(venvPath, 'bin', 'python');
|
|
78
|
+
|
|
79
|
+
const uvicornExec = process.platform === 'win32'
|
|
80
|
+
? path.join(venvPath, 'Scripts', 'uvicorn')
|
|
81
|
+
: path.join(venvPath, 'bin', 'uvicorn');
|
|
82
|
+
|
|
83
|
+
// Check if venv exists, otherwise fallback to system python
|
|
84
|
+
const useVenv = fs.existsSync(uvicornExec);
|
|
85
|
+
const command = useVenv ? uvicornExec : 'uvicorn';
|
|
86
|
+
|
|
87
|
+
console.log(`Starting python server via ${command}...`);
|
|
88
|
+
|
|
89
|
+
const pythonProcess = spawn(command, ['server:app', '--port', PYTHON_PORT.toString()], {
|
|
90
|
+
cwd: __dirname,
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let warmedUp = false;
|
|
95
|
+
async function warmupModel() {
|
|
96
|
+
if (warmedUp) return;
|
|
97
|
+
warmedUp = true;
|
|
98
|
+
console.log('\x1b[36m⏳ Waking up the Laya model. This might take a moment if weights are loading...\x1b[0m');
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/predict`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
...(SECRET_API_KEY ? { 'Authorization': `Bearer ${SECRET_API_KEY}` } : {})
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
state: "Warmup test",
|
|
108
|
+
questions: { "status": { "type": "choice", "instructions": "Is it working?", "criteria": {"yes": "yes", "no": "no"} } }
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (res.ok) {
|
|
113
|
+
console.log('\n\x1b[32m✔ Laya model is warmed up and the HTTP service is ready to receive requests!\x1b[0m\n');
|
|
114
|
+
if (isDaemonChild && process.send) {
|
|
115
|
+
process.send('WARMUP_COMPLETE');
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
console.log('\x1b[33m⚠ Model warmup returned a non-OK status, but the server is running.\x1b[0m\n');
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error('\x1b[31m✖ Failed to warmup the model:\x1b[0m', e.message, '\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pythonProcess.stdout.on('data', (data) => {
|
|
126
|
+
// Hide standard stdout to keep terminal clean
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
pythonProcess.stderr.on('data', (data) => {
|
|
130
|
+
const msg = data.toString().trim();
|
|
131
|
+
|
|
132
|
+
if (msg.includes('Application startup complete.')) {
|
|
133
|
+
warmupModel();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Hide INFO and specific verbose warnings to keep terminal clean
|
|
137
|
+
if (msg.includes('INFO:') || msg.includes('NotOpenSSLWarning') || msg.includes('MPS autocast') || msg.includes('UserWarning:')) {
|
|
138
|
+
return;
|
|
139
|
+
} else if (msg.includes('WARNING:')) {
|
|
140
|
+
console.log(`\x1b[33m[Laya Engine Warning]: ${msg}\x1b[0m`);
|
|
141
|
+
} else if (msg.includes('%|') || msg.includes('it/s')) {
|
|
142
|
+
// Let tqdm progress bars for downloads show through, but formatted
|
|
143
|
+
process.stdout.write(`\r\x1b[36m${msg}\x1b[0m`);
|
|
144
|
+
} else {
|
|
145
|
+
// Only show real errors
|
|
146
|
+
console.error(`\x1b[31m[Laya Engine Error]: ${msg}\x1b[0m`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
pythonProcess.on('close', (code) => {
|
|
151
|
+
console.log(`Python process exited with code ${code}`);
|
|
152
|
+
process.exit(code);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Setup Express to proxy requests
|
|
156
|
+
const app = express();
|
|
157
|
+
|
|
158
|
+
// Authorization middleware
|
|
159
|
+
if (SECRET_API_KEY) {
|
|
160
|
+
app.use((req, res, next) => {
|
|
161
|
+
const authHeader = req.headers.authorization;
|
|
162
|
+
if (!authHeader || authHeader !== `Bearer ${SECRET_API_KEY}`) {
|
|
163
|
+
return res.status(401).json({ error: 'Unauthorized: Invalid or missing API Key' });
|
|
164
|
+
}
|
|
165
|
+
next();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
app.use('/', createProxyMiddleware({
|
|
170
|
+
target: `http://127.0.0.1:${PYTHON_PORT}`,
|
|
171
|
+
changeOrigin: true,
|
|
172
|
+
logLevel: 'silent',
|
|
173
|
+
onError: (err, req, res) => {
|
|
174
|
+
res.status(502).json({ error: 'Laya Engine is not reachable or still starting up.' });
|
|
175
|
+
}
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
app.listen(PORT, () => {
|
|
179
|
+
console.log(`Laya Node wrapper is running on http://localhost:${PORT}`);
|
|
180
|
+
if (SECRET_API_KEY) {
|
|
181
|
+
console.log(`API Key protection is ENABLED. Please send "Authorization: Bearer <your-key>"`);
|
|
182
|
+
}
|
|
183
|
+
console.log(`You can make requests to http://localhost:${PORT}/predict\n`);
|
|
184
|
+
|
|
185
|
+
const authHeader = SECRET_API_KEY ? ` \\\n -H "\x1b[33mAuthorization: Bearer <your-secret-api-key>\x1b[0m"` : '';
|
|
186
|
+
const curlExample =
|
|
187
|
+
`\x1b[36mcurl\x1b[0m -X POST \x1b[32mhttp://localhost:${PORT}/predict\x1b[0m \\\n` +
|
|
188
|
+
` -H \x1b[33m"Content-Type: application/json"\x1b[0m${authHeader} \\\n` +
|
|
189
|
+
` -d \x1b[33m'{\n` +
|
|
190
|
+
` "state": "The app crashes on startup",\n` +
|
|
191
|
+
` "questions": {\n` +
|
|
192
|
+
` "issue": {\n` +
|
|
193
|
+
` "type": "choice",\n` +
|
|
194
|
+
` "instructions": "What is the issue?",\n` +
|
|
195
|
+
` "criteria": {\n` +
|
|
196
|
+
` "bug": "crash or error",\n` +
|
|
197
|
+
` "feature": "new request"\n` +
|
|
198
|
+
` }\n` +
|
|
199
|
+
` }\n` +
|
|
200
|
+
` }\n` +
|
|
201
|
+
` }'\x1b[0m\n`;
|
|
202
|
+
|
|
203
|
+
console.log(`\x1b[1mExample Request:\x1b[0m\n${curlExample}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Clean up child process on exit
|
|
207
|
+
process.on('SIGINT', () => {
|
|
208
|
+
pythonProcess.kill('SIGINT');
|
|
209
|
+
if (isDaemonChild && fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
|
210
|
+
process.exit();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
process.on('SIGTERM', () => {
|
|
214
|
+
pythonProcess.kill('SIGTERM');
|
|
215
|
+
if (isDaemonChild && fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
|
216
|
+
process.exit();
|
|
217
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import util from 'util';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
async function runSetup() {
|
|
17
|
+
console.log('Laya Node wrapper setup');
|
|
18
|
+
|
|
19
|
+
if (process.env.CI || !process.stdout.isTTY) {
|
|
20
|
+
console.log('Non-interactive environment detected. Skipping python setup.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const envPath = path.join(__dirname, '.env');
|
|
25
|
+
const envExists = fs.existsSync(envPath);
|
|
26
|
+
|
|
27
|
+
let currentPort = '4000';
|
|
28
|
+
let currentApiKey = '';
|
|
29
|
+
|
|
30
|
+
if (envExists) {
|
|
31
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
32
|
+
const portMatch = envContent.match(/^PORT=(.*)$/m);
|
|
33
|
+
if (portMatch) currentPort = portMatch[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { port, apiKeyChoice, customApiKey, setupPython, downloadModel } = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'port',
|
|
40
|
+
message: 'The port for the HTTP service',
|
|
41
|
+
default: currentPort
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'select',
|
|
45
|
+
name: 'apiKeyChoice',
|
|
46
|
+
message: 'How do you want to configure the SECRET_API_KEY?',
|
|
47
|
+
choices: [
|
|
48
|
+
{ name: 'Generate a random key', value: 'generate' },
|
|
49
|
+
{ name: 'Enter a custom key', value: 'custom' },
|
|
50
|
+
{ name: 'Skip (no key needed / keep existing)', value: 'skip' }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'customApiKey',
|
|
56
|
+
message: 'Enter the SECRET_API_KEY:',
|
|
57
|
+
when: (answers) => answers.apiKeyChoice === 'custom'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'select',
|
|
61
|
+
name: 'setupPython',
|
|
62
|
+
message: 'Setup all dependencies (python env, laya, etc)?',
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: 'Yes, do your thing', value: true },
|
|
65
|
+
{ name: 'No, I will set them up manually myself', value: false },
|
|
66
|
+
{ name: 'No, I already have everything set up', value: false },
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'select',
|
|
71
|
+
name: 'downloadModel',
|
|
72
|
+
message: 'Download the model immediately or during first use?',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'Yes, download it now', value: true },
|
|
75
|
+
{ name: 'No, download it when the service starts for the first time', value: false },
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
let finalApiKey = '';
|
|
81
|
+
if (apiKeyChoice === 'generate') {
|
|
82
|
+
finalApiKey = crypto.randomBytes(32).toString('hex');
|
|
83
|
+
} else if (apiKeyChoice === 'custom') {
|
|
84
|
+
finalApiKey = customApiKey;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Write or update .env file
|
|
88
|
+
if (!envExists || apiKeyChoice !== 'skip' || port !== currentPort) {
|
|
89
|
+
let envContent = envExists ? fs.readFileSync(envPath, 'utf8') : '';
|
|
90
|
+
|
|
91
|
+
// Update or append PORT
|
|
92
|
+
if (envContent.match(/^PORT=/m)) {
|
|
93
|
+
envContent = envContent.replace(/^PORT=.*$/m, `PORT=${port}`);
|
|
94
|
+
} else {
|
|
95
|
+
envContent += `\nPORT=${port}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update or append SECRET_API_KEY
|
|
99
|
+
if (finalApiKey) {
|
|
100
|
+
if (envContent.match(/^SECRET_API_KEY=/m)) {
|
|
101
|
+
envContent = envContent.replace(/^SECRET_API_KEY=.*$/m, `SECRET_API_KEY=${finalApiKey}`);
|
|
102
|
+
} else {
|
|
103
|
+
envContent += `\nSECRET_API_KEY=${finalApiKey}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(envPath, envContent.trim() + '\n');
|
|
108
|
+
console.log(`Updated .env file with Port: ${port}${finalApiKey ? ' and Secret API Key' : ''}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (setupPython) {
|
|
112
|
+
const spinner = ora('Setting up Python virtual environment...').start();
|
|
113
|
+
try {
|
|
114
|
+
const venvPath = path.join(__dirname, 'venv');
|
|
115
|
+
if (!fs.existsSync(venvPath)) {
|
|
116
|
+
await execAsync('python3 -m venv venv', { cwd: __dirname });
|
|
117
|
+
}
|
|
118
|
+
|
|
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');
|
|
123
|
+
|
|
124
|
+
await execAsync(`${pipCommand} install fastapi uvicorn laya`, { cwd: __dirname });
|
|
125
|
+
spinner.succeed('Setup complete!');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
spinner.fail('Setup failed during python environment creation or pip install.');
|
|
128
|
+
console.error(error.stdout || error.message);
|
|
129
|
+
if (error.stderr) console.error(error.stderr);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.log('Skipping python setup. Please ensure laya is available in your python environment.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (downloadModel) {
|
|
136
|
+
const spinner = ora('Downloading Laya model checkpoints...').start();
|
|
137
|
+
try {
|
|
138
|
+
const venvPath = path.join(__dirname, 'venv');
|
|
139
|
+
const pythonCommand = fs.existsSync(venvPath)
|
|
140
|
+
? (process.platform === 'win32' ? path.join(venvPath, 'Scripts', 'python') : path.join(venvPath, 'bin', 'python'))
|
|
141
|
+
: 'python3';
|
|
142
|
+
|
|
143
|
+
await execAsync(`${pythonCommand} -c "import warnings; warnings.filterwarnings('ignore'); from laya import Router; Router(preload=True)"`, { cwd: __dirname });
|
|
144
|
+
spinner.succeed('Model downloaded successfully!');
|
|
145
|
+
} catch (error) {
|
|
146
|
+
spinner.fail('Failed to download the model.');
|
|
147
|
+
console.error(error.stdout || error.message);
|
|
148
|
+
if (error.stderr) console.error(error.stderr);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
runSetup().catch(err => {
|
|
154
|
+
console.error('Setup failed:', err);
|
|
155
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@felipenmoura/laya-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node wrapper for laya text classification",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Felipe N. Moura",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js",
|
|
11
|
+
"stop": "node index.js --stop",
|
|
12
|
+
"postinstall": "node install.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"dotenv": "^18.0.4",
|
|
16
|
+
"express": "^4.21.0",
|
|
17
|
+
"http-proxy-middleware": "^3.0.0",
|
|
18
|
+
"inquirer": "^14.2.2",
|
|
19
|
+
"ora": "^9.4.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/server.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from laya import Router
|
|
7
|
+
|
|
8
|
+
app = FastAPI()
|
|
9
|
+
|
|
10
|
+
# Optionally preload models based on environment variables or just default Router
|
|
11
|
+
preload = os.environ.get("LAYA_PRELOAD", "false").lower() == "true"
|
|
12
|
+
router = Router(preload=preload)
|
|
13
|
+
|
|
14
|
+
class PredictRequest(BaseModel):
|
|
15
|
+
state: str
|
|
16
|
+
questions: Dict[str, Any]
|
|
17
|
+
model: Optional[str] = None
|
|
18
|
+
max_len: Optional[int] = None
|
|
19
|
+
|
|
20
|
+
@app.post("/predict")
|
|
21
|
+
def predict(req: PredictRequest):
|
|
22
|
+
kwargs = {}
|
|
23
|
+
if req.model is not None:
|
|
24
|
+
kwargs["model"] = req.model
|
|
25
|
+
|
|
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)
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
@app.get("/health")
|
|
33
|
+
def health():
|
|
34
|
+
return {"status": "ok"}
|