First commit.

This commit is contained in:
2025-11-18 11:02:39 +01:00
parent e57266f93d
commit d4c4ccfc18
16 changed files with 874 additions and 1 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
python/__pycache__/
models/
*.pyc
package-lock.json

191
INNER_WORKING.md Normal file
View File

@@ -0,0 +1,191 @@
# Node.js Python Binding Class Documentation
## Overview
This project provides a Node.js interface (`python_bindings.js`) to interact with a Python backend. The Python backend exposes an API via a Controller class (`controller.py`) and communicates with Node.js via JSON messages over stdin/stdout using `python-shell` in Node.js.
The Node.js class `Bindings` wraps the interaction with the Python process and exposes asynchronous methods mapped dynamically to Python Controller methods. It supports property setting/getting, method calling with parameters, and streaming partial results via callbacks.
---
## Files and Components
### 1. `index.js`
- Entry point example illustrating usage of the `ChatModel` (the default export from `python_bindings.js`).
- Shows setting properties, calling methods, receiving streamed messages, and proper shutdown.
- This file is intended for customization by users for their use case.
Example usage:
```js
import ChatModel from "./python_bindings.js";
const chat = new ChatModel({ "model": "something" });
await chat.setProperty("tokenizer", 123);
const modelResponse = await chat.getModelPath();
console.log(modelResponse); // { model: "something" }
const tokenizerResponse = await chat.getTokenizer();
console.log(tokenizerResponse); // { tokenizer: 123 }
let response = await chat.increment({ by: 5 });
console.log("Incremented counter:", response.counter);
chat.onMessage(function(data) {
console.log(data); // receives streamed partial results
});
chat.testStream();
chat.end();
```
---
### 2. `python_bindings.js`
- Implements `Bindings` class which manages the Python subprocess using `python-shell`.
- Sends JSON-formatted method calls with parameters to Python.
- Receives JSON responses asynchronously.
- Supports streaming JSON messages prefixed with `__STREAM__`.
- Uses Proxy to dynamically route method calls to Python backend methods.
- Provides:
- `.setProperty(name, value)` to set Python controller properties.
- `.getProperty(name)` to get Python controller properties.
- `.onMessage(callback)` to register streaming data callback.
- `.end()` to gracefully end the Python subprocess.
Key methods:
- `callMethod(method, ...args)`: sends a method call to Python and returns a promise resolving to the response.
- `processSendQueue()`: ensures requests are sent sequentially.
- `onMessage(callback)`: sets a callback for streaming partial data.
---
### 3. `python/controller.py`
- Defines `Controller` class that inherits from `BaseController`.
- Implements backend logic called by Node.js methods.
- Example methods:
- `getModelPath(params)`
- `getTokenizer(params)`
- `increment(params)`
- `reset(params)`
- `testStream(params)` — emits streaming partial data via `self.send()`.
---
### 4. `python/basecontroller.py`
- Provides base class with common controller features:
- `setProperty(params)`: sets attributes dynamically on controller.
- `getProperty(params)`: gets attributes dynamically.
- `send(data)`: sends partial streaming data through injected stream function.
- `set_stream_func(stream_func)`: used to inject the stream callback for `send()`.
---
### 5. `python/router.py`
- Acts as the Python message router.
- Reads JSON messages from `stdin`, calls the appropriate `Controller` method, and writes JSON responses to `stdout`.
- Supports streaming partial responses with the `__STREAM__` prefix.
- Handles unknown methods gracefully by returning error JSON.
---
### 6. `python/index.py`
- Runs the `Router` instance when executed as a script.
- This is the Python entry point for the subprocess.
---
### 7. `router.js`
- Alternative Node.js approach (not used directly by `Bindings`).
- Spawns a Python process per method call.
- Supports streaming messages.
- More suited for stateless or separate process calls.
---
## How It Works (Workflow)
1. **Initialization**
- `Bindings` starts a persistent Python subprocess running `python/index.py` via `python-shell`.
- The Python process reads JSON messages from stdin and sends JSON responses on stdout.
2. **Method Calls**
- Node.js sends JSON messages with `{ method: "methodName", params: { ... } }`.
- Python calls the matching method in `Controller` class with params.
- Python sends back `{ result: ... }` or `{ error: ... }`.
3. **Streaming**
- Python can send partial results during long-running methods using the injected `send()` function.
- These partial messages are prefixed with `__STREAM__` so Node.js can route them to the streaming callback.
4. **Dynamic Proxy**
- The `Bindings` class uses a JavaScript Proxy to make any method call on the object automatically send the request to Python and return a Promise with the result.
5. **Property Setting/Getting**
- Properties like `model` and `tokenizer` can be set or fetched through dedicated calls or via `.setProperty()`.
6. **Shutdown**
- Calling `.end()` ends the Python process cleanly.
---
## Usage Summary
- Import `Bindings` or `ChatModel`.
- Instantiate with initial properties if desired.
- Call any controller method asynchronously on the instance.
- Use `.onMessage()` to handle streamed data.
- Call `.end()` to terminate the backend.
---
## Notes for Customization
- Edit `controller.py` to add your own Python logic and methods.
- Update `index.js` for your application-specific flow.
- Ensure Python methods return JSON serializable objects.
- Implement streaming with `send()` and handle it in Node.js via `.onMessage()`.
---
## Example
```js
import ChatModel from "./python_bindings.js";
const chat = new ChatModel({ model: "gpt" });
await chat.setProperty("tokenizer", 42);
const info = await chat.getModelPath();
console.log(info); // { model: "gpt" }
chat.onMessage(data => {
console.log("Streamed partial data:", data);
});
await chat.testStream();
await chat.end();
```
---
This documentation provides a comprehensive understanding of the binding class design and usage to integrate Python backend logic into a Node.js environment efficiently.

