From c3fe1bb6fd94d62af91fedc47683d1c4eda30599 Mon Sep 17 00:00:00 2001 From: mbruzon Date: Sat, 9 May 2026 09:31:24 +0200 Subject: [PATCH] #wip: For redo. --- .gitignore | 4 +- AIChat.py | 333 ++++++++++++++++++++++++++--- JSON/AIChat.settings.json | 6 +- JSON/I18N/AIChat.i18n.espanol.json | 3 + Public/ecma/AIChat.ecma.js | 80 ++++++- 5 files changed, 385 insertions(+), 41 deletions(-) create mode 100644 JSON/I18N/AIChat.i18n.espanol.json diff --git a/.gitignore b/.gitignore index 55c94fc..a4b7d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /ollama /open-webui -/websockets \ No newline at end of file +/websockets +*.[Ss]ecrets.* +*.[Ss]ecret.* \ No newline at end of file diff --git a/AIChat.py b/AIChat.py index 7047f1f..8c8e3a2 100644 --- a/AIChat.py +++ b/AIChat.py @@ -1,24 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Self, Any, Optional, Sequence +from typing import Self, Any, Optional, Sequence, Callable from requests import post as Post, Response from os.path import dirname as directory_name, abspath as absolute_path, exists as path_exists from io import FileIO from re import compile as re_compile, Pattern as REPattern, Match as REMatch, IGNORECASE as RE_IGNORECASE from threading import Thread from json import loads as json_decode, dumps as json_encode +from time import sleep from http import server as http_server from http.server import BaseHTTPRequestHandler, HTTPServer from mimetypes import guess_type as read_mime_types from websockets.sync.server import serve as web_socket_server_serve from websockets import ServerConnection as WebSocketServer, ClientConnection as WebSocketClient -class ClientRequestModel: - def __init__(self:Self, client:WebSocketClient, message:str) -> None: - self.client:WebSocketClient = client - self.message:str = message - class AIChat: ROOT_PATH:str = directory_name(absolute_path(__file__)) @@ -29,7 +25,7 @@ class AIChat: "titles_model" : "gemma3:1b", "titles_temperature" : 0.0, "titles_prompt_file" : "/TXT/AIChat.titles-prompt.md", - "responses_model" : "gemma3", + "responses_model" : "gemma3:1b", "responses_temperature" : 7.0, "response_with_titles_prompt_file" : "/TXT/AIChat.response-with-titles.md", } @@ -51,36 +47,47 @@ class AIChat: self.__sentences:dict[str, dict[str, str|Sequence[str]]] = {language : { key : value for key, value in sentences.items() } for language, sentences in self.DEFAULT_SENTENCES.items()} + self.__default_language:str = self.get("default_language", inputs, "espanol") self.__language:str = self.get("language", inputs, self.__default_language) - self.__host:str = self.get("host", inputs, "localhost") - self.__port:int = self.get("port", inputs, 11434) - self.__api_url:str = self.get("api_url", inputs, "http://{host}:{port}/api/generate") + + self.__host:str = "localhost" + self.__port:int = 11434 + self.__api_url:str = "http://{host}:{port}/api/generate" + self.__index:dict[str, str] = {} + self.__requests_i:int = 0 self.__requests_order:list[int] = [] - self.__requests:dict[int, ClientRequestModel] = {} + self.__requests:dict[int, dict[str, Any|None]] = {} self.__web_socket_server:WebSocketServer - self.__web_socket_host:str = self.get("web_socket_host", inputs, "localhost") - self.__web_socket_port:int = self.get("web_socket_port", inputs, 18001) + self.__web_socket_host:str = "localhost" + self.__web_socket_port:int = 18001 self.__web_socket_server_thread:Thread = Thread(target = self.__run_web_socket_server) - self.__titles_model:str = self.get("titles_model", inputs) - self.__titles_temperature:float = self.get("titles_temperature", inputs) - self.__titles_prompt:str = self.RE_NEW_LINES.sub("\\n", self.load_file(self.get("titles_prompt_file", inputs))) - self.__response_model:str = self.get("responses_model", inputs) - self.__response_temperature:float = self.get("responses_temperature", inputs) - self.__response_with_titles:str = self.RE_NEW_LINES.sub("\\n", self.load_file(self.get("response_with_titles_prompt_file", inputs))) + self.__requests_thread:Thread = Thread(target = self.__run_orders) + self.__requests_sleep:float = 0.1 + + self.__titles_model:str = "gemma3:1b" + self.__titles_temperature:float = 0.0 + self.__titles_prompt:str = self.RE_NEW_LINES.sub("\\n", self.load_file("/TXT/AIChat.titles-prompt.md")) + self.__response_model:str = "gemma3:1b" + self.__response_temperature:float = 7.0 + self.__response_with_titles:str = self.RE_NEW_LINES.sub("\\n", self.load_file("/TXT/AIChat.response-with-titles.md")) + self.__http_server:HTTPServer self.__http_server_thread:Thread = Thread(target = self.__run_http_server) - self.__http_host:str = self.get("http_host", inputs, "") - self.__http_port:int = self.get("http_port", inputs, 18000) - self.__http_directories:list[str] = self.get("http_indexes", inputs, ["/Public"]) - self.__http_indexes:list[str] = self.get("http_indexes", inputs, ["", "index.html", "index.htm", "index.md", "index.txt"]) + self.__http_host:str = "" + self.__http_port:int = 18000 + self.__http_directories:list[str] = ["/Public"] + self.__http_indexes:list[str] = ["", "index.html", "index.htm", "index.md", "index.txt"] self.__terminal_thread:Thread = Thread(target = self.__terminal) self.__web_sockets_clients:list[WebSocketClient] = [] + self.update() + self.__terminal_thread.start() self.__http_server_thread.start() self.__web_socket_server_thread.start() + self.__requests_thread.start() def close(self:Self) -> None: @@ -98,6 +105,63 @@ class AIChat: self.__http_server.shutdown() + def update(self:Self) -> None: + + titles_promp_file:str + response_with_titles_prompt_file:str + key:str + + for key in ("default_settings_files", "settings_files", "default_settings", "settings"): + self.add_settings(self.get(key), True) + for key in ("default_secrets_files", "secrets_files", "default_secrets", "secrets"): + self.add_secrets(self.get(key), True) + for key in ("default_i18n_files", "i18n_files", "default_i18n", "i18n"): + self.add_i18n(self.get(key), True) + for key in ( + "default_index_files", "index_files", "default_index", "index", + "default_secrets_index_files", "secrets_index_files", "default_secrets_index", "secrets_index" + ): + self.add_index(self.get(key), True) + + titles_promp_file = self.get("titles_prompt_file") + response_with_titles_prompt_file = self.get("response_with_titles_prompt_file") + + self.__default_language = self.get("default_language", None, self.__default_language) + self.__language = self.get("language", None, self.__language) + + self.__host = self.get("host", None, self.__host) + self.__port = self.get("port", None, self.__port) + self.__api_url = self.get("api_url", None, self.__api_url) + + self.__web_socket_host = self.get("web_socket_host", None, self.__web_socket_host) + self.__web_socket_port = self.get("web_socket_port", None, self.__web_socket_port) + self.__requests_sleep = self.get("requests_sleep", None, self.__requests_sleep) + + self.__titles_model = self.get("titles_model", None, self.__http_host) + self.__titles_temperature = self.get("titles_temperature", None, self.__http_host) + if titles_promp_file is not None: + self.__titles_prompt = self.RE_NEW_LINES.sub("\\n", self.load_file(titles_promp_file)) + self.__response_model = self.get("responses_model", None, self.__http_host) + self.__response_temperature = self.get("responses_temperature", None, self.__http_host) + if response_with_titles_prompt_file is not None: + self.__response_with_titles = self.RE_NEW_LINES.sub("\\n", self.load_file(response_with_titles_prompt_file)) + + self.__http_host = self.get("http_host", None, self.__http_host) + self.__http_port = self.get("http_port", None, self.__http_port) + self.__http_directories = self.get("http_indexes", None, self.__http_directories) + self.__http_indexes = self.get("http_indexes", None, self.__http_indexes) + + def reset(self:Self) -> None: + self.__settings.clear() + self.__secrets.clear() + self.__sentences.clear() + self.__index.clear() + self.update() + + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## + def get(self:Self, keys:str|Sequence[str], inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None, @@ -137,6 +201,78 @@ class AIChat: return self.string_variables("".join(text) if isinstance(text, (list, tuple)) else str(text), inputs) + def add_settings(self:Self, inputs:Any|None, overwrite:bool = False) -> None: + + subinputs:dict[str, Any|None] + + for subinputs in self.get_dictionaries(inputs, True): + + key:str + value:Any|None + + for key, value in subinputs.items(): + if overwrite or key not in self.__settings: + self.__settings[key] = value + + def add_secrets(self:Self, inputs:Any|None, overwrite:bool = False) -> None: + + subinputs:dict[str, Any|None] + + for subinputs in self.get_dictionaries(inputs, True): + + key:str + value:Any|None + + for key, value in subinputs.items(): + if overwrite or key not in self.__secrets: + self.__secrets[key] = value + + def add_i18n(self:Self, inputs:dict[str, Any|None], overwrite:bool = False) -> None: + + subinputs:dict[str, Any|None] + + for subinputs in self.get_dictionaries(inputs, True): + + language:str + sentences:dict[str, str|Sequence[str]] + + for language, sentences in subinputs.items(): + if language not in self.__sentences: + self.__sentences[language] = {} + if overwrite or language not in self.__sentences: + + key:str + value:str|Sequence[str] + + for key, value in sentences.items(): + if overwrite or key not in self.__sentences[language]: + self.__sentences[language][key] = value + + def add_index(self:Self, inputs:dict[str, str], overwrite:bool = False, subgroup:Optional[dict[str, str|dict]] = None) -> None: + + group:dict[str, Any|None] + + if subgroup is None: + subgroup = self.__index + + for group in self.get_dictionaries(inputs, True): + + key:str + value:str|dict + + for key, value in group.items(): + if isinstance(value, str): + if overwrite or key not in subgroup: + subgroup[key] = value + elif isinstance(value, dict): + if key not in subgroup: + subgroup[key] = {} + self.add_index(value, overwrite, subgroup[key]) + + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## + def __terminal(self:Self) -> None: while self.__working: try: @@ -145,12 +281,22 @@ class AIChat: if command in ("exit", "close", "quit", "stop", "bye"): self.close() + elif command in ("update",): + self.update() + elif command in ("reset",): + self.reset() + elif command in ("help", "h", "?"): + print(self.i18n("commands_help")) else: print(self.i18n("unknown_command", {"command" : command})) except Exception as exception: print(exception) + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## + def __run_web_socket_server(self:Self) -> None: with web_socket_server_serve(self.__web_socket_handler, self.__web_socket_host, self.__web_socket_port) as self.__web_socket_server: self.__web_socket_server.serve_forever() @@ -161,7 +307,31 @@ class AIChat: try: for message in client: try: - print(message) + self.__requests_i += 1 + + i:int = self.__requests_i + new_order:dict[str, Any|None] = { + **json_decode(message), + "__client__" : client, + "__i__" : i + } + + print(new_order) + + if new_order["action"] == "cancel": + self.__remove_order(new_order["i"]) + continue + + i:int + order:dict[str, Any|None] + + for i, order in self.__requests.items(): + if new_order["action"] == order["action"] and order["__client__"] == client: + self.__remove_order(i) + + self.__requests[i] = new_order + self.__requests_order.append(i) + except Exception as exception: print(exception) except Exception as exception: @@ -169,6 +339,41 @@ class AIChat: self.__web_sockets_clients.remove(client) print("Client disconnected") + def __run_orders(self:Self) -> None: + while self.__working: + if len(self.__requests_order): + + order:dict[Any|None] = self.__requests[self.__requests_order[0]] + + if order["action"] == "new_message": + Thread(target = lambda:self.send_to_ai(order["message"], lambda chunk:order["__client__"].send(json_encode({ + "order" : order["__i__"], + "key" : order["key"], + "chunk" : json_decode(chunk) + })), lambda:self.__remove_order_and_continue(order["__i__"]), order)).start() + else: + Thread(target = lambda:self.__remove_order_and_continue(order["__i__"])).start() + break + + sleep(0.1) + + def __remove_order(self:Self, i:int) -> None: + print("PASA REMOVE") + if i in self.__requests: + del self.__requests[i] + if i in self.__requests_order: + self.__requests_order.remove(i) + + def __remove_order_and_continue(self:Self, i:int) -> None: + print("PASA REMOVE AND CONTINUE") + self.__remove_order(i) + sleep(0.1) + self.__run_orders() + + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## + def __run_http_server(self:Self) -> None: self.__http_server = HTTPServer((self.__http_host, self.__http_port), self.HTTPRequestHandler) self.__http_server.aichat = self @@ -220,6 +425,10 @@ class AIChat: else: self.send_response(404) self.end_headers() + + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## def get_titles(self:Self, message:str) -> list[str]: try: @@ -244,38 +453,65 @@ class AIChat: pass return [] - def send(self:Self, message:str, client:WebSocketClient) -> str: + def send_to_ai(self:Self, + message:str, + each_callback:Callable[[bytes], None], + end_callback:Optional[Callable[[], None]] = None, + order:Optional[dict[str, Any|None]] = None + ) -> None: - titles:list[str] = self.get_titles(message) + # titles:list[str] = self.get_titles(message) + titles:list[str] = [] + + print([titles, message]) try: response:Response + print(self.string_variables(self.__response_with_titles, { + "message" : message, + "guides" : "\n\n".join("## " + title + "\n\n" + self.load_file(self.__index[title]) for title in titles) + }) if len(titles) else message) + with Post(self.string_variables(self.__api_url, { "host" : self.__host, "port" : self.__port }), json = { - "model" : self.__titles_model, + "model" : self.__response_model, "prompt" : self.string_variables(self.__response_with_titles, { "message" : message, "guides" : "\n\n".join("## " + title + "\n\n" + self.load_file(self.__index[title]) for title in titles) }) if len(titles) else message, "stream" : True, "options" : { - "temperature" : self.__titles_temperature + "temperature" : self.__response_temperature } }, stream = True) as response: - line:str + line:bytes for line in response.iter_lines(): - if line: - client.send({"chunk" : json_decode(line)}) + each_callback(line) + # print(["LINE", line, order is not None and order["__i__"] not in self.__requests]) + # if order is not None and order["__i__"] not in self.__requests: + # break + # if line: + # print("EACH START") + # each_callback(line) + # print("EACH END") + # print(json_decode(line).get("done", False)) + # if json_decode(line).get("done", False): + # print("BREAK") + # end_callback and end_callback() + # break except Exception as exception: - pass - return [] + print(exception) + + ########################################################################################################## + ########################################################################################################## + ########################################################################################################## @classmethod def get_keys(cls:type[Self], *items:list[Any|None]) -> list[str]: @@ -439,4 +675,35 @@ class AIChat: pass return None + @classmethod + def load_json(cls:type[Self], + data:str|dict[str, Any|None]|Sequence[Any|None], + only_dictionaries:bool = True + ) -> list[dict[str, Any|None]|Sequence[Any|None]]: + + json:list[dict[str, Any|None]|Sequence[Any|None]] = [] + + if isinstance(data, str): + + subdata:dict[str, Any|None]|Sequence[Any|None]|None + + try: + if subdata := json_decode(data): + json.extend(cls.load_json(subdata, only_dictionaries)) + return + except Exception as exception: + pass + + json.extends(cls.load_json(json_decode(cls.load_file(cls.fix_path(data), "r")))) + + elif isinstance(data, (list, tuple)): + if only_dictionaries: + + item:Any|None + + for item in data: + json.extend(cls.load_json(item, only_dictionaries)) + else: + json.append(data) + aichat:AIChat = AIChat() \ No newline at end of file diff --git a/JSON/AIChat.settings.json b/JSON/AIChat.settings.json index 0bc8a67..c9f8f30 100644 --- a/JSON/AIChat.settings.json +++ b/JSON/AIChat.settings.json @@ -2,5 +2,9 @@ "autostart" : true, "default_settings_files" : "/JSON/AIChat.settings.json", "ai_model" : "gemma", - "ai_stream" : true + "ai_stream" : true, + "default_titles_files" : [ + "/JSON/AIChat.titles.json", + "/JSON/AIChat.titles.secrets.json" + ] } \ No newline at end of file diff --git a/JSON/I18N/AIChat.i18n.espanol.json b/JSON/I18N/AIChat.i18n.espanol.json new file mode 100644 index 0000000..f464355 --- /dev/null +++ b/JSON/I18N/AIChat.i18n.espanol.json @@ -0,0 +1,3 @@ +{ + "espanol" : {} +} \ No newline at end of file diff --git a/Public/ecma/AIChat.ecma.js b/Public/ecma/AIChat.ecma.js index fb7d449..8819489 100644 --- a/Public/ecma/AIChat.ecma.js +++ b/Public/ecma/AIChat.ecma.js @@ -50,7 +50,9 @@ export const AIChat = (function(){ /** @type {number} */ frames_per_second = 60, /** @type {WebSocket|null} */ - web_socket_client = null; + web_socket_client = null, + /** @type {HTMLArticleElement|null} */ + last_ai_message = null; /** * @returns {void} @@ -118,11 +120,55 @@ export const AIChat = (function(){ return end(true); }; - const on_web_socket_open = event => { - web_socket_client.send("Hello, WebSocket server!"); + const set_new_message = (sender, message) => { + + const messages = document.querySelector(".aichat .messages"); + + AIChat.HTML(messages, ["article", { + "data-sender" : "ai" + }, message]); + messages.setAttribute("data-messages", Number(messages.getAttribute("data-messages")) + 1); + }; - const on_web_socket_message = event => {}; + const on_web_socket_open = event => { + // web_socket_client.send("Hello, WebSocket server!"); + }; + + const on_web_socket_message = event => { + if(event.data instanceof Blob){ + + const reader = new FileReader(); + + reader.onload = () => { + + const reponse = JSON.parse(reader.result); + + if(reponse.chunk.done){ + last_ai_message = null; + }else{ + if(!last_ai_message){ + last_ai_message = document.querySelector(".aichat .messages").appendChild(document.createElement("article")); + last_ai_message.setAttribute("data-sender", "ai"); + }; + last_ai_message.innerHTML += reponse.chunk.response; + }; + + }; + + reader.readAsText(event.data); + }; + + // console.log(event.data.text()); + + // console.log(JSON.parse(event.data)); + + // const data = JSON.parse(event.data); + + // if(data.action == "message") + // set_new_message("ai", data.message); + + }; const on_web_socket_error = event => {}; @@ -191,7 +237,10 @@ export const AIChat = (function(){ ["main", null, [ ["fieldset", {class : "chat"}, [ ["legend", {data_i18n : "chat"}, "Chat"], - ["section", {"class" : "messages"}], + ["section", { + "class" : "messages", + "data-messages" : 0 + }], ["form", { action : "#", method : "post", @@ -238,9 +287,28 @@ export const AIChat = (function(){ */ const send = (item, event) => { + /** @type {HTMLTextAreaElement} */ + const textarea = document.querySelector(".aichat form textarea"), + /** @type {string} */ + message = textarea.value, + /** @type {HTMLSectionElement} */ + messages_box = document.querySelector(".aichat .messages"), + /** @type {HTMLArticleElement} */ + new_message = messages_box.appendChild(document.createElement("article")), + /** @type {number} */ + i = Number(messages_box.getAttribute("data-messages")); + event.preventDefault(); - web_socket_client.send(document.querySelector(".aichat form textarea").value); + textarea.value = ""; + + set_new_message("user", message); + web_socket_client.send(JSON.stringify({ + i : i, + key : "TEST KEY", + action : "new_message", + message : message + })); return false; };