diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e52f48e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +python/__pycache__/ +models/ +*.pyc +package-lock.json \ No newline at end of file diff --git a/INNER_WORKING.md b/INNER_WORKING.md new file mode 100644 index 0000000..ab13b16 --- /dev/null +++ b/INNER_WORKING.md @@ -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. diff --git a/README.md b/README.md index c2b25d7..f762c9f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/create.js b/create.js new file mode 100644 index 0000000..9578675 --- /dev/null +++ b/create.js @@ -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); \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..d1d6a95 --- /dev/null +++ b/index.js @@ -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(); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..6772abd --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "scripts": { + "postinstall": "node scripts/install-python-deps.js" + }, + "dependencies": { + "python-shell": "^5.0.0" + } +} diff --git a/python-path.txt b/python-path.txt new file mode 100644 index 0000000..b2f4ecf --- /dev/null +++ b/python-path.txt @@ -0,0 +1 @@ +/usr/bin/python3.9 \ No newline at end of file diff --git a/python/baseController.py b/python/baseController.py new file mode 100644 index 0000000..b12434e --- /dev/null +++ b/python/baseController.py @@ -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} diff --git a/python/controller.py b/python/controller.py new file mode 100644 index 0000000..a1ce345 --- /dev/null +++ b/python/controller.py @@ -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} diff --git a/python/index.py b/python/index.py new file mode 100644 index 0000000..b8bf11d --- /dev/null +++ b/python/index.py @@ -0,0 +1,5 @@ +from router import Router + +if __name__ == "__main__": + router = Router() + router.run() \ No newline at end of file diff --git a/python/router.py b/python/router.py new file mode 100644 index 0000000..9d00552 --- /dev/null +++ b/python/router.py @@ -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)}) diff --git a/python/test_import.py b/python/test_import.py new file mode 100644 index 0000000..3e483dd --- /dev/null +++ b/python/test_import.py @@ -0,0 +1,4 @@ +from controller import Controller + +c = Controller() +print(c.increment({"by": 3})) diff --git a/python_bindings.js b/python_bindings.js new file mode 100644 index 0000000..255e0fa --- /dev/null +++ b/python_bindings.js @@ -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); + } + }); + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f146970 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +torch +transformers +peft \ No newline at end of file diff --git a/router.js b/router.js new file mode 100644 index 0000000..84f65b3 --- /dev/null +++ b/router.js @@ -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 ) ); + } + } ); + } ); + } +} diff --git a/scripts/install-python-deps.js b/scripts/install-python-deps.js new file mode 100644 index 0000000..fb88ea8 --- /dev/null +++ b/scripts/install-python-deps.js @@ -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();