110
README.md
View File

@@ -1,2 +1,110 @@
# Python-bindings-for-nodejs
# Python Binding for Node.js
## Description
This project enables seamless integration of a Python backend with a Node.js application by maintaining a persistent Python subprocess. It allows calling Python methods asynchronously from Node.js using a dynamic JavaScript class interface, supports setting and getting Python-side properties, and enables receiving streamed partial results from long-running Python operations.
## How to Use
### JavaScript Usage Example
```js
import ChatModel from "./python_bindings.js";
async function runExample() {
const chat = new ChatModel({ model: "something" });
// Set properties dynamically
await chat.setProperty("model", "something");
await chat.setProperty("tokenizer", 123);
// Get properties from Python backend
const modelResponse = await chat.getModelPath();
console.log(modelResponse); // { Model: "something" }
const tokenizerResponse = await chat.getTokenizer();
console.log(tokenizerResponse); // { Tokenizer: 123 }
// Call Python methods asynchronously
let response = await chat.increment({ by: 5 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
// Listen for streamed partial results
chat.onMessage(function(data) {
console.log("Streamed data:", data);
});
// Call method that streams partial results
await chat.testStream();
// Cleanly terminate Python subprocess
chat.end();
}
runExample();
```
## How to Write the Python Controller
The Python controller defines the backend logic and exposes methods callable from Node.js. It should extend the provided `BaseController` class and implement any methods you want to call from Node.js.
### Controller Structure Example
```python
# python/controller.py
import time
from baseController import BaseController
class Controller(BaseController):
model = None
tokenizer = None
counter = 0
def getModelPath(self, params):
return {"Model": self.model}
def getTokenizer(self, params):
return {"Tokenizer": self.tokenizer}
def increment(self, params):
self.counter += params.get("by", 1)
return {"counter": self.counter}
def reset(self, params):
self.counter = 0
return {"counter": self.counter}
def testStream(self, params):
for i in range(5):
time.sleep(0.5)
self.send({"partial": f"step {i+1} complete"})
return {"counter": self.counter}
```
### Important Details
- **Inheritance:** Your controller must inherit from `BaseController`.
- **Methods:** Each method takes a single `params` dictionary argument containing parameters passed from Node.js.
- **Return Value:** Methods return a JSON-serializable dictionary as a response.
- **Streaming:** Use `self.send(data)` within methods to send partial streaming data back to Node.js. The JavaScript side will receive these via the registered stream callback.
- **Properties:** Define class properties to maintain state accessible from both Python and Node.js via dynamic `setProperty` and `getProperty` calls.
## Summary
- Extend the Python `Controller` class to implement your backend logic.
- Methods receive parameters and return JSON-serializable results.
- Use `self.send()` to stream intermediate results when needed.
- From Node.js, call these methods via the binding class, passing parameters as objects and receiving results asynchronously.
- The binding handles JSON serialization, communication, and a persistent Python process lifecycle.
This design allows flexible and efficient integration between Node.js and Python for complex applications.

