From 8bd5f46c3a9f8ace89970fefcbc32b4571a433eb Mon Sep 17 00:00:00 2001 From: KyMAN <0kyman0@gmail.com> Date: Mon, 1 Jun 2026 08:02:00 +0200 Subject: [PATCH] #wip(py): Building AI Sessions and PseudoLoRAs interaction --- JSON/AnP.pseudoloras.json | 4 + JSON/AnP.routes.json | 4 +- JSON/AnP.settings.json | 23 ++++ MD/AnP.Settings.md | 13 ++ MD/AnP.md | 9 ++ Python/Abstracts/AIInterpretersAbstract.py | 33 ++++- Python/Application/AnP.py | 24 +++- Python/Controllers/AIController.py | 23 +++- Python/Drivers/OllamaDriver.py | 84 +++++++++++- Python/Interfaces/Application/AnPInterface.py | 8 ++ .../AIInterpretersManagerInterface.py | 29 ++++ .../Managers/PseudoLoRAsManagerInterface.py | 20 +++ .../Managers/SessionsManagerInterface.py | 20 +++ .../Managers/UniqueKeysManagerInterface.py | 19 +++ Python/Managers/AIInterpretersManager.py | 87 ++++++++++-- Python/Managers/ControllersManager.py | 1 + Python/Managers/PseudoLoRAsManager.py | 126 +++++++++++++++++- Python/Managers/RoutesManager.py | 13 +- Python/Managers/SessionsManager.py | 39 ++++++ Python/Managers/UniqueKeysManager.py | 44 ++++++ Python/Models/AIPoolRequestsModel.py | 92 +++++++++++++ Python/Models/AIResponseModel.py | 32 +++++ Python/Models/PseudoLoRAModel.py | 10 +- Python/Models/SessionModel.py | 10 ++ Python/Utils/Checks.py | 4 + Python/Utils/Common.py | 28 +++- Python/run.py | 4 +- 27 files changed, 754 insertions(+), 49 deletions(-) create mode 100644 JSON/AnP.pseudoloras.json create mode 100644 MD/AnP.Settings.md create mode 100644 MD/AnP.md create mode 100644 Python/Interfaces/Managers/AIInterpretersManagerInterface.py create mode 100644 Python/Interfaces/Managers/PseudoLoRAsManagerInterface.py create mode 100644 Python/Interfaces/Managers/SessionsManagerInterface.py create mode 100644 Python/Interfaces/Managers/UniqueKeysManagerInterface.py create mode 100644 Python/Managers/SessionsManager.py create mode 100644 Python/Managers/UniqueKeysManager.py create mode 100644 Python/Models/AIPoolRequestsModel.py create mode 100644 Python/Models/AIResponseModel.py create mode 100644 Python/Models/SessionModel.py diff --git a/JSON/AnP.pseudoloras.json b/JSON/AnP.pseudoloras.json new file mode 100644 index 0000000..8452fb3 --- /dev/null +++ b/JSON/AnP.pseudoloras.json @@ -0,0 +1,4 @@ +[ + ["Qué es AnP", "/MD/PseudoLoRAs/AnP.md", [], false], + ["Gestión de configuración", "/MD/AnP.Settings.md", [], false] +] \ No newline at end of file diff --git a/JSON/AnP.routes.json b/JSON/AnP.routes.json index 29e5a8b..91769dd 100644 --- a/JSON/AnP.routes.json +++ b/JSON/AnP.routes.json @@ -1,4 +1,4 @@ [ - "[anp]get:/ /Public", - "[anp]post:/ai/new_message ai@new_message" + "[anp]get:/ai/test test@ai", + "[anp]get:/ /Public" ] \ No newline at end of file diff --git a/JSON/AnP.settings.json b/JSON/AnP.settings.json index 5e458a7..286af31 100644 --- a/JSON/AnP.settings.json +++ b/JSON/AnP.settings.json @@ -78,6 +78,29 @@ }, "AnP_HTTPServersManager_end" : null, + "AnP_PseudoLoRAsManager_start" : null, + "default_pseudoloras_files" : "/JSON/AnP.pseudoloras.json", + "AnP_PseudoLoRAsManager_end" : null, + + "AnP_AIInterpretersManager_start" : null, + "default_ai_interpreters" : { + "anp_titles" : { + "type" : "OllamaDriver", + "host" : "localhost", + "port" : 11434, + "model" : "gemma3:1b", + "pool" : "anp" + }, + "anp_responses" : { + "type" : "OllamaDriver", + "host" : "localhost", + "port" : 11434, + "model" : "gemma4:e4b", + "pool" : "anp" + } + }, + "AnP_AIInterpretersManager_end" : null, + "AnP_TitlesManager_start" : null, "default_titles_files" : [ "/JSON/AnP.titles.json", diff --git a/MD/AnP.Settings.md b/MD/AnP.Settings.md new file mode 100644 index 0000000..bb6a07a --- /dev/null +++ b/MD/AnP.Settings.md @@ -0,0 +1,13 @@ +## Gestión de configuración + +Para gestionar las configuraciones de AnP hay que mandarle dichos parámetros por diccionarios, ya sea JSON como un diccionario a nivel de código. Se pueden apilar en Arrays. + +Los parámetros de configuración son los siguientes: + +- **autostart**: Booleano. Determina si se autoinicia o no. +- **end_print_types**: Array de Strings. Determina los tipos de impresión que integra el valor `end` en el `print`. +- **root_projects_paths**: Array de Strings. Determina las rutas absolutas de los proyectos. Con esto se suprime esta parte del Path en la impresión de los ficheros. +- **print_format**: String. Formato de impresión. +- **exception_format**: String. Formato de impresión de la parte que define una excepción. + +A mayores, cada librería, Driver o Gestor entre otros, tiene sus propios parámetros de configuración. \ No newline at end of file diff --git a/MD/AnP.md b/MD/AnP.md new file mode 100644 index 0000000..a8f1696 --- /dev/null +++ b/MD/AnP.md @@ -0,0 +1,9 @@ +## ¿Qué es AnP? + +AnP, de Attach & Play en inglés, es un Framework basado en Gestores y su consumo sobre un sistema de Controladores. + +Está desarrollado en Python para el servidor; y JavaScript para el entorno cliente Web. + +Para crear un objecto AnP simplemente hemos de crearlo sin ningún argumento si queremos dejar todo por defecto o si queremos personalizar algo, podemos hacer uso del argumento `inputs`. + +> Para saber qué puede contener el argumento `inputs` mirar la configuración. \ No newline at end of file diff --git a/Python/Abstracts/AIInterpretersAbstract.py b/Python/Abstracts/AIInterpretersAbstract.py index abb3e78..aa499cc 100644 --- a/Python/Abstracts/AIInterpretersAbstract.py +++ b/Python/Abstracts/AIInterpretersAbstract.py @@ -1,22 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Any, Self, Sequence +from typing import Any, Self, Sequence, Optional, Callable +from abc import ABC, abstractmethod from Interfaces.Application.AnPInterface import AnPInterface +from Models.AIResponseModel import AIResponseModel from Utils.Checks import Check -class AIInterpretersAbstract: +class AIInterpretersAbstract(ABC): - def __init__(self:Self, anp:AnPInterface, inputs:str|dict[str, Any|None]|Sequence[Any|None]) -> None: + def __init__(self:Self, anp:AnPInterface, key:str, inputs:str|dict[str, Any|None]|Sequence[Any|None]) -> None: if Check.is_string(inputs): inputs = {"url" : inputs.strip()} self.anp:AnPInterface = anp + 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.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, 2048) + ), inputs, None) + self.maximum_response_tokens:int = self.anp.settings.get(( + "ai_interpreter_maximum_response_tokens", "ai_maximum_response_tokens", "maximum_response_tokens" + ), inputs, None) + self.format:Any|None = self.anp.settings.get(("ai_interpreter_format", "ai_format", "format"), inputs, None) + self.model:str = self.anp.settings.get(("ai_interpreter_model", "ai_model", "model"), inputs, "gemma3:1b") + self.pool:str = self.anp.settings.get(("ai_interpreter_pool", "ai_pool", "pool"), inputs, self.key) self.sessions:dict[int, list[int]] = {} self.sessions_i:int = 0 @@ -35,8 +46,20 @@ class AIInterpretersAbstract: return id, self.sessions[id] + def save_context(self:Self, id:int, context:list[int]) -> None: + if id in self.sessions: + self.sessions[id] = context + def close_session(self:Self, id:int) -> bool: if id in self.sessions: del self.sessions[id] return True - return False \ No newline at end of file + return False + + @abstractmethod + def request(self:Self, + session:int|None, + message:str, + orders:list[str] = [], + callback:Optional[Callable[[int, AIResponseModel], None]] = None + ) -> tuple[int|None, AIResponseModel]:pass \ No newline at end of file diff --git a/Python/Application/AnP.py b/Python/Application/AnP.py index bbe027b..4e52217 100644 --- a/Python/Application/AnP.py +++ b/Python/Application/AnP.py @@ -10,13 +10,16 @@ from Managers.SettingsManager import SettingsManager from Managers.PrintTypesManager import PrintTypesManager from Managers.TerminalManager import TerminalManager from Managers.ModelsManager import ModelsManager +from Managers.UniqueKeysManager import UniqueKeysManager +from Managers.SessionsManager import SessionsManager from Managers.ControllersManager import ControllersManager from Managers.DispatchesManager import DispatchesManager from Managers.IndexesManager import IndexesManager from Managers.RoutesManager import RoutesManager from Managers.WebSocketServersManager import WebSocketServersManager from Managers.HTTPServersManager import HTTPServersManager -from Drivers.HTTPDriver import HTTPDriver +from Managers.PseudoLoRAsManager import PseudoLoRAsManager +from Managers.AIInterpretersManager import AIInterpretersManager from Utils.Common import Common from Utils.Patterns import RE @@ -43,13 +46,16 @@ class AnP: self.__own_update() self.terminal:TerminalManager = TerminalManager(self) self.models:ModelsManager = ModelsManager(self) + self.unique_keys:UniqueKeysManager = UniqueKeysManager(self) + self.sessions:SessionsManager = SessionsManager(self) self.controllers:ControllersManager = ControllersManager(self) self.dispatches:DispatchesManager = DispatchesManager(self) self.indexes:IndexesManager = IndexesManager(self) self.routes:RoutesManager = RoutesManager(self) self.web_socket_servers:WebSocketServersManager = WebSocketServersManager(self) self.http_servers:HTTPServersManager = HTTPServersManager(self) - # self.http_server:HTTPDriver = HTTPDriver(self) + self.pseudoloras:PseudoLoRAsManager = PseudoLoRAsManager(self) + self.ai_interpreters:AIInterpretersManager = AIInterpretersManager(self) def update(self:Self) -> None: self.settings.update() @@ -58,34 +64,38 @@ class AnP: self.__own_update() self.terminal.update() self.models.update() + self.unique_keys.update() + self.sessions.update() self.controllers.update() self.dispatches.update() self.indexes.update() self.routes.update() self.web_socket_servers.update() self.http_servers.update() - # self.http_server.update() - + self.pseudoloras.update() + self.ai_interpreters.update() + def reset(self:Self) -> None: self.settings.reset() self.i18n.reset() self.print_types.reset() - self.__own_update() self.terminal.reset() self.models.reset() + self.unique_keys.reset() + self.sessions.reset() self.controllers.reset() self.dispatches.reset() self.indexes.reset() self.routes.reset() self.web_socket_servers.reset() self.http_servers.reset() - # self.http_server.reset() + self.ai_interpreters.reset() def close(self:Self) -> None: self.__working = False + self.ai_interpreters.close() self.web_socket_servers.close() self.http_servers.close() - # self.http_server.close() def working(self:Self) -> bool: return self.__working diff --git a/Python/Controllers/AIController.py b/Python/Controllers/AIController.py index 8b70f90..0c2f5f8 100644 --- a/Python/Controllers/AIController.py +++ b/Python/Controllers/AIController.py @@ -1,15 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Self +from typing import Self, Any, Callable from Abstracts.ModelAbstract import ModelAbstract +from Abstracts.ControllerAbstract import ControllerAbstract from Interfaces.Application.AnPInterface import AnPInterface from Models.RequestModel import RequestModel -class AIController(ModelAbstract): +class AIController(ControllerAbstract, ModelAbstract): - def __init__(self:Self, anp:AnPInterface) -> None: - self.anp: AnPInterface = anp + # def __init__(self:Self, anp:AnPInterface) -> None: + # self.anp: AnPInterface = anp - def new_message(self:Self, request:RequestModel) -> None: - pass \ No newline at end of file + # def __temp(self:Self) + + # def __get_data(self:Self, request:RequestModel, callback:Callable[..., Any|None]) -> None: + # self.anp.pseudoloras.get("anp_titles") + + def test(self:Self, request:RequestModel) -> None: + self.anp.ai_interpreters.request("anp_titles", None, request.get("message", "Hola"), lambda id, response: print((id, response.response))) + request.set_response({ + "ok" : True, + "code" : 200, + "message" : "ok" + }) \ No newline at end of file diff --git a/Python/Drivers/OllamaDriver.py b/Python/Drivers/OllamaDriver.py index 52bb654..88004c4 100644 --- a/Python/Drivers/OllamaDriver.py +++ b/Python/Drivers/OllamaDriver.py @@ -1,14 +1,90 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Any, Optional, Self, Sequence +from typing import Any, Optional, Self, Sequence, Callable +from requests import post as Post, Response +# from pydantic import BaseModel from Interfaces.Application.AnPInterface import AnPInterface -from requests import post as Post +from Abstracts.AIInterpretersAbstract import AIInterpretersAbstract +from Abstracts.ModelAbstract import ModelAbstract +from Models.AIResponseModel import AIResponseModel +from Utils.Checks import Check +from Utils.Common import Common -class OllamaDriver: +class OllamaDriver(AIInterpretersAbstract, ModelAbstract): def __init__(self:Self, anp:AnPInterface, + key:str, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None ) -> None: - self.anp:AnPInterface = anp \ No newline at end of file + super().__init__(anp, key, inputs) + + def request(self:Self, + session:int|None, + message:str, + orders:list[str] = [], + callback:Optional[Callable[[int, AIResponseModel], None]] = None + ) -> tuple[int|None, AIResponseModel]: + + response:Response + context:list[int] + options:dict[str, Any] = {} + results:AIResponseModel = AIResponseModel() + + if self.maximum_response_tokens is not None: + options["num_predict"] = self.maximum_response_tokens + + 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) + + 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 {}), + "stream": self.stream, + **( + {"format" : self.format} if ( + self.format == "json" or + # Check.is_array(self.format) or + Check.is_dictionary(self.format) + ) else + # {"format" : self.format.model_json_schema()} if isinstance(self.format, BaseModel) else + {}), + **({"context" : context} if len(context) else {}), + **({"options" : options} if len(options) else {}) + }, stream = self.stream) as response: + results.http_code = response.status_code + if results.http_code == 200: + results.ok = True + try: + + chunk:bytes + + for chunk in response.iter_lines(): + if chunk: + print(Common.json_decode(chunk)) + results.update(Common.json_decode(chunk)) + if results.done: + self.save_context(session, results.context) + break + + Common.execute(callback, session, results) + + except Exception as exception: + self.anp.exception(exception, "anp_ollama_driver_request") + results.ok = False + else: + try: + results.http_message = response.json().get("error", "unknown_error") + except Exception as _: + results.http_message = "unknown_error" + + Common.execute(callback, session, results) + + return session, results \ No newline at end of file diff --git a/Python/Interfaces/Application/AnPInterface.py b/Python/Interfaces/Application/AnPInterface.py index 5e3ba54..6114d07 100644 --- a/Python/Interfaces/Application/AnPInterface.py +++ b/Python/Interfaces/Application/AnPInterface.py @@ -8,12 +8,16 @@ from Interfaces.Managers.I18NManagerInterface import I18NManagerInterface from Interfaces.Managers.PrintTypesManagerInterface import PrintTypesManagerInterface from Interfaces.Managers.TerminalManagerInterface import TerminalManagerInterface from Interfaces.Managers.ModelsManagerInterface import ModelsManagerInterface +from Interfaces.Managers.UniqueKeysManagerInterface import UniqueKeysManagerInterface +from Interfaces.Managers.SessionsManagerInterface import SessionsManagerInterface from Interfaces.Managers.ControllersManagerInterface import ControllersManagerInterface from Interfaces.Managers.DispatchesManagerInterface import DispatchesManagerInterface from Interfaces.Managers.IndexesManagerInterface import IndexesManagerInterface from Interfaces.Managers.RoutesManagerInterface import RoutesManagerInterface from Interfaces.Managers.WebSocketServersManagerInterface import WebSocketServersManagerInterface from Interfaces.Managers.HTTPServersManagerInterface import HTTPServersManagerInterface +from Interfaces.Managers.PseudoLoRAsManagerInterface import PseudoLoRAsManagerInterface +from Interfaces.Managers.AIInterpretersManagerInterface import AIInterpretersManagerInterface class AnPInterface(ABC): @@ -23,12 +27,16 @@ class AnPInterface(ABC): self.print_types:PrintTypesManagerInterface = None self.terminal:TerminalManagerInterface = None self.models:ModelsManagerInterface = None + self.unique_keys:UniqueKeysManagerInterface = None + self.sessions:SessionsManagerInterface = None self.controllers:ControllersManagerInterface = None self.dispatches:DispatchesManagerInterface = None self.indexes:IndexesManagerInterface = None self.routes:RoutesManagerInterface = None self.web_socket_servers:WebSocketServersManagerInterface = None self.http_servers:HTTPServersManagerInterface = None + self.pseudoloras:PseudoLoRAsManagerInterface = None + self.ai_interpreters:AIInterpretersManagerInterface = None @abstractmethod def update(self:Self) -> None:pass diff --git a/Python/Interfaces/Managers/AIInterpretersManagerInterface.py b/Python/Interfaces/Managers/AIInterpretersManagerInterface.py new file mode 100644 index 0000000..4098c5e --- /dev/null +++ b/Python/Interfaces/Managers/AIInterpretersManagerInterface.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Callable, Self, Optional +from abc import ABC, abstractmethod +from Models.AIResponseModel import AIResponseModel + +class AIInterpretersManagerInterface(ABC): + + @abstractmethod + def update(self:Self) -> None:pass + + @abstractmethod + def reset(self:Self) -> None:pass + + @abstractmethod + def close(self:Self) -> None:pass + + @abstractmethod + def add(self:Self, inputs:Any|None, overwrite:bool = False) -> None:pass + + @abstractmethod + def remove(self:Self, keys:Optional[str|list[str]] = None) -> None:pass + + @abstractmethod + def request(self:Self, key:str, session:int|None, message:str, callback:Callable[[int, AIResponseModel], None]) -> int|None:pass + + @abstractmethod + def cancel_request(self:Self, key:str, id:int) -> None:pass \ No newline at end of file diff --git a/Python/Interfaces/Managers/PseudoLoRAsManagerInterface.py b/Python/Interfaces/Managers/PseudoLoRAsManagerInterface.py new file mode 100644 index 0000000..904d316 --- /dev/null +++ b/Python/Interfaces/Managers/PseudoLoRAsManagerInterface.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self, Any, Callable +from abc import ABC, abstractmethod +from Models.PseudoLoRAModel import PseudoLoRAModel + +class PseudoLoRAsManagerInterface(ABC): + + @abstractmethod + def update(self:Self) -> None:pass + + @abstractmethod + def add(self:Self, inputs:Any|None) -> None:pass + + @abstractmethod + def clean_cache(self:Self) -> None:pass + + @abstractmethod + def get(self:Self, callback:Callable[[list[PseudoLoRAModel]], bool]) -> list[tuple[str, str]]:pass \ No newline at end of file diff --git a/Python/Interfaces/Managers/SessionsManagerInterface.py b/Python/Interfaces/Managers/SessionsManagerInterface.py new file mode 100644 index 0000000..8585682 --- /dev/null +++ b/Python/Interfaces/Managers/SessionsManagerInterface.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self, Optional +from abc import ABC, abstractmethod +from Models.SessionModel import SessionModel + +class SessionsManagerInterface(ABC): + + @abstractmethod + def update(self:Self) -> None:pass + + @abstractmethod + def reset(self:Self) -> None:pass + + @abstractmethod + def get(self:Self, id:Optional[str] = None) -> SessionModel|None:pass + + @abstractmethod + def remove(self:Self, id:str) -> bool:pass \ No newline at end of file diff --git a/Python/Interfaces/Managers/UniqueKeysManagerInterface.py b/Python/Interfaces/Managers/UniqueKeysManagerInterface.py new file mode 100644 index 0000000..e5d32eb --- /dev/null +++ b/Python/Interfaces/Managers/UniqueKeysManagerInterface.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self +from abc import ABC, abstractmethod + +class UniqueKeysManagerInterface(ABC): + + @abstractmethod + def update(self:Self) -> None:pass + + @abstractmethod + def reset(self:Self) -> None:pass + + @abstractmethod + def get(self:Self) -> str:pass + + @abstractmethod + def remove(self:Self, key:str) -> None:pass \ No newline at end of file diff --git a/Python/Managers/AIInterpretersManager.py b/Python/Managers/AIInterpretersManager.py index 1437288..cfea818 100644 --- a/Python/Managers/AIInterpretersManager.py +++ b/Python/Managers/AIInterpretersManager.py @@ -1,25 +1,40 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Any, Self, Optional +from typing import Any, Callable, Self, Optional from Interfaces.Application.AnPInterface import AnPInterface +from Abstracts.AIInterpretersAbstract import AIInterpretersAbstract +from Models.AIResponseModel import AIResponseModel +from Models.AIPoolRequestsModel import AIPoolRequestsModel from Utils.Common import Common class AIInterpretersManager: def __init__(self:Self, anp:AnPInterface) -> None: + self.anp:AnPInterface = anp - self.__interpreters:dict[str, Any] = {} + self.__interpreters:dict[str, AIInterpretersAbstract] = {} + self.__pool_requests:dict[str, AIPoolRequestsModel] = {} + + self.update() def update(self:Self) -> None: - pass + + key:str + + for key in ("default_ai_interpreters_files", "ai_interpreters_files", "default_ai_interpreters", "ai_interpreters"): + self.add(self.anp.settings.get(key), True) def reset(self:Self) -> None: self.__interpreters = {} self.update() def close(self:Self) -> None: - self.__interpreters = {} + + key:str + + for key in list(self.__interpreters.keys()): + self.remove(key) def add(self:Self, inputs:Any|None, overwrite:bool = False) -> None: @@ -32,12 +47,64 @@ class AIInterpretersManager: for key, value in subinputs.items(): if overwrite or key not in self.__interpreters: - self.__interpreters[key] = value - def remove(self:Self, names:Optional[str|list[str]] = None) -> None: + interpreter:AIInterpretersAbstract|None = None - name:str + if isinstance(value, AIInterpretersAbstract): + interpreter = value + elif isinstance(value, dict) and "type" in value: - for name in Common.get_keys(names) if names else list(self.__interpreters.keys()): - if name in self.__interpreters: - del self.__interpreters[name] \ No newline at end of file + Model:AIInterpretersAbstract|None = self.anp.models.get(AIInterpretersAbstract, value["type"]) + + if Model is not None: + interpreter = Model(self.anp, key, value) + + if interpreter is not None: + if key in self.__interpreters: + self.remove(key) + self.__interpreters[key] = interpreter + self.__pool_requests[interpreter.pool] = AIPoolRequestsModel() + + def remove(self:Self, keys:Optional[str|list[str]] = None) -> None: + + key:str + + for key in Common.get_keys(keys) if keys else list(self.__interpreters.keys()): + if key in self.__interpreters: + + pool:str = self.__interpreters[key].pool + has_interpreters:bool = False + interpreter:AIInterpretersAbstract + + self.__pool_requests.get(pool).remove(self.__interpreters[key].key) + self.__interpreters[key].close() + del self.__interpreters[key] + + for interpreter in self.__interpreters.values(): + if interpreter.pool == pool: + has_interpreters = True + break + + if not has_interpreters and pool in self.__pool_requests: + del self.__pool_requests[pool] + + def request(self:Self, key:str, session:int|None, message:str, callback:Callable[[int, AIResponseModel], None]) -> int|None: + + i:int|None = None + + if key in self.__interpreters: + + pool:str = self.__interpreters[key].pool + + i = self.__pool_requests[pool].add(self.__interpreters[key], session, message, callback) + self.__pool_requests[pool].execute() + + return i + + def cancel_request(self:Self, key:str, id:int) -> None: + if key in self.__interpreters: + + pool:str = self.__interpreters[key].pool + + if id in self.__pool_requests[pool].pool: + del self.__pool_requests[pool].pool[id] \ No newline at end of file diff --git a/Python/Managers/ControllersManager.py b/Python/Managers/ControllersManager.py index f07cd34..9061383 100644 --- a/Python/Managers/ControllersManager.py +++ b/Python/Managers/ControllersManager.py @@ -58,6 +58,7 @@ class ControllersManager: None) def execute(self:Self, key:str, method:str, request:RequestModel) -> bool: + print([self.__controllers, key, method]) if key in self.__controllers and hasattr(self.__controllers[key], method): getattr(self.__controllers[key], method)(request) return True diff --git a/Python/Managers/PseudoLoRAsManager.py b/Python/Managers/PseudoLoRAsManager.py index 3276dab..8d6c2e3 100644 --- a/Python/Managers/PseudoLoRAsManager.py +++ b/Python/Managers/PseudoLoRAsManager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Self, Any, Sequence +from typing import Self, Any, Sequence, Callable from Interfaces.Application.AnPInterface import AnPInterface from Models.PseudoLoRAModel import PseudoLoRAModel from Utils.Common import Common @@ -10,10 +10,37 @@ from Utils.Checks import Check class PseudoLoRAsManager: def __init__(self:Self, anp:AnPInterface) -> None: + self.anp:AnPInterface = anp + self.__maximum_cache:int = (1 << 20) * 100 self.__memory_cached:int = 0 + self.__cache:dict[int, PseudoLoRAModel] = {} self.__loras:list[PseudoLoRAModel] = [] + self.update() + + def update(self:Self) -> None: + + key:str + + self.clean_cache() + self.__loras.clear() + self.__maximum_cache = self.anp.settings.get(( + "pseudoloras_maximum_cache_size", "maximum_cache_size" + ), None, self.__maximum_cache) + + for key in ("default_pseudoloras_files", "pseudoloras_files", "default_pseudoloras", "pseudoloras"): + self.add(self.anp.settings.get(key)) + + def __try_load_file(self:Self, path:str) -> bool: + + data:list[dict[str, Any|None]|Sequence[Any|None]] = Common.load_json(path) + + if len(data): + self.add(data) + return True + return False + def add(self:Self, inputs:Any|None) -> None: if isinstance(inputs, PseudoLoRAModel): self.__loras.append(inputs) @@ -22,5 +49,98 @@ class PseudoLoRAsManager: subinputs:dict[str, Any|None]|Sequence[Any|None] for subinputs in Common.load_json(inputs, False): - if Check.is_array(inputs): - pass \ No newline at end of file + if Check.is_string(subinputs): + self.__try_load_file(subinputs) + elif Check.is_array(subinputs): + if all(Check.is_string(item) for item in subinputs): + + item:str + ok:bool = True + + for item in subinputs: + if not self.__try_load_file(item): + ok = False + break + + if not ok: + self.__loras.append(PseudoLoRAModel(*subinputs)) + + elif ( + len(subinputs) >= 2 and len(subinputs) <= 4 and + Check.is_string(subinputs[0]) and Check.is_string(subinputs[1]) and + (len(subinputs) < 3 or subinputs[2] is None or (Check.is_array(subinputs[2]) and all(Check.is_string(item) for item in subinputs[2]))) and + (len(subinputs) < 4 or subinputs[3] is None or Check.is_boolean(subinputs[3])) + ): + self.__loras.append(PseudoLoRAModel(*subinputs)) + else: + + item:Any|None + + for item in subinputs: + self.add(item) + + def clean_cache(self:Self) -> None: + + lora:PseudoLoRAModel + + self.__memory_cached = 0 + self.__cache.clear() + + for lora in self.__loras: + lora.clean_cache() + + def get(self:Self, callback:Callable[[list[PseudoLoRAModel]], bool], keys:list[str] = []) -> list[tuple[str, str]]: + + next:list[PseudoLoRAModel] = [] + results:list[tuple[str, str]] = [] + i:int + ok:bool + has_keys:bool = len(keys) > 0 + + for i, ok in enumerate(callback( + lora.title for lora in self.__loras if not has_keys or any( + key in lora.keys for key in keys + ) + )): + if not ok: + continue + + lora:PseudoLoRAModel = self.__loras[i] + + if lora.path is not None: + + data:str|None + + if lora.cacheable: + if not lora.cache: + + lora.cache = Common.load_file(lora.path, "r") + lora.memory = len(lora.cache) + self.__memory_cached += lora.memory + + if self.__cache[lora.i]: + del self.__cache[lora.i] + lora.i += 1 + if lora.i in self.__cache: + self.__cache[lora.i].append(lora) + else: + self.__cache[lora.i] = [lora] + + if self.__memory_cached > self.__maximum_cache: + + i:int = min(self.__cache.keys()) + + for lora in self.__cache[i]: + lora.clean_cache() + del self.__cache[i] + + if (data := lora.cache or Common.load_file(lora.path, "r")): + results.append((lora.title, lora.path)) + + else: + next.extend(lora.nested) + + if len(next): + results.extend(callback(next)) + + return results \ No newline at end of file diff --git a/Python/Managers/RoutesManager.py b/Python/Managers/RoutesManager.py index f2698d0..b2550ad 100644 --- a/Python/Managers/RoutesManager.py +++ b/Python/Managers/RoutesManager.py @@ -57,13 +57,12 @@ class RoutesManager: "code" : 404, "message" : "not_found" }) - elif route.controller and route.controller_method: - if not self.anp.controllers.execute(route.controller, route.controller_method, request): - request.set_response({ - "ok" : False, - "code" : 505, - "message" : "not_implemented" - }) + elif route.controller and route.action: + self.anp.controllers.execute(route.controller, route.action, request) or request.set_response({ + "ok" : False, + "code" : 505, + "message" : "not_implemented" + }) else: request.set_response({ "ok" : True, diff --git a/Python/Managers/SessionsManager.py b/Python/Managers/SessionsManager.py new file mode 100644 index 0000000..4dddafe --- /dev/null +++ b/Python/Managers/SessionsManager.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self, Optional +from Interfaces.Application.AnPInterface import AnPInterface +from Models.SessionModel import SessionModel + +class SessionsManager: + + def __init__(self:Self, anp:AnPInterface) -> None: + + self.anp:AnPInterface = anp + self.__sessions:dict[str, SessionModel] = {} + + self.update() + + def update(self:Self) -> None:pass + + def reset(self:Self) -> None: + + self.__sessions = {} + + self.update() + + def get(self:Self, id:Optional[str] = None) -> SessionModel|None: + if id is None: + + session:SessionModel = SessionModel(self.anp.unique_keys.get()) + + self.__sessions[session.id] = session + + return session + return self.__sessions.get(id, None) + + def remove(self:Self, id:str) -> bool: + if id in self.__sessions: + del self.__sessions[id] + return True + return False \ No newline at end of file diff --git a/Python/Managers/UniqueKeysManager.py b/Python/Managers/UniqueKeysManager.py new file mode 100644 index 0000000..9961e9f --- /dev/null +++ b/Python/Managers/UniqueKeysManager.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self, Any +from Interfaces.Application.AnPInterface import AnPInterface +from Utils.Common import Common + +class UniqueKeysManager: + + def __init__(self:Self, anp:AnPInterface) -> None: + self.anp:AnPInterface = anp + self.__keys:list[str] = [] + self.__alphabet:list[str] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + self.__length:int = 13 + + self.update() + + def update(self:Self) -> None: + self.__alphabet = Common.unique(self.anp.settings.get(("unique_keys_alphabet", "alphabet"), None, self.__alphabet)) + self.__length = self.anp.settings.get(("unique_keys_length", "length"), None, self.__length) + + def reset(self:Self) -> None: + + self.__keys = [] + self.__alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + self.__length = 13 + + self.update() + + def get(self:Self) -> str: + + key:str + + while True: + key = "" + while len(key) < self.__length: + key += Common.random(self.__alphabet) + if key[0] not in "0123456789" and key not in self.__keys: + self.__keys.append(key) + return key + + def remove(self:Self, key:str) -> None: + if key in self.__keys: + self.__keys.remove(key) \ No newline at end of file diff --git a/Python/Models/AIPoolRequestsModel.py b/Python/Models/AIPoolRequestsModel.py new file mode 100644 index 0000000..ee503b9 --- /dev/null +++ b/Python/Models/AIPoolRequestsModel.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Callable, Self +from threading import Thread, Lock +from Models.AIResponseModel import AIResponseModel +from Abstracts.AIInterpretersAbstract import AIInterpretersAbstract +from Utils.Common import Common + +class AIPoolRequestsItemsModel: + def __init__(self:Self, + key:str, + interpreter:AIInterpretersAbstract, + session:int|None, + message:str, + callback:Callable[[int, AIResponseModel], None], + orders:list[str] = [] + ) -> None: + self.key:str = key + self.interpreter:AIInterpretersAbstract = interpreter + self.session:int|None = session + self.message:str = message + self.callback:Callable[[int, AIResponseModel], None] = callback + self.orders:list[str] = orders + +class AIPoolRequestsModel: + + def __init__(self:Self) -> None: + self.pool:dict[int, AIPoolRequestsItemsModel] = {} + self.iterations:int = 0 + self.maximum_iterations:int = 1 + self.i:int = 0 + self.j:int = 0 + self.__lock:Lock = Lock() + + def add(self:Self, + interpreter:AIInterpretersAbstract, + session:int|None, + message:str, + callback:Callable[[int, AIResponseModel], None], + orders:list[str] = [] + ) -> int: + + id:int + + with self.__lock: + self.i += 1 + id = self.i + self.pool[self.i] = AIPoolRequestsItemsModel( + interpreter.key, interpreter, session, message, callback, orders + ) + + self.execute() + + return id + + def __next(self:Self, callback:Callable[[int, AIResponseModel], None], session:int, response:AIResponseModel) -> None: + Common.execute(callback, session, response) + if response.done or not response.ok: + with self.__lock: + self.iterations -= 1 + self.execute() + + def __execute(self:Self) -> None: + + item:AIPoolRequestsItemsModel|None = None + + with self.__lock: + if len(self.pool) and self.iterations != self.maximum_iterations and self.i != self.j: + self.iterations += 1 + self.j = min(self.pool.keys()) + item:AIPoolRequestsItemsModel = self.pool[self.j] + del self.pool[self.j] + + item and item.interpreter.request( + item.session, item.message, item.orders, lambda session, response: self.__next(item.callback, session, response) + ) + + def execute(self:Self) -> None: + Thread(target = self.__execute).start() + + def cancel(self:Self, ids:int|list[int]) -> None: + with self.__lock: + for id in Common.get_keys(ids): + if id in self.pool: + del self.pool[id] + + def remove(self:Self, key:str) -> None: + with self.__lock: + for id, item in list(self.pool.items()): + if item.key == key: + del self.pool[id] \ No newline at end of file diff --git a/Python/Models/AIResponseModel.py b/Python/Models/AIResponseModel.py new file mode 100644 index 0000000..afe6ed7 --- /dev/null +++ b/Python/Models/AIResponseModel.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self +from time import time as timestamp + +class AIResponseModel: + + def __init__(self:Self) -> None: + self.start:float = timestamp() + self.model:str = "" + self.response:str = "" + self.total:str = "" + self.context:str|list[str] = "" + self.done:bool = False + self.end:float|None = None + self.chunks:int = 0 + self.ok:bool = False + self.http_code:int = 0 + self.http_message:str = "" + + def update(self:Self, data:dict[str, Any|None]) -> None: + + self.model = data.get("model", self.model) + self.response = data.get("response", "") + self.total += self.response + self.done = data.get("done", self.done) + self.chunks += 1 + + if self.done: + self.end = timestamp() + self.context = data.get("context", self.context) \ No newline at end of file diff --git a/Python/Models/PseudoLoRAModel.py b/Python/Models/PseudoLoRAModel.py index de0f2ed..50164e5 100644 --- a/Python/Models/PseudoLoRAModel.py +++ b/Python/Models/PseudoLoRAModel.py @@ -6,9 +6,10 @@ from Utils.Checks import Check from Utils.Common import Common class PseudoLoRAModel: + def __init__(self:Self, title:str, - content:str|dict[str, str|Sequence[str, str|Sequence[Any], Optional[str|Sequence[str]], bool]], + content:str|Sequence[Any|None], keys:Optional[str|Sequence[str]] = None, cacheable:bool = False ) -> None: @@ -21,4 +22,9 @@ class PseudoLoRAModel: self.cacheable:bool = cacheable self.nested:list[PseudoLoRAModel] = [ PseudoLoRAModel(subtitle, subcontent, subkeys, subcacheable) for subtitle, subcontent, subkeys, subcacheable in content - ] if Check.is_array(content) else [] \ No newline at end of file + ] if Check.is_array(content) else [] + + def clean_cache(self:Self) -> None: + self.cache = "" + self.memory = 0 + self.i = 0 \ No newline at end of file diff --git a/Python/Models/SessionModel.py b/Python/Models/SessionModel.py new file mode 100644 index 0000000..b55429b --- /dev/null +++ b/Python/Models/SessionModel.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Self, Any + +class SessionModel: + + def __init__(self:Self, id:str) -> None: + self.id:str = id + self.variables:dict[str, Any|None] = {} \ No newline at end of file diff --git a/Python/Utils/Checks.py b/Python/Utils/Checks.py index 0f80265..cf53880 100644 --- a/Python/Utils/Checks.py +++ b/Python/Utils/Checks.py @@ -23,6 +23,10 @@ class Check: def is_dictionary(item:Any|None) -> bool: return isinstance(item, dict) + @staticmethod + def is_boolean(item:Any|None) -> bool: + return isinstance(item, bool) + @staticmethod def is_function(item:Any|None) -> bool: return callable(item) diff --git a/Python/Utils/Common.py b/Python/Utils/Common.py index 5489a00..d845b86 100644 --- a/Python/Utils/Common.py +++ b/Python/Utils/Common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Any, Optional, Sequence, Self +from typing import Any, Callable, Optional, Sequence, Self from re import Match as REMatch from os.path import abspath as absolute_path, dirname as directory_name, exists as path_exists, isfile as is_file from json import loads as json_decode @@ -10,6 +10,7 @@ from mimetypes import guess_type as get_mime_by_extension from inspect import FrameInfo, stack as get_stack from json import dumps as json_encode, loads as json_decode from base64 import b64encode as base64_encode, b64decode as base64_decode +from random import choice as random_choice from Utils.Checks import Check from Utils.Patterns import RE @@ -325,4 +326,27 @@ class Common: return cls.json_decode(data) except Exception as exception: return data - return None \ No newline at end of file + return None + + @staticmethod + def execute(callback:Callable[..., Any|None], *arguments:Any|None) -> Any|None: + if Check.is_function(callback): + try: + return callback(*arguments) + except Exception as _: + pass + return None + + @staticmethod + def unique(items:str|Sequence[Any|None]) -> str|list[Any|None]: + if Check.is_string(items): + return "".join(character for i, character in enumerate(items) if character not in items[:i]) + elif Check.is_array(items): + return [item for i, item in enumerate(items) if item not in items[:i]] + return items + + @staticmethod + def random(items:str|Sequence[Any|None]) -> Any|None: + return random_choice(items) if ( + Check.is_string(items) or Check.is_array(items) + ) and len(items) else None \ No newline at end of file diff --git a/Python/run.py b/Python/run.py index 40f98e4..9ac71f1 100644 --- a/Python/run.py +++ b/Python/run.py @@ -6,12 +6,14 @@ from Application.AnP import AnP from Controllers.AIController import AIController from Drivers.WebSocketServerDriver import WebSocketServerDriver from Drivers.HTTPDriver import HTTPDriver +from Drivers.OllamaDriver import OllamaDriver inputs:dict[str, dict[str, Any|None]] = { "default_models" : { "AIController" : AIController, "WebSocketServerDriver" : WebSocketServerDriver, - "HTTPDriver" : HTTPDriver + "HTTPDriver" : HTTPDriver, + "OllamaDriver" : OllamaDriver } }