From f1371e7389b7227655a4221ee092006719656bda Mon Sep 17 00:00:00 2001 From: mbruzon Date: Fri, 5 Jun 2026 15:00:29 +0200 Subject: [PATCH] #wip: Web Service communication. --- JSON/AnP.settings.json | 2 +- Public/ecma/Application/AnP.ecma.js | 3 + .../ecma/Components/AIChatComponent.ecma.js | 30 +++++- .../Managers/WebSocketsClientsManager.ecma.js | 65 +++++++++++++ .../ecma/Models/WebSocketClientModel.ecma.js | 92 +++++++++++++++++++ Python/Abstracts/AIInterpretersAbstract.py | 19 +++- Python/Abstracts/WebSocketServersAbstract.py | 12 ++- Python/Drivers/OllamaDriver.py | 8 +- Python/Drivers/WebSocketServerDriver.py | 19 ++-- 9 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 Public/ecma/Managers/WebSocketsClientsManager.ecma.js create mode 100644 Public/ecma/Models/WebSocketClientModel.ecma.js diff --git a/JSON/AnP.settings.json b/JSON/AnP.settings.json index dd59637..2fb9c48 100644 --- a/JSON/AnP.settings.json +++ b/JSON/AnP.settings.json @@ -71,7 +71,7 @@ "default_web_sockets_servers2" : { "anp" : { "type" : "WebSocketServerDriver", - "host" : "localhost", + "host" : "", "port" : 18765 } }, diff --git a/Public/ecma/Application/AnP.ecma.js b/Public/ecma/Application/AnP.ecma.js index e44c964..9f59293 100644 --- a/Public/ecma/Application/AnP.ecma.js +++ b/Public/ecma/Application/AnP.ecma.js @@ -10,6 +10,7 @@ import {SessionsManager} from "../Managers/SessionsManager.ecma.js"; import {ModelsManager} from "../Managers/ModelsManager.ecma.js"; import {ViewsManager} from "../Managers/ViewsManager.ecma.js"; import {RoutesManager} from "../Managers/RoutesManager.ecma.js"; +import {WebSocketsClientsManager} from "../Managers/WebSocketsClientsManager.ecma.js"; import {Components} from "./Components.ecma.js"; import {Common} from "../Utils/Common.ecma.js"; import {Check} from "../Utils/Checks.ecma.js"; @@ -73,6 +74,8 @@ export const AnP = (function(){ this.views = new ViewsManager(self); /** @type {RoutesManager} */ this.routes = new RoutesManager(self); + /** @type {WebSocketsClientsManager} */ + this.web_sockets_clients = new WebSocketsClientsManager(self); /** * @returns {void} diff --git a/Public/ecma/Components/AIChatComponent.ecma.js b/Public/ecma/Components/AIChatComponent.ecma.js index c4cb7b0..f7114f6 100644 --- a/Public/ecma/Components/AIChatComponent.ecma.js +++ b/Public/ecma/Components/AIChatComponent.ecma.js @@ -28,7 +28,9 @@ export const AIChatComponent = (function(){ /** @type {AIChatComponent} */ const self = this; - + /** @type {string|null} */ + let web_socket_id = null; + /** * @returns {void} * @access private @@ -39,6 +41,8 @@ export const AIChatComponent = (function(){ const name = Common.get_value("name", inputs, "aichat"); + web_socket_id = anp.web_sockets_clients.add("AAA", "https://localhost:18000/"); + return Fieldset({class : "aichat"}, [ anp.components.i18n(name, "legend"), Section({class : "messages"}), @@ -66,7 +70,8 @@ export const AIChatComponent = (function(){ if(text){ Common.HTML(".aichat .messages", Fieldset({ class : "message", - data_type : "user" + data_type : "user", + data_id : anp.unique_keys.create() }, [ anp.components.i18n("user", "legend"), Div({class : "content"}, text) @@ -81,6 +86,27 @@ export const AIChatComponent = (function(){ event.key == "Enter" && !event.shiftKey && send(item, event); }; + this.write_response = (id, fragment) => { + + let box = document.querySelector(".aichat .messages>[data-type=bot][data-id='" + id + "']"); + + if(!box){ + if(!document.querySelector(".aichat .messages>[data-type=user][data-id='" + id + "']")) + return; + box = Common.HTML(".aichat .messages", Fieldset({ + class : "message", + data_type : "box", + data_id : id + }, [ + anp.components.i18n("assistant", "legend"), + Div({class : "content"}, "") + ]), true)[0]; + }; + + box.querySelector(".content").innerHTML += fragment; + + }; + constructor(); }; diff --git a/Public/ecma/Managers/WebSocketsClientsManager.ecma.js b/Public/ecma/Managers/WebSocketsClientsManager.ecma.js new file mode 100644 index 0000000..0e4d786 --- /dev/null +++ b/Public/ecma/Managers/WebSocketsClientsManager.ecma.js @@ -0,0 +1,65 @@ +"use strict"; + +import {WebSocketClientModel} from "../Models/WebSocketClientModel.ecma.js"; + +/** + * @typedef {import("../Application/AnP.ecma.js").AnP} AnP + */ + +/** + * @class WebSocketsClientsManager + * @constructor + * @param {!AnP} anp + * @param {!string} key + * @param {!(Object.|Array.)} inputs + * @returns {void} + * @access private + * @static + */ +export const WebSocketsClientsManager = (function(){ + + /** + * @constructs WebSocketsClientsManager + * @param {!AnP} anp + * @param {!string} key + * @param {!(Object.|Array.)} inputs + * @returns {void} + * @access private + * @static + */ + const WebSocketsClientsManager = function(anp, key, inputs){ + + /** @type {WebSocketsClientsManager} */ + const self = this, + /** @type {Object. {}; + + this.add = (key, url) => { + + clients[key] = new WebSocketClientModel(anp, url); + + }; + + this.remove = key => { + if(key in clients){ + clients[key].close(); + delete clients[key]; + return true; + }; + return false; + }; + + this.send = (key, data) => {}; + + constructor(); + + }; + + return WebSocketsClientsManager; +})(); \ No newline at end of file diff --git a/Public/ecma/Models/WebSocketClientModel.ecma.js b/Public/ecma/Models/WebSocketClientModel.ecma.js new file mode 100644 index 0000000..01f5f86 --- /dev/null +++ b/Public/ecma/Models/WebSocketClientModel.ecma.js @@ -0,0 +1,92 @@ +"use strict"; + +import {Event} from "../Application/Event.ecma.js"; +import {Check} from "../Utils/Checks.ecma.js"; +import {Common} from "../Utils/Common.ecma.js"; + +/** + * @typedef {import("../Application/AnP.ecma.js").AnP} AnP + */ + +export const WebSocketClientModel = (function(){ + + const WebSocketClientModel = function(anp, inputs){ + + const self = this; + let web_socket = null, + url = null, + id = null; + + this.on_open = new Event(); + this.on_message = new Event(); + this.on_close = new Event(); + this.on_error = new Event(); + + const constructor = () => { + + Check.is_string(inputs) && (inputs = {url : inputs}); + + web_socket = new WebSocket(url = Common.get_value("url", inputs, "")); + + web_socket.onopen = self.on_open.execute; + web_socket.onmessage = self.on_message.execute; + web_socket.onclose = self.on_close.execute; + web_socket.onerror = self.on_error.execute; + + // self.on_open.add(opened); + self.on_message.add(reveive_message); + self.on_close.add(closed); + self.on_error.add(error); + + }; + + // const opened = event => { + // self.send("web_socket_client", "get_id", null, 200); + // }; + + const reveive_message = event => { + console.log(event); + + const data = Common.data_decode(event.data); + + switch(data.controller + "." + data.method){ + case "web_socket_client.set_id": + id = data.data; + break; + default: + anp.controllers.execute(data.controller, data.method, data.data, data); + break; + }; + + }; + + this.send = (controller, method, data, code = 200) => { + web_socket.send(Common.data_encode({ + ok : code >= 200 && code < 300, + code : code, + id : id, + controller : controller, + method : method, + data : data + })); + }; + + this.close = () => { + web_socket.close(); + }; + + const closed = event => { + id = null; + }; + + const error = event => { + console.error(event); + self.close(); + }; + + constructor(); + + }; + + return WebSocketClientModel; +})(); \ No newline at end of file diff --git a/Python/Abstracts/AIInterpretersAbstract.py b/Python/Abstracts/AIInterpretersAbstract.py index aa499cc..72c801e 100644 --- a/Python/Abstracts/AIInterpretersAbstract.py +++ b/Python/Abstracts/AIInterpretersAbstract.py @@ -18,7 +18,7 @@ class AIInterpretersAbstract(ABC): self.key:str = key self.url:str = self.anp.settings.get(("ai_interpreter_url", "ai_url", "url"), inputs, "") self.stream:bool = self.anp.settings.get(("ai_interpreter_stream", "ai_stream", "stream"), inputs, False) - self.orders:list[str] = self.anp.settings.get(("ai_interpreter_orders", "ai_orders", "orders"), inputs, []) + self.orders:str|list[str] = self.anp.settings.get(("ai_interpreter_orders", "ai_orders", "orders"), inputs, []) self.maximum_tokens_per_session:int = self.anp.settings.get(( "ai_interpreter_maximum_tokens_per_session", "ai_maximum_tokens_per_session", "maximum_tokens_per_session" ), inputs, None) @@ -55,6 +55,23 @@ class AIInterpretersAbstract(ABC): del self.sessions[id] return True return False + + def get_orders(self:Self, orders:str|list[str]) -> str: + + results:str = "" + i:int = 0 + + for block in (orders, self.orders): + if block: + if Check.is_array(block): + + block_string:str = "".join(str(i + 1 + j) + ". " + str(order) + "\n" for j, order in enumerate(block)) + + i += len(block) + results += ("\n\n" if results else "") + block_string.strip() + + else: + results += ("\n\n" if results else "") + str(block).strip() @abstractmethod def request(self:Self, diff --git a/Python/Abstracts/WebSocketServersAbstract.py b/Python/Abstracts/WebSocketServersAbstract.py index 071de89..b27edce 100644 --- a/Python/Abstracts/WebSocketServersAbstract.py +++ b/Python/Abstracts/WebSocketServersAbstract.py @@ -5,6 +5,7 @@ from typing import Any, Self, Optional, Sequence from abc import ABC, abstractmethod from Interfaces.Application.AnPInterface import AnPInterface from Application.Event import Event +from Utils.Common import Common class WebSocketServersAbstract(ABC): @@ -24,5 +25,14 @@ class WebSocketServersAbstract(ABC): @abstractmethod def close_client(self:Self, id:int) -> None:pass + def format_data(self:Self, controller:str, method:str, data:Any|None, id:str, code:int = 200) -> str: + return Common.data_encode({ + "controller": controller, + "method": method, + "data": data, + "id": id, + "code": code + }) + @abstractmethod - def send(self:Self, data:Any|None, ids:Optional[int|Sequence[int]] = None) -> None:pass \ No newline at end of file + def send(self:Self, controller:str, method:str, data:Any|None, ids:Optional[int|Sequence[int]] = None, code:int = 200) -> None:pass \ No newline at end of file diff --git a/Python/Drivers/OllamaDriver.py b/Python/Drivers/OllamaDriver.py index 25b646c..5ced5da 100644 --- a/Python/Drivers/OllamaDriver.py +++ b/Python/Drivers/OllamaDriver.py @@ -24,7 +24,7 @@ class OllamaDriver(AIInterpretersAbstract, ModelAbstract): session:int|None, message:str, callback:Optional[Callable[[int, AIResponseModel], None]] = None, - orders:list[str] = [] + orders:str|list[str] = [] ) -> tuple[int|None, AIResponseModel]: response:Response @@ -38,15 +38,13 @@ class OllamaDriver(AIInterpretersAbstract, ModelAbstract): if self.maximum_tokens_per_session is not None: options["num_ctx"] = self.maximum_tokens_per_session - orders += self.orders session, context = self.get_session(session) + orders = self.get_orders(orders) with Post(self.url, json = { "model" : self.model, "prompt": message, - **({"system": "\\n".join( - str(i + 1) + ". " + order for i, order in enumerate(orders, 1) - )} if len(orders) else {}), + **({"system": orders} if len(orders) else {}), "stream": self.stream, **( {"format" : self.format} if ( diff --git a/Python/Drivers/WebSocketServerDriver.py b/Python/Drivers/WebSocketServerDriver.py index c39a246..f530632 100644 --- a/Python/Drivers/WebSocketServerDriver.py +++ b/Python/Drivers/WebSocketServerDriver.py @@ -16,10 +16,9 @@ class WebSocketServerDriver(WebSocketServersAbstract, ModelAbstract): super().__init__(anp, inputs) self.__server:WebSocketServer - self.__clients:dict[int, WebSocketClient] = {} + self.__clients:dict[str, WebSocketClient] = {} self.__host:str = anp.settings.get(("web_socket_server_host", "host"), inputs, "") self.__port:int = anp.settings.get(("web_socket_server_port", "port"), inputs, 8765) - self.__client_i:int = 0 self.__thread:Thread = None anp.settings.get(("web_socket_server_autostart", "autostart"), inputs, True) and self.start() @@ -55,9 +54,7 @@ class WebSocketServerDriver(WebSocketServersAbstract, ModelAbstract): def __handler(self:Self, client:WebSocketClient) -> None: - id:int = self.__client_i - - self.__client_i += 1 + id:int = self.anp.unique_keys.create() self.__clients[id] = client self.on_new_client.execute(id) @@ -70,6 +67,8 @@ class WebSocketServerDriver(WebSocketServersAbstract, ModelAbstract): "client_port" : client.remote_address[1] }) + self.send("web_socket_client", "set_id", id, id) + try: while self.anp.working(): message:str = client.recv() @@ -85,6 +84,7 @@ class WebSocketServerDriver(WebSocketServersAbstract, ModelAbstract): self.on_error.execute(id, exception) finally: self.close_client(id) + self.anp.unique_keys.remove(id) self.on_close.execute(id) self.anp.print("info", "web_socket_server_client_disconnected", { "client": id, @@ -92,15 +92,18 @@ class WebSocketServerDriver(WebSocketServersAbstract, ModelAbstract): "host": self.host }) - def send(self:Self, data:str, ids:int|Sequence[int]) -> bool: + def send(self:Self, controller:str, method:str, data:str, ids:Optional[int|Sequence[int]] = None, code:int = 200) -> bool: success:bool = True id:int - for id in ids if Check.is_array(ids) else [ids]: + for id in ( + list(self.__clients.keys()) if ids is None else + ids if Check.is_array(ids) else + [ids]): if id in self.__clients: try: - self.__clients[id].send(data) + self.__clients[id].send(self.format_data(controller, method, data, id, code)) except Exception as exception: self.anp.exception(exception, "web_socket_server_client_send_exception", { "client": id,