116
create.js Normal file
View File

@@ -0,0 +1,116 @@
const readme = `# Python Binding for Node.js
## Description
This project enables seamless integration of a Python backend with a Node.js application by maintaining a persistent Python subprocess. It allows calling Python methods asynchronously from Node.js using a dynamic JavaScript class interface, supports setting and getting Python-side properties, and enables receiving streamed partial results from long-running Python operations.
## How to Use
### JavaScript Usage Example
\`\`\`js
import ChatModel from "./python_bindings.js";
async function runExample() {
const chat = new ChatModel({ model: "something" });
// Set properties dynamically
await chat.setProperty("model", "something");
await chat.setProperty("tokenizer", 123);
// Get properties from Python backend
const modelResponse = await chat.getModelPath();
console.log(modelResponse); // { Model: "something" }
const tokenizerResponse = await chat.getTokenizer();
console.log(tokenizerResponse); // { Tokenizer: 123 }
// Call Python methods asynchronously
let response = await chat.increment({ by: 5 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
// Listen for streamed partial results
chat.onMessage(function(data) {
console.log("Streamed data:", data);
});
// Call method that streams partial results
await chat.testStream();
// Cleanly terminate Python subprocess
chat.end();
}
runExample();
\`\`\`
## How to Write the Python Controller
The Python controller defines the backend logic and exposes methods callable from Node.js. It should extend the provided \`BaseController\` class and implement any methods you want to call from Node.js.
### Controller Structure Example
\`\`\`python
# python/controller.py
import time
from baseController import BaseController
class Controller(BaseController):
model = None
tokenizer = None
counter = 0
def getModelPath(self, params):
return {"Model": self.model}
def getTokenizer(self, params):
return {"Tokenizer": self.tokenizer}
def increment(self, params):
self.counter += params.get("by", 1)
return {"counter": self.counter}
def reset(self, params):
self.counter = 0
return {"counter": self.counter}
def testStream(self, params):
for i in range(5):
time.sleep(0.5)
self.send({"partial": f"step {i+1} complete"})
return {"counter": self.counter}
\`\`\`
### Important Details
- **Inheritance:** Your controller must inherit from \`BaseController\`.
- **Methods:** Each method takes a single \`params\` dictionary argument containing parameters passed from Node.js.
- **Return Value:** Methods return a JSON-serializable dictionary as a response.
- **Streaming:** Use \`self.send(data)\` within methods to send partial streaming data back to Node.js. The JavaScript side will receive these via the registered stream callback.
- **Properties:** Define class properties to maintain state accessible from both Python and Node.js via dynamic \`setProperty\` and \`getProperty\` calls.
## Summary
- Extend the Python \`Controller\` class to implement your backend logic.
- Methods receive parameters and return JSON-serializable results.
- Use \`self.send()\` to stream intermediate results when needed.
- From Node.js, call these methods via the binding class, passing parameters as objects and receiving results asynchronously.
- The binding handles JSON serialization, communication, and a persistent Python process lifecycle.
This design allows flexible and efficient integration between Node.js and Python for complex applications.
`;
console.log(readme);
import { writeFileSync } from "fs";
writeFileSync("README.md", readme);

33
index.js Normal file
View File

