#wip: For redo.

This commit is contained in:
mbruzon 2026-05-09 09:31:24 +02:00
parent 4552cf4c81
commit c3fe1bb6fd
5 changed files with 385 additions and 41 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
/ollama /ollama
/open-webui /open-webui
/websockets /websockets
*.[Ss]ecrets.*
*.[Ss]ecret.*

333
AIChat.py
View File

@ -1,24 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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 requests import post as Post, Response
from os.path import dirname as directory_name, abspath as absolute_path, exists as path_exists from os.path import dirname as directory_name, abspath as absolute_path, exists as path_exists
from io import FileIO from io import FileIO
from re import compile as re_compile, Pattern as REPattern, Match as REMatch, IGNORECASE as RE_IGNORECASE from re import compile as re_compile, Pattern as REPattern, Match as REMatch, IGNORECASE as RE_IGNORECASE
from threading import Thread from threading import Thread
from json import loads as json_decode, dumps as json_encode from json import loads as json_decode, dumps as json_encode
from time import sleep
from http import server as http_server from http import server as http_server
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from mimetypes import guess_type as read_mime_types from mimetypes import guess_type as read_mime_types
from websockets.sync.server import serve as web_socket_server_serve from websockets.sync.server import serve as web_socket_server_serve
from websockets import ServerConnection as WebSocketServer, ClientConnection as WebSocketClient 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: class AIChat:
ROOT_PATH:str = directory_name(absolute_path(__file__)) ROOT_PATH:str = directory_name(absolute_path(__file__))
@ -29,7 +25,7 @@ class AIChat:
"titles_model" : "gemma3:1b", "titles_model" : "gemma3:1b",
"titles_temperature" : 0.0, "titles_temperature" : 0.0,
"titles_prompt_file" : "/TXT/AIChat.titles-prompt.md", "titles_prompt_file" : "/TXT/AIChat.titles-prompt.md",
"responses_model" : "gemma3", "responses_model" : "gemma3:1b",
"responses_temperature" : 7.0, "responses_temperature" : 7.0,
"response_with_titles_prompt_file" : "/TXT/AIChat.response-with-titles.md", "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 : { self.__sentences:dict[str, dict[str, str|Sequence[str]]] = {language : {
key : value for key, value in sentences.items() key : value for key, value in sentences.items()
} for language, sentences in self.DEFAULT_SENTENCES.items()} } for language, sentences in self.DEFAULT_SENTENCES.items()}
self.__default_language:str = self.get("default_language", inputs, "espanol") self.__default_language:str = self.get("default_language", inputs, "espanol")
self.__language:str = self.get("language", inputs, self.__default_language) 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.__host:str = "localhost"
self.__api_url:str = self.get("api_url", inputs, "http://{host}:{port}/api/generate") self.__port:int = 11434
self.__api_url:str = "http://{host}:{port}/api/generate"
self.__index:dict[str, str] = {} self.__index:dict[str, str] = {}
self.__requests_i:int = 0
self.__requests_order:list[int] = [] 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_server:WebSocketServer
self.__web_socket_host:str = self.get("web_socket_host", inputs, "localhost") self.__web_socket_host:str = "localhost"
self.__web_socket_port:int = self.get("web_socket_port", inputs, 18001) self.__web_socket_port:int = 18001
self.__web_socket_server_thread:Thread = Thread(target = self.__run_web_socket_server) self.__web_socket_server_thread:Thread = Thread(target = self.__run_web_socket_server)
self.__titles_model:str = self.get("titles_model", inputs) self.__requests_thread:Thread = Thread(target = self.__run_orders)
self.__titles_temperature:float = self.get("titles_temperature", inputs) self.__requests_sleep:float = 0.1
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.__titles_model:str = "gemma3:1b"
self.__response_temperature:float = self.get("responses_temperature", inputs) self.__titles_temperature:float = 0.0
self.__response_with_titles:str = self.RE_NEW_LINES.sub("\\n", self.load_file(self.get("response_with_titles_prompt_file", inputs))) 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:HTTPServer
self.__http_server_thread:Thread = Thread(target = self.__run_http_server) self.__http_server_thread:Thread = Thread(target = self.__run_http_server)
self.__http_host:str = self.get("http_host", inputs, "") self.__http_host:str = ""
self.__http_port:int = self.get("http_port", inputs, 18000) self.__http_port:int = 18000
self.__http_directories:list[str] = self.get("http_indexes", inputs, ["/Public"]) self.__http_directories:list[str] = ["/Public"]
self.__http_indexes:list[str] = self.get("http_indexes", inputs, ["", "index.html", "index.htm", "index.md", "index.txt"]) self.__http_indexes:list[str] = ["", "index.html", "index.htm", "index.md", "index.txt"]
self.__terminal_thread:Thread = Thread(target = self.__terminal) self.__terminal_thread:Thread = Thread(target = self.__terminal)
self.__web_sockets_clients:list[WebSocketClient] = [] self.__web_sockets_clients:list[WebSocketClient] = []
self.update()
self.__terminal_thread.start() self.__terminal_thread.start()
self.__http_server_thread.start() self.__http_server_thread.start()
self.__web_socket_server_thread.start() self.__web_socket_server_thread.start()
self.__requests_thread.start()
def close(self:Self) -> None: def close(self:Self) -> None:
@ -98,6 +105,63 @@ class AIChat:
self.__http_server.shutdown() 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, def get(self:Self,
keys:str|Sequence[str], keys:str|Sequence[str],
inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None, 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) 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: def __terminal(self:Self) -> None:
while self.__working: while self.__working:
try: try:
@ -145,12 +281,22 @@ class AIChat:
if command in ("exit", "close", "quit", "stop", "bye"): if command in ("exit", "close", "quit", "stop", "bye"):
self.close() 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: else:
print(self.i18n("unknown_command", {"command" : command})) print(self.i18n("unknown_command", {"command" : command}))
except Exception as exception: except Exception as exception:
print(exception) print(exception)
##########################################################################################################
##########################################################################################################
##########################################################################################################
def __run_web_socket_server(self:Self) -> None: 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: 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() self.__web_socket_server.serve_forever()
@ -161,7 +307,31 @@ class AIChat:
try: try:
for message in client: for message in client:
try: 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: except Exception as exception:
print(exception) print(exception)
except Exception as exception: except Exception as exception:
@ -169,6 +339,41 @@ class AIChat:
self.__web_sockets_clients.remove(client) self.__web_sockets_clients.remove(client)
print("Client disconnected") 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: def __run_http_server(self:Self) -> None:
self.__http_server = HTTPServer((self.__http_host, self.__http_port), self.HTTPRequestHandler) self.__http_server = HTTPServer((self.__http_host, self.__http_port), self.HTTPRequestHandler)
self.__http_server.aichat = self self.__http_server.aichat = self
@ -220,6 +425,10 @@ class AIChat:
else: else:
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()
##########################################################################################################
##########################################################################################################
##########################################################################################################
def get_titles(self:Self, message:str) -> list[str]: def get_titles(self:Self, message:str) -> list[str]:
try: try:
@ -244,38 +453,65 @@ class AIChat:
pass pass
return [] 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: try:
response:Response 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, { with Post(self.string_variables(self.__api_url, {
"host" : self.__host, "host" : self.__host,
"port" : self.__port "port" : self.__port
}), json = { }), json = {
"model" : self.__titles_model, "model" : self.__response_model,
"prompt" : self.string_variables(self.__response_with_titles, { "prompt" : self.string_variables(self.__response_with_titles, {
"message" : message, "message" : message,
"guides" : "\n\n".join("## " + title + "\n\n" + self.load_file(self.__index[title]) for title in titles) "guides" : "\n\n".join("## " + title + "\n\n" + self.load_file(self.__index[title]) for title in titles)
}) if len(titles) else message, }) if len(titles) else message,
"stream" : True, "stream" : True,
"options" : { "options" : {
"temperature" : self.__titles_temperature "temperature" : self.__response_temperature
} }
}, stream = True) as response: }, stream = True) as response:
line:str line:bytes
for line in response.iter_lines(): for line in response.iter_lines():
if line: each_callback(line)
client.send({"chunk" : json_decode(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: except Exception as exception:
pass print(exception)
return []
##########################################################################################################
##########################################################################################################
##########################################################################################################
@classmethod @classmethod
def get_keys(cls:type[Self], *items:list[Any|None]) -> list[str]: def get_keys(cls:type[Self], *items:list[Any|None]) -> list[str]:
@ -439,4 +675,35 @@ class AIChat:
pass pass
return None 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() aichat:AIChat = AIChat()

View File

@ -2,5 +2,9 @@
"autostart" : true, "autostart" : true,
"default_settings_files" : "/JSON/AIChat.settings.json", "default_settings_files" : "/JSON/AIChat.settings.json",
"ai_model" : "gemma", "ai_model" : "gemma",
"ai_stream" : true "ai_stream" : true,
"default_titles_files" : [
"/JSON/AIChat.titles.json",
"/JSON/AIChat.titles.secrets.json"
]
} }

View File

@ -0,0 +1,3 @@
{
"espanol" : {}
}

View File

@ -50,7 +50,9 @@ export const AIChat = (function(){
/** @type {number} */ /** @type {number} */
frames_per_second = 60, frames_per_second = 60,
/** @type {WebSocket|null} */ /** @type {WebSocket|null} */
web_socket_client = null; web_socket_client = null,
/** @type {HTMLArticleElement|null} */
last_ai_message = null;
/** /**
* @returns {void} * @returns {void}
@ -118,11 +120,55 @@ export const AIChat = (function(){
return end(true); return end(true);
}; };
const on_web_socket_open = event => { const set_new_message = (sender, message) => {
web_socket_client.send("Hello, WebSocket server!");
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 => {}; const on_web_socket_error = event => {};
@ -191,7 +237,10 @@ export const AIChat = (function(){
["main", null, [ ["main", null, [
["fieldset", {class : "chat"}, [ ["fieldset", {class : "chat"}, [
["legend", {data_i18n : "chat"}, "Chat"], ["legend", {data_i18n : "chat"}, "Chat"],
["section", {"class" : "messages"}], ["section", {
"class" : "messages",
"data-messages" : 0
}],
["form", { ["form", {
action : "#", action : "#",
method : "post", method : "post",
@ -238,9 +287,28 @@ export const AIChat = (function(){
*/ */
const send = (item, event) => { 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(); 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; return false;
}; };