@@ -0,0 +1,33 @@
import ChatModel from "./python_bindings.js";
const chat = new ChatModel({ "model":"something" });
//await chat.setProperty( "model", "something");
await chat.setProperty( "tokenizer", 123 );
const modelResponse = await chat.getModelPath();
console.log(modelResponse); // { model: "something" }
const tokenizerResponse = await chat.getTokenizer();
console.log(tokenizerResponse); // { model: 123 }
let response = await chat.increment({ by: 5 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
response = await chat.increment({ by: 2 });
console.log("Incremented counter:", response.counter);
chat.onMessage( function( data ) {
console.log( data );
} );
chat.testStream();
chat.end();

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"type": "module",
"scripts": {
"postinstall": "node scripts/install-python-deps.js"
},
"dependencies": {
"python-shell": "^5.0.0"
}
}

1
python-path.txt Normal file
View File

@@ -0,0 +1 @@
/usr/bin/python3.9

35
python/baseController.py Normal file
View File

@@ -0,0 +1,35 @@
class BaseController:
def set_stream_func(self, stream_func):
self._stream_func = stream_func
def send(self, data):
if getattr(self, "_stream_func", None) is not None:
self._stream_func(data)
def setProperty(self, params):
name = params.get("name")
value = params.get("value")
if not isinstance(name, str):
return {"error": "Property name must be a string"}
if not hasattr(self, name):
return {"error": f"Property '{name}' does not exist"}
setattr(self, name, value)
return {"success": True}
def getProperty(self, params):
name = params.get("name")
if not hasattr(self, name):
return {"error": f"Property '{name}' does not exist"}
value = getattr(self, name)
return {"value": value}

41
python/controller.py Normal file
View File

@@ -0,0 +1,41 @@
import time
from baseController import BaseController
class Controller(BaseController):
model = None
tokenizer = None
counter = 0
def getModelPath(self, params):
return {"Model": self.model}
def getTokenizer(self, params):
return {"Tokenizer": self.tokenizer}
def increment(self, params):
self.counter += params.get("by", 1)
return {"counter": self.counter}
def reset(self, params):
self.counter = 0
return {"counter": self.counter}
def testStream(self, params):
for i in range(5):
time.sleep(0.5)
self.send({"partial": f"step {i+1} complete"})
return {"counter": self.counter}

5
python/index.py Normal file
View File

@@ -0,0 +1,5 @@
from router import Router
if __name__ == "__main__":
router = Router()
router.run()

42
python/router.py Normal file
View File

@@ -0,0 +1,42 @@
import time
from controller import Controller
import sys
import json
class Router:
def __init__(self):
self.controller = Controller()
self.controller.send = self.send_stream # inject send method here
def send_response(self, response):
print(json.dumps(response))
sys.stdout.flush()
def send_stream(self, stream_data):
print("__STREAM__" + json.dumps(stream_data))
sys.stdout.flush()
def run(self):
for line in sys.stdin:
if not line.strip():
continue
try:
message = json.loads(line)
method = message.get("method")
params = message.get("params", {})
if not hasattr(self.controller, method):
self.send_response({"error": f"Method '{method}' not found in ./python/controller.py"})
continue
method_to_call = getattr(self.controller, method)
# Call method WITHOUT stream_func argument,
# controller methods use self.send() internally
result = method_to_call(params)
self.send_response({"result": result})
except Exception as e:
self.send_response({"error": str(e)})

4
python/test_import.py Normal file
View File

@@ -0,0 +1,4 @@
from controller import Controller
c = Controller()
print(c.increment({"by": 3}))

184
python_bindings.js Normal file
View File

@@ -0,0 +1,184 @@
import { PythonShell } from "python-shell";
export default class Bindings {
constructor(initialProps) {
this.pythonShell = new PythonShell("./python/index.py", { mode: "text" });
this.responseQueue = new Array();
this.sendQueue = new Array();
this.isSending = false;
this.streamCallback = null;
this.pythonShell.on("message", function(message) {
if (typeof message === "string" && message.startsWith("__STREAM__")) {
var dataStr = message.substring(10);
var data = null;
try {
data = JSON.parse(dataStr);
} catch (err) {
console.error("Invalid streamed JSON:", dataStr);
return;
}
if (this.streamCallback) {
this.streamCallback(data);
}
return;
}
var response = null;
try {
response = JSON.parse(message);
} catch (err) {
console.error("Invalid JSON response:", message);
var resolve = this.responseQueue.shift();
if (resolve) {
resolve({ error: "Invalid JSON from Python" });
}
return;
}
var resolve = this.responseQueue.shift();
if (resolve) {
resolve(response);
}
this.processSendQueue();
}.bind(this));
this.pythonShell.on("error", function(err) {
console.error("PythonShell error:", err);
});
this.pythonShell.on("stderr", function(stderr) {
console.error("PythonShell stderr:", stderr);
});
// Set initial properties if any
if (initialProps && typeof initialProps === "object") {
var keys = Object.keys(initialProps);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = initialProps[key];
this.callMethod("setProperty", key, value);
}
}
return new Proxy(this, {
get: function(target, prop) {
if (typeof prop === "string") {
if (prop === "end") {
return target.end.bind(target);
}
if (prop === "onMessage") {
return target.onMessage.bind(target);
}
return function() {
var args = new Array(arguments.length);
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
return target.callMethod(prop, ...args);
};
}
return undefined;
},
set: function(target, prop, value) {
return target.callMethod("setProperty", prop, value)
.then(function() {
return true;
})
.catch(function(err) {
console.error(err);
return false;
});
}
});
}
onMessage(callback) {
this.streamCallback = callback;
}
callMethod(method) {
var args = new Array(arguments.length - 1);
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
return new Promise(function(resolve, reject) {
this.responseQueue.push(function(response) {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response.result !== undefined ? response.result : response);
}
});
var params = {};
if (method === "setProperty" && args.length === 2) {
params = { name: args[0], value: args[1] };
} else if (args.length === 1 && typeof args[0] === "object") {
params = args[0];
} else if (args.length > 0) {
params = { args: args };
}
this.sendQueue.push({ method: method, params: params });
this.processSendQueue();
}.bind(this));
}
processSendQueue() {
if (this.isSending === true) {
return;
}
if (this.sendQueue.length === 0) {
return;
}
var message = this.sendQueue.shift();
this.isSending = true;
try {
this.pythonShell.send(JSON.stringify(message));
} catch (err) {
var resolve = this.responseQueue.shift();
if (resolve) {
resolve({ error: err.message });
}
}
this.isSending = false;
}
end() {
this.pythonShell.end(function(err) {
if (err !== undefined && err !== null) {
console.error("Error ending PythonShell:", err);
}
});
}
}

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
torch
transformers
peft

46
router.js Normal file
View File

@@ -0,0 +1,46 @@
import { spawn } from "child_process";
export class Router {
constructor () {
this.callbacks = { };
this.stdoutBuffer = "";
this.onmessage = null;
}
async callMethod ( method, params = { } ) {
const child = spawn( "python3", [ "index.py", method, JSON.stringify( params ) ] );
return await new Promise( ( resolve, reject ) => {
let result = "";
child.stdout.on( "data", ( data ) => {
const lines = data.toString().split( "\n" );
for ( const line of lines ) {
if ( line.startsWith( "__STREAM__" ) ) {
const payload = line.substring( 10 ).trim();
if ( this.onmessage ) {
this.onmessage( JSON.parse( payload ) );
}
} else if ( line.trim() ) {
result += line.trim();
}
}
} );
child.stderr.on( "data", ( err ) => {
reject( new Error( err.toString() ) );
} );
child.on( "close", () => {
try {
resolve( JSON.parse( result ) );
} catch ( err ) {
reject( new Error( "Failed to parse response: " + result ) );
}
} );
} );
}
}

View File

@@ -0,0 +1,50 @@
import { execSync } from "child_process";
import { writeFileSync } from "fs";
import path from "path";
const candidates = [
"/usr/bin/python3.9",
"python3",
"python"
];
const pythonPathFile = path.resolve(process.cwd(), "python-path.txt");
function checkPython(pythonCmd) {
try {
execSync(`${pythonCmd} --version`, { stdio: "ignore" });
execSync(`${pythonCmd} -m pip --version`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
function installDeps(pythonCmd) {
try {
console.log(`Using Python command: ${pythonCmd}`);
execSync(`${pythonCmd} -m pip install -r requirements.txt`, { stdio: "inherit" });
writeFileSync(pythonPathFile, pythonCmd, "utf-8");
console.log(`✅ Python packages installed successfully. Saved Python path to ${pythonPathFile}`);
process.exit(0);
} catch (err) {
console.error("Failed to install Python packages with", pythonCmd);
}
}
function main() {
for (const cmd of candidates) {
if (checkPython(cmd)) {
installDeps(cmd);
return;
}
}
console.error(
"\n⚠ Python or pip not found or pip is broken.\n" +
"Please install Python 3 and pip manually, then run:\n\n" +
" pip install -r requirements.txt\n"
);
process.exit(1);
}
main();