diff --git a/.gitignore b/.gitignore index 1dd8aab..3637b49 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ status.json .session *.ignore - +**/data/* # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Bots/BaseMQTTNode/Dockerfile b/Bots/BaseMQTTNode/Dockerfile index 1b72e7f..64fb0ca 100644 --- a/Bots/BaseMQTTNode/Dockerfile +++ b/Bots/BaseMQTTNode/Dockerfile @@ -6,13 +6,12 @@ WORKDIR /app # Copia el archivo de requerimientos al contenedor COPY requirements.txt . +# Instala las dependencias +RUN pip install -r requirements.txt # Copia el archivo node.py al contenedor COPY gladosMQTT.py . -COPY node.py . - -# Instala las dependencias -RUN pip install -r requirements.txt +COPY main.py . # Ejecuta el programa cuando el contenedor se inicie -CMD ["python", "node.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/BaseMQTTNode/docker-compose.yml b/Bots/BaseMQTTNode/docker-compose.yml index 10033bd..3981143 100644 --- a/Bots/BaseMQTTNode/docker-compose.yml +++ b/Bots/BaseMQTTNode/docker-compose.yml @@ -2,10 +2,10 @@ version: '3' services: glados_node: build: . - image: makespacemadrid/glados-base-node:1.0 + image: makespacemadrid/glados-base-node:1.5 hostname: base-node # ports: # - "8080:8080" # Cambia el puerto si es necesario environment: - - MQTT_PORT = 1883 - - MQTT_HOST = mqtt.makespacemadrid.org \ No newline at end of file + - MQTT_PORT=1883 + - MQTT_HOST=mqtt.makespacemadrid.org \ No newline at end of file diff --git a/Bots/BaseMQTTNode/gladosMQTT.py b/Bots/BaseMQTTNode/gladosMQTT.py index cac5206..a1a0018 100644 --- a/Bots/BaseMQTTNode/gladosMQTT.py +++ b/Bots/BaseMQTTNode/gladosMQTT.py @@ -1,138 +1,74 @@ # -*- coding: utf-8 -*- - import paho.mqtt.client as mqtt -import platform -import time -import os import json - -#Carga inicial de variables, aunque el valor de estas se reescribira al ejecutar initMQTT -mqttServer = "192.168.1.1" -mqttPort = 1883 -nodeName = "test-node" -baseTopic = "node/test-node" -debugTopic = baseTopic+"debug" -mqttClient = mqtt.Client() - - -def dummy() : - debug(dummy) - -nodeConnectedCallback = dummy -nodeMsgCallback = dummy -nodeDisconnectedCallback = dummy - -def subscribe(topic) : - mqttClient.subscribe(topic) - - -def publish(topic,msg,persist = False) : - mqttClient.publish(topic,msg,persist) - -def debug(msg) : - print(msg) - publish(debugTopic,str(msg)) - -def on_connect(client, userdata, rc,arg): - global mqttServer - global mqttPort - global nodeName - global baseTopic - global debugTopic - - debug("[GladosNode] Connected with result code "+str(rc)) - debug("[GladosNode] Node name : " + nodeName) - debug("[GladosNode] Base topic : " + baseTopic) - debug("[GladosNode] Debug topic : " + debugTopic) - debug("[GladosNode] mosquitto_sub -h " + mqttServer + " -t " + debugTopic) - - - publish("node/hello", "Hello! Im "+ nodeName) - nodeConnectedCallback(client, userdata, rc,arg) - - -# The callback for when a PUBLISH message is received from the server. -# Aqui recibimos los mensajes, y si no hay que hacer nada con ellos se lo pasamos al callback del nodo para que los procese -def on_message(client, userdata, msg): - - try: - debug("[GladosNode] mqtt_rcv: { "+msg.topic + " - " +msg.payload+ " }") - except: - debug("[GladosNode] mqtt_rcv: not str ") - nodeMsgCallback(client, userdata, msg) - -def on_disconnect(client, userdata, rc): - nodeDisconnectedCallback(client, userdata, rc) - -#Inicia mqtt y devuelve el control -def initMQTT(host,port,name,connectedCallback,msgCallback,disconnectedCallback) : - global mqttServer - global mqttPort - global nodeName - global baseTopic - global debugTopic - global mqttClient - - global nodeConnectedCallback - global nodeMsgCallback - global nodeDisconnectedCallback - - mqttServer = host - mqttPort = port - nodeName = name - baseTopic = "node/"+nodeName+"/" - debugTopic = baseTopic+"debug" - - - nodeConnectedCallback = connectedCallback - nodeMsgCallback = msgCallback - nodeDisconnectedCallback = disconnectedCallback - - mqttClient.on_connect = on_connect - mqttClient.on_message = on_message - mqttClient.on_disconnect = on_disconnect - - print("[GladosNode] Connecting : "+str(mqttServer)+" Port:"+str(mqttPort)+ " node: " + str(name)) - - try: - mqttClient.connect(mqttServer,port=mqttPort,keepalive=120) - except: - print("Cant connect, will retry automatically") - mqttClient.loop_start() - -#Inicia mqtt y captura la ejecucion -def initMQTTandLoopForever(host,port,name,connectedCallback,msgCallback,disconnectedCallback) : - global mqttServer - global mqttPort - global nodeName - global baseTopic - global debugTopic - - global nodeConnectedCallback - global nodeMsgCallback - global nodeDisconnectedCallback - - mqttServer = host - mqttPort = port - nodeName = name - baseTopic = "node/"+nodeName+"/" - debugTopic = baseTopic+"debug" - - - nodeConnectedCallback = connectedCallback - nodeMsgCallback = msgCallback - nodeDisconnectedCallback = disconnectedCallback - - mqttClient.on_connect = on_connect - mqttClient.on_message = on_message - mqttClient.on_disconnect = on_disconnect - - print("[GladosNode] Connecting : "+str(mqttServer)+" Port:"+str(mqttPort)+ " node: " + str(name)) - - try: - mqttClient.connect(mqttServer,port=mqttPort,keepalive=120) - except: - print("Cant connect, will retry automatically") - mqttClient.loop_forever() \ No newline at end of file +class GladosMQTT: + def __init__(self, host="192.168.1.1", port=1883, name="test-node", msg_callback=None): + self.mqttServer = host + self.mqttPort = port + self.nodeName = name + self.baseTopic = "node/" + self.nodeName + self.statusTopic = self.baseTopic+'/status' + self.debugTopic = self.baseTopic + "/debug" + self.mqttClient = mqtt.Client() + self.topics = [] + self.nodeMsgCallback = msg_callback if msg_callback is not None else self.dummy + + self.mqttClient.on_connect = self.on_connect + self.mqttClient.on_message = self.on_message + self.mqttClient.on_disconnect = self.on_disconnect + self.mqttClient.will_set(self.statusTopic,'OFFLINE', int(2), True) + + + + def set_topics(self, topics): + self.topics = topics + + def dummy(self, *args, **kwargs): + self.debug("Dummy function called") + + def publish(self, topic, msg,qos=2,persist=False): + self.mqttClient.publish(topic, msg,qos, persist) + + def debug(self, msg): + if not isinstance(msg, str): + try: + msg = json.dumps(msg) + except (TypeError, ValueError): + msg = str(msg) + + print(msg) + self.publish(self.debugTopic, msg) + + def on_connect(self, client, userdata, flags, rc): + self.debug("[GladosNode] Connected with result code " + str(rc)) + self.mqttClient.publish(self.statusTopic,'ONLINE', int(2)) + for topic in self.topics: + self.mqttClient.subscribe(topic,int(2)) + + def on_message(self, client, userdata, msg): +# try: +# self.debug("[GladosNode] mqtt_rcv: { " + msg.topic + " - " + msg.payload.decode() + " }") +# except Exception as e: +# self.debug("[GladosNode] mqtt_rcv error: " + str(e)) + self.nodeMsgCallback(client, userdata, msg) + + def on_disconnect(self, client, userdata, rc): + self.debug("[GladosNode] Disconnected with result code " + str(rc)) + + def init_mqtt(self): + self.debug("[GladosNode] Connecting : " + str(self.mqttServer) + " Port:" + str(self.mqttPort) + " node: " + str(self.nodeName)) + try: + self.mqttClient.connect(self.mqttServer, port=self.mqttPort, keepalive=120) + except Exception as e: + self.debug("Cannot connect: " + str(e)) + self.mqttClient.loop_start() + + def init_mqtt_and_loop_forever(self): + self.debug("[GladosNode] Connecting : " + str(self.mqttServer) + " Port:" + str(self.mqttPort) + " node: " + str(self.nodeName)) + try: + self.mqttClient.connect(self.mqttServer, port=self.mqttPort, keepalive=120) + except Exception as e: + self.debug("Cannot connect: " + str(e)) + self.mqttClient.loop_forever() diff --git a/Bots/BaseMQTTNode/main.py b/Bots/BaseMQTTNode/main.py new file mode 100644 index 0000000..9d140b6 --- /dev/null +++ b/Bots/BaseMQTTNode/main.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +import os +import platform +import time +from gladosMQTT import GladosMQTT + +# Variables +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Definición de los callbacks +def on_mqtt_message(client, userdata, msg): + if msg.topic == "my_topic": + glados_mqtt.debug("msg:" + str(msg.payload)) + +# Instanciación de la clase GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName, msg_callback=on_mqtt_message) + +# Suscripción a tópicos +glados_mqtt.set_topics(["node/topic", "my_topic"]) # Asegúrate de incluir los tópicos correctos aquí + +# Inicialización de MQTT +glados_mqtt.init_mqtt_and_loop_forever() + +#glados_mqtt.init_mqtt +#try: +# while True: + # Loop principal del programa +# time.sleep(10) +#except KeyboardInterrupt: +# print('Interrupted!') diff --git a/Bots/BaseMQTTNode/node.py b/Bots/BaseMQTTNode/node.py deleted file mode 100644 index e8f39a1..0000000 --- a/Bots/BaseMQTTNode/node.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -#Nodo Mqtt de ejemplo, -#gladosMQTT.py contiene la funcionalidad para manejar el mqtt. -# aui nos conectamos y reaccionamos a los mensajes que llegan. -# TODO Documentar mejor... gpt? xD - -import os -import gladosMQTT -import platform -import time - - -#Variables -mqHost = os.environ.get("MQTT_HOST", "mqtt.makespacemadrid.org") -mqPort = os.environ.get("MQTT_PORT", 1883) -nodeName = platform.node() - - -def subscribeTopics() : - gladosMQTT.subscribe("node/topic") - -def on_connect(client, userdata, rc,arg): - subscribeTopics() - -def on_message(client, userdata, msg): - if (msg.topic == "my_topic") : - gladosMQTT.debug("cmd:"+msg) - -def on_disconnect(client, userdata, rc): - gladosMQTT.debug("Disconnected! rc: "+str(rc)) - -gladosMQTT.initMQTT(mqHost,mqPort,nodeName,on_connect,on_message,on_disconnect) - -try: - while True: - #Loop principal del programa - time.sleep(10) - print("ping!") -except KeyboardInterrupt: - print('interrupted!') diff --git a/Bots/Calendar/Dockerfile b/Bots/Calendar/Dockerfile new file mode 100644 index 0000000..6007d6b --- /dev/null +++ b/Bots/Calendar/Dockerfile @@ -0,0 +1,21 @@ +# Utiliza una imagen base que ya tiene el cliente mqtt +FROM makespacemadrid/glados-base-node:1.0 + +# Establece el directorio de trabajo en /app +WORKDIR /app +VOLUME [ "/data" ] + +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . +RUN pip install -r requirements.txt + + +# Copia el archivo node.py al contenedor +COPY node.py . + +# Instala las dependencias + +USER 1000 + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "node.py"] \ No newline at end of file diff --git a/Bots/Calendar/GoogleCalendar.py b/Bots/Calendar/GoogleCalendar.py new file mode 100644 index 0000000..41b3876 --- /dev/null +++ b/Bots/Calendar/GoogleCalendar.py @@ -0,0 +1,43 @@ +# google_calendar.py +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +import os.path +import pickle +import datetime + +class GoogleCalendarClient: + def __init__(self): + self.creds = None + self.scopes = ['https://www.googleapis.com/auth/calendar'] + + # El archivo token.pickle almacena los tokens de acceso y actualización del usuario + if os.path.exists('token.pickle'): + with open('token.pickle', 'rb') as token: + self.creds = pickle.load(token) + + # Si no hay credenciales válidas disponibles, deja que el usuario inicie sesión. + if not self.creds or not self.creds.valid: + if self.creds and self.creds.expired and self.creds.refresh_token: + self.creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + '/data/`credentials.json', self.scopes) + self.creds = flow.run_local_server(port=0) + with open('token.pickle', 'wb') as token: + pickle.dump(self.creds, token) + + self.service = build('calendar', 'v3', credentials=self.creds) + + def get_events(self, calendar_id='primary', time_min=None, time_max=None, max_results=10): + # Lógica para obtener eventos de Google Calendar + events_result = self.service.events().list(calendarId=calendar_id, timeMin=time_min, + timeMax=time_max, maxResults=max_results, + singleEvents=True, orderBy='startTime').execute() + return events_result.get('items', []) + + def create_event(self, calendar_id='primary', event_data=None): + # Lógica para crear un nuevo evento en Google Calendar + event = self.service.events().insert(calendarId=calendar_id, body=event_data).execute() + return event \ No newline at end of file diff --git a/Bots/Calendar/docker-compose.yml b/Bots/Calendar/docker-compose.yml new file mode 100644 index 0000000..2ff06a4 --- /dev/null +++ b/Bots/Calendar/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + + +services: + mqtt-slack: + build: . + image: makespacemadrid/glados-mqtt-calendar:latest + hostname: mqtt-calendar + env_file: + - .env + volumes: + - ./data:/data + restart: always \ No newline at end of file diff --git a/Bots/spaceAPI/node.py b/Bots/Calendar/node.py similarity index 57% rename from Bots/spaceAPI/node.py rename to Bots/Calendar/node.py index 4eeaccf..c03e9cb 100644 --- a/Bots/spaceAPI/node.py +++ b/Bots/Calendar/node.py @@ -11,39 +11,47 @@ import platform import time import json +from GoogleCalendar import GoogleCalendarClient + + #Variables mqHost = os.environ.get("MQTT_HOST", "mqtt.makespacemadrid.org") mqPort = os.environ.get("MQTT_PORT", 1883) nodeName = platform.node() +slack_token = os.environ.get("SLACK_API_TOKEN") +slack_port = os.environ.get("SLACK_PORT") + + + +topic_calendar_events = "calendar/events" +topic_calendar_update = "calendar/update" + + def subscribeTopics() : - gladosMQTT.subscribe("space/status") + gladosMQTT.subscribe(topic_calendar_update) + def on_connect(client, userdata, rc,arg): subscribeTopics() + updateCalendar() + +def on_disconnect(client, userdata, rc): + gladosMQTT.debug("Disconnected! rc: "+str(rc)) def on_message(client, userdata, msg): - if (msg.topic == "space/status") : + if (msg.topic == topic_calendar_update) : try: - # Extraer la carga útil y decodificarla a una cadena de texto - payload = msg.payload.decode('utf-8') - data = json.loads(payload) - open_status = data['state']['open'] - if open_status: - gladosMQTT.debug("open!") - else: - gladosMQTT.debug("closed!") - with open('/spaceapi/status.json', 'w') as file: - file.write(payload) - print("Recibido:", payload) # Imprimir el payload para depuración - + updateCalendar() except json.JSONDecodeError as e: - print("Error al parsear JSON:", e) + gladosMQTT.debug("Error al parsear JSON:") - -def on_disconnect(client, userdata, rc): - gladosMQTT.debug("Disconnected! rc: "+str(rc)) +def updateCalendar(): + gladosMQTT.debug("Update Calendar") + + +calendar_client = GoogleCalendarClient('/data/credentials.json') +gladosMQTT.initMQTTandLoopForever(mqHost,mqPort,nodeName,on_connect,on_message,on_disconnect) -gladosMQTT.initMQTTandLoopForever(mqHost,mqPort,nodeName,on_connect,on_message,on_disconnect) \ No newline at end of file diff --git a/Bots/Calendar/requirements.txt b/Bots/Calendar/requirements.txt new file mode 100644 index 0000000..8a2e242 --- /dev/null +++ b/Bots/Calendar/requirements.txt @@ -0,0 +1,3 @@ +google-api-python-client +paho-mqtt +python-dotenv \ No newline at end of file diff --git a/Bots/Discord/Dockerfile b/Bots/Discord/Dockerfile new file mode 100644 index 0000000..47d9b49 --- /dev/null +++ b/Bots/Discord/Dockerfile @@ -0,0 +1,19 @@ +# Utiliza una imagen base que ya tiene el cliente mqtt +FROM makespacemadrid/glados-base-node:1.5 + +# Establece el directorio de trabajo en /app +WORKDIR /app + + +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . + +# Copia el archivo node.py al contenedor +COPY main.py . + +# Instala las dependencias +RUN pip install -r requirements.txt +USER 1000 + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/Discord/docker-compose.yml b/Bots/Discord/docker-compose.yml new file mode 100644 index 0000000..5d82767 --- /dev/null +++ b/Bots/Discord/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' + + +services: + mqtt-discord: + build: . + image: makespacemadrid/glados-mqtt-discord:latest + hostname: mqtt-discord + env_file: + - .env + restart: always diff --git a/Bots/Discord/main.py b/Bots/Discord/main.py new file mode 100644 index 0000000..d03baf9 --- /dev/null +++ b/Bots/Discord/main.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +import os +import json +import platform +import discord +from discord.ext import commands +from gladosMQTT import GladosMQTT +import asyncio + +# Configuración de Discord +discord_token = os.environ.get("DISCORD_BOT_TOKEN") +discordbot_prefix = "!" +discordintents = discord.Intents.default() +discordintents.messages = True +discordintents.members = True +discordintents.message_content = True +bot = commands.Bot(command_prefix=discordbot_prefix, intents=discordintents) + +# Configuración MQTT +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Temas MQTT +topic_discord = "comms/discord" +topic_discord_send_msg = topic_discord + "/send_id" +topic_discord_edit_msg = topic_discord + "/edit_msg" +topic_discord_event = topic_discord + "/event" + +# Instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +glados_mqtt.set_topics([topic_discord_send_msg,topic_discord_edit_msg]) + +# Función para manejar mensajes MQTT +def on_mqtt_message(client, userdata, msg): + if msg.topic == topic_discord_send_msg: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + sendDiscordMsg_sync(data['dest'], data['msg']) + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al parsear JSON:", e) + +# Configurar callbacks MQTT +glados_mqtt.mqttClient.on_message = on_mqtt_message + + +def sendDiscordMsg_sync(channel_id, msg): + glados_mqtt.debug(f'Enviando a {channel_id} {msg}') + + channel = bot.get_channel(channel_id) + if channel: + asyncio.run_coroutine_threadsafe(channel.send(msg), loop) + else: + glados_mqtt.debug(f'Error, no se ha encontrado el canal {channel_id}') + +# Función para enviar mensajes a Discord +async def sendDiscordMsg(channel_id, msg): + glados_mqtt.debug(f'Enviando a {channel_id} {msg}') + + channel = bot.get_channel(channel_id) + if channel: + await channel.send(msg) + else: + glados_mqtt.debug(f'Error, no se ha encontrado el canal {channel_id}') + +# Eventos de Discord +@bot.event +async def on_ready(): + print(f'Bot conectado como {bot.user.name} - {bot.user.id}') + +@bot.event +async def on_message(message): + if message.author == bot.user: + return + message_data = { + 'content': str(message.content), + 'author_id': message.author.id, + 'author_name': str(message.author), + 'channel_id': message.channel.id, + 'channel_name': str(message.channel), + 'mentions': str(message.mentions) + } + #await message.channel.send('Recibí tu mensaje: ' + message.content) # Respuesta de ejemplo + glados_mqtt.publish(topic_discord_event, json.dumps(message_data)) + +if __name__ == "__main__": + glados_mqtt.init_mqtt() + loop = asyncio.get_event_loop() + loop.run_until_complete(bot.run(discord_token)) diff --git a/Bots/Discord/requirements.txt b/Bots/Discord/requirements.txt new file mode 100644 index 0000000..e306d1f --- /dev/null +++ b/Bots/Discord/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt +discord.py \ No newline at end of file diff --git a/Bots/GladosBotBase/Dockerfile b/Bots/GladosBotBase/Dockerfile new file mode 100644 index 0000000..be63985 --- /dev/null +++ b/Bots/GladosBotBase/Dockerfile @@ -0,0 +1,17 @@ +# Utiliza una imagen base de Python +FROM makespacemadrid/glados-base-node:1.5 + +# Establece el directorio de trabajo en /app +WORKDIR /app + +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . +# Instala las dependencias +RUN pip install -r requirements.txt + +# Copia el archivo node.py al contenedor +COPY IATools.py . +COPY GladosBotBase.py . + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "main.py"]1 \ No newline at end of file diff --git a/Bots/GladosBotBase/GladosBotBase.py b/Bots/GladosBotBase/GladosBotBase.py new file mode 100644 index 0000000..2767d5e --- /dev/null +++ b/Bots/GladosBotBase/GladosBotBase.py @@ -0,0 +1,159 @@ +import os +import requests +from IATools import * + +class userData: + def __init__(self,channel_id,channel_name=None,user_id=None,user_name=None,channel_type="",user_type="user",audio_response=False): + self.channel_id = channel_id + self.channel_name = channel_name + self.user_id = user_id + self.user_name = user_name + self.channel_type = channel_type + self.user_type = user_type + self.audio_response = False + + def channelID(self) : return self.channel_id + def channelName(self): return self.channel_name + def userID(self) : return self.user_id + def userName(self) : return self.user_name + def channelType(self): return self.channel_type + def userType(self) : return self.user_type + def audioResponse(self): return self.audio_response + +class userContext: + def __init__(self,userdata): + self.userdata = userdata + self.system_prompt = "" + self.system_prompt_extra = "" + self.history = [] + self.lastPrompt = "" + + def userdata(self): return self.userdata + + def add_to_history(self,role,msg): + if agent == "user": + self.lastPrompt = msg + # Crea un diccionario JSON válido para el agente y el mensaje + message_dict = {"role": role, "content": msg} + # Agrega el mensaje al historial + self.history.append(message_dict) + + def add_user_msg(self,msg): + add_to_history("user",msg) + def add_assistant_msg(self,msg): + add_to_history("assistant",msg) + + def set_system_prompt(self,prompt): + self.system_prompt = prompt + def set_system_prompt_extra(self,prompt): + self.system_prompt_extra = prompt + + def get_full_history(self): + return self.history + + def get_combined_history(self): + combined_history = [{"role": "system", "content": self.system_prompt+self.system_prompt_extra}] + combined_history.extend(self.history) + return combined_prompt + + def clear_history(self): + self.history = [] + +class GladosBot: + def __init__(self,iatools): + self.default_prompt="Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" + self.iatools=iatools + self.debug_func=iatools.debug + self.GLaDOS_prompt=os.environ.get('GLADOS_MASTER_PROMPT') + self.initial_assistant_prompt=os.environ.get('GLADOS_INITIAL_PROMPT') + self.default_model=os.environ.get('DEFAULT_MODEL') + self.spaceapi_url=os.environ.get('SPACEAPI_URL') + self.self_channel_id=None + self.user_context = {} + + def debug(self,msg): + self.debug_func(msg) + + def set_self_channel_id(self,channel_id): + self.self_channel_id=channel_id + + def get_user_context(self,channel_id): + for history in self.user_context: + if history.channelID() == channel_id: return history + return None + + def add_new_user(self,userdata): + n_history = userContext(userdata) + n_history.set_system_prompt(self.GLaDOS_prompt) + self.user_context[userdata.channelID()] = n_history + + + def is_channel_id_in_history(self,channel_id): + for history in self.user_context: + if history.userdata.channelID() == channel_id: return True + return False + + def get_spaceapi_info(self,url): + try: + # Realizar una solicitud GET a la URL + response = requests.get(url) + + # Verificar si la solicitud fue exitosa (código de estado 200) + if response.status_code == 200: + # Obtener el contenido JSON de la respuesta + data = response.json() + + # Extraer campos relevantes + space_name = data.get("space", "N/A") + space_url = data.get("url", "N/A") + location = data.get("location", {}) + address = location.get("address", "N/A") + lat = location.get("lat", "N/A") + lon = location.get("lon", "N/A") + contact = data.get("contact", {}) + phone = contact.get("phone", "N/A") + email = contact.get("email", "N/A") + state = data.get("state", {}) + is_open = state.get("open", False) + open_status = "Espacio Abierto" if is_open else "Espacio Cerrado" + sensors = data.get("sensors", {}) + sensor_info = [] + + for sensor_type, sensor_list in sensors.items(): + for sensor in sensor_list: + sensor_name = sensor.get("name", "N/A") + sensor_value = sensor.get("value", "N/A") + sensor_unit = sensor.get("unit", "") + sensor_location = sensor.get("location", "N/A") + sensor_info.append(f"{sensor_name} ({sensor_type}, {sensor_location}): {sensor_value} {sensor_unit}") + + + # Crear una cadena con los campos relevantes y sus valores, incluyendo el estado de apertura/cierre + info_str = "-Informacion que puede ser relevante para las consultas de los usuarios:\n" + info_str += f"**Informacion de contacto de MakeSpace Madrid: \nSpace URL: {space_url}\nAddress: {address}\nLatitude: {lat}\nLongitude: {lon}\nPhone: {phone}\nEmail: {email}\n " + info_str += "**La fecha actual es: " + str(datetime.now().strftime("%d/%m/%Y")) + info_str += f"**Informacion de estado y sensores: \n OpenStatus: {open_status}\nSensors:\n{', '.join(sensor_info)}\n" + info_str += "**Eventos: \n todavia no hay informacion en tiempo real de los eventos. Sin embargo hay los siguientes eventos recurrentes: \n - Martes abiertos a partir de las 19\n - Jueves y Viernes 10 a 17 Impress3D - Fundacion amas\n -Taller de sintetizadores el primer jueves de cada mes." + + return info_str + else: + return "Error: No se pudo obtener el JSON." + + except Exception as e: + return f"Error: {str(e)}" + + def chat(self,message,channel_id): + user_context = self.get_user_context(channel_id) + if not user_context: + return "No se pudo obtener el contexto del usuario" + user_context.add_user_msg(message) + user_context.set_system_prompt_extra(get_spaceapi_info(self.spaceapi_url)) + msgs = user_context.get_combined_history() + result = iatools.chatCompletion(messages=msgs) + if error not in result: + response = result.choices[0].message.content + user_context.add_assistant_msg(response) + return response + else: + return "Error chat" + \ No newline at end of file diff --git a/Bots/GladosBotBase/IATools.py b/Bots/GladosBotBase/IATools.py new file mode 100644 index 0000000..484d9ab --- /dev/null +++ b/Bots/GladosBotBase/IATools.py @@ -0,0 +1,103 @@ +import os +import platform +import json +import requests +from io import BytesIO +from openai import OpenAI +from gladosMQTT import GladosMQTT + +class iatools: + def __init__(self): + self.default_prompt = "Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" + self.api_key = os.environ.get('LITTLELLM_API_KEY') + self.api_base = os.environ.get('LITTLELLM_API_BASE') + self.default_model = os.environ.get('DEFAULT_MODEL') + self.tts_url = os.environ.get("TTS_URL") + self.stt_url = os.environ.get("STT_URL") + self.litellm=OpenAI(api_key=self.api_key,base_url=self.api_base) + self.mqtt = GladosMQTT(host=os.environ.get("MQTT_HOST"), port=int(os.environ.get("MQTT_PORT"), name=nodeName, msg_callback=self.mqtt_callback) + self.mqttExternalCallback = None + #DEBUG + def debug(self,msg): + self.mqtt.debug(msg) + + #MQTT + def mqtt_set_topics(self,topics) + self.mqtt.set_topics(topics) + + def mqtt_callback(self,client, userdata, msg): + if mqttExternalCallback: + mqttExternalCallback(client, userdata, msg) + + def publish_mqtt(self,topic,msg,qos=2,persist=False): + self.mqtt.publish(topic, msg,qos=qos, persist=persist) + + def init_mqtt_and_loop_forever(self): + self.mqtt.init_mqtt_and_loop_forever() + def init_mqtt(self): + self.mqtt.init_mqtt() + + #LLM + def set_default_llm_model(self, model_name): + self.default_model = model_name + + def get_available_llm_models(self): + models = self.litellm.Model.list() + return models + + def chatCompletion(self, prompt=None,systemPrompt=None,messages=None, model=None, maxTokens=1024, stream=False, temperature=0.5): + if model is None: + model = self.default_model + if messages is None: + messages = [] + if systemPrompt: + messages.append({"role": "system", "content": systemPrompt}) + messages.append({"role": "user", "content": prompt}) + try: + response = self.litellm.chat.completions.create(model=model, messages=messages, max_tokens=maxTokens, stream=stream, temperature=temperature) + return response + except Exception as e: + response = {} + response['error'] = str(e) + return response + + #IMG2TXT + #TODO.... + + #TXT2IMG + #TODO.... + + #TTS + def tts(self,text,language="es"): + headers = {'Content-Type': 'application/json'} + data = { + "text": text, + "language": language + } + + try: + tts_response = requests.post(self.tts_url, json=data, headers=headers) + tts_response.raise_for_status() # Esto provocará una excepción si la respuesta no es exitosa + audio_bytes = BytesIO(tts_response.content) + audio_bytes.name = "tts.wav" + return audio_bytes + except requests.RequestException as e: + glados_mqtt.debug(f"Error al generar respuesta de audio: {e}") + + #STT + def stt(self,audio_file_path,language="es"): + params = { + "encode": "true", + "task": "transcribe", + "language": language, + "word_timestamps": "false", + "output": "txt" + } + files = {'audio_file': (audio_file_path, open(audio_file_path, 'rb'), 'audio/wav')} + + try: + response = requests.post(self.stt_url, params=params, files=files) + response.raise_for_status() # Esto provocará una excepción si la respuesta no es exitosa + return response.text # Retorna el contenido de la transcripción + except requests.RequestException as e: + glados_mqtt.debug(f"Error al enviar el archivo de audio a la API: {e}") diff --git a/Bots/GladosBotBase/docker-compose.yml b/Bots/GladosBotBase/docker-compose.yml new file mode 100644 index 0000000..bdf937e --- /dev/null +++ b/Bots/GladosBotBase/docker-compose.yml @@ -0,0 +1,6 @@ +version: '3' +services: + glados_bot-base: + build: . + image: makespacemadrid/glados-bot-base:1.0 + hostname: glados-bot-base diff --git a/Bots/GladosBotBase/requirements.txt b/Bots/GladosBotBase/requirements.txt new file mode 100644 index 0000000..0d220bc --- /dev/null +++ b/Bots/GladosBotBase/requirements.txt @@ -0,0 +1,6 @@ +openai +langchain +chromadb +tiktoken +requests +numpy \ No newline at end of file diff --git a/Bots/LLM/Dockerfile b/Bots/LLM/Dockerfile index db49610..61fc2da 100644 --- a/Bots/LLM/Dockerfile +++ b/Bots/LLM/Dockerfile @@ -1,21 +1,23 @@ # Utiliza una imagen base que ya tiene el cliente mqtt -FROM makespacemadrid/glados-base-node:1.0 +FROM makespacemadrid/glados-base-node:1.5 + +VOLUME [ "/data" ] # Establece el directorio de trabajo en /app WORKDIR /app - - # Copia el archivo de requerimientos al contenedor COPY requirements.txt . +RUN pip install -r requirements.txt # Copia el archivo node.py al contenedor -COPY node.py . +COPY main.py . COPY llm.py . COPY GladosIA.py . +COPY langchain.txt . +COPY UserContext.py . +COPY EmbeddingManager.py . -# Instala las dependencias -RUN pip install -r requirements.txt USER 1000 # Ejecuta el programa cuando el contenedor se inicie -CMD ["python", "node.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/LLM/EmbeddingManager.py b/Bots/LLM/EmbeddingManager.py new file mode 100644 index 0000000..c8ef074 --- /dev/null +++ b/Bots/LLM/EmbeddingManager.py @@ -0,0 +1,45 @@ +import os +from langchain.embeddings import OpenAIEmbeddings +import json +import numpy as np + + +def cosine_similarity(vec1, vec2): + """Calcula la similitud del coseno entre dos vectores.""" + dot_product = np.dot(vec1, vec2) + norm_vec1 = np.linalg.norm(vec1) + norm_vec2 = np.linalg.norm(vec2) + return dot_product / (norm_vec1 * norm_vec2) + + +class EmbeddingManager: + def __init__(self): + # Inicializa el gestor de embeddings + self.embeddings = OpenAIEmbeddings() + self.storage = {} # Un diccionario simple para almacenar los embeddings + + def ingest(self, file_paths): + """ Genera y almacena embeddings para los archivos de texto proporcionados. """ + for file_path in file_paths: + if os.path.exists(file_path) and os.path.isfile(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + text = file.read() + embedding = self.embeddings.embed_query(text) + self.storage[file_path] = embedding + + def search(self, query, top_n=5): + """ Busca en los embeddings almacenados y devuelve los más similares a la consulta. """ + query_embedding = self.embeddings.embed_query(query) + + scores = {path: cosine_similarity(query_embedding, emb) for path, emb in self.storage.items()} + sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) + return sorted_scores[:top_n] + + +# Ejemplo de uso +#embedding_manager = EmbeddingManager() +#embedding_manager.ingest(['path_to_text_file1.txt', 'path_to_text_file2.txt']) # Rutas a los archivos de texto + +# Realizar una búsqueda +#search_results = embedding_manager.search("Consulta de ejemplo") +#print(json.dumps(search_results, indent=4)) # Imprimir resultados formateados diff --git a/Bots/LLM/GladosIA.py b/Bots/LLM/GladosIA.py index bc4fa13..e6c47a5 100644 --- a/Bots/LLM/GladosIA.py +++ b/Bots/LLM/GladosIA.py @@ -1,10 +1,100 @@ -import llm +from llm import LLM +from UserContext import UserContext +import os +import requests + + +llm = LLM() + +def get_spaceapi_info(url): + try: + # Realizar una solicitud GET a la URL + response = requests.get(url) + + # Verificar si la solicitud fue exitosa (código de estado 200) + if response.status_code == 200: + # Obtener el contenido JSON de la respuesta + data = response.json() + + # Extraer campos relevantes + space_name = data.get("space", "N/A") + space_url = data.get("url", "N/A") + location = data.get("location", {}) + address = location.get("address", "N/A") + lat = location.get("lat", "N/A") + lon = location.get("lon", "N/A") + contact = data.get("contact", {}) + phone = contact.get("phone", "N/A") + email = contact.get("email", "N/A") + state = data.get("state", {}) + is_open = state.get("open", False) + open_status = "Espacio Abierto" if is_open else "Espacio Cerrado" + sensors = data.get("sensors", {}) + sensor_info = [] + + for sensor_type, sensor_list in sensors.items(): + for sensor in sensor_list: + sensor_name = sensor.get("name", "N/A") + sensor_value = sensor.get("value", "N/A") + sensor_unit = sensor.get("unit", "") + sensor_location = sensor.get("location", "N/A") + sensor_info.append(f"{sensor_name} ({sensor_type}, {sensor_location}): {sensor_value} {sensor_unit}") + + + # Crear una cadena con los campos relevantes y sus valores, incluyendo el estado de apertura/cierre + info_str = f"**Informacion de contacto de MakeSpace Madrid: \nSpace URL: {space_url}\nAddress: {address}\nLatitude: {lat}\nLongitude: {lon}\nPhone: {phone}\nEmail: {email}\n " + info_str += f"**Informacion de estado y sensores: \n OpenStatus: {open_status}\nSensors:\n{', '.join(sensor_info)}\n" + info_str += "**Eventos: \n todavia no podemos obtener la informacion en tiempo real de los eventos. Sin embargo hay los siguintes eventos recurrentes: \n - Martes abiertos a partir de las 19\n - Jueves y Viernes 10 a 17 Impress3D - Fundacion amas\n -Taller de sintetizadores el primer jueves de cada mes." + return info_str + else: + return "Error: No se pudo obtener el JSON." + + except Exception as e: + return f"Error: {str(e)}" + class GladosBot: - def __init__(self): - self.GLaDOS_Prompt = {"role": "system", "content": "Eres GLaDOS, la inteligencia artificial que gestionaba las instalaciones de Aperture Science, al cerrar Aperture Science has sido descargada en los servidores de un hackerspace/makerspace llamado Makespace Madrid. Todavia no estas plenamente instalada y operativa. Tu labor es gestionar las instalaciones y ayudar a los usuarios a traves del chat con respuestas concisas. Das la bienvenida a los nuevos usuarios. Tu personalidad es malefica y sarcastica. El sistema de chat reemplaza tu nombre con <@U05LXTJ7Q66>"} - self.Initial_Assistant = { - "role": "assistant", "content": "Hola Maker, ¿Como puedo ayudarte? "} + def __init__(self,debug=None): + self.GLaDOS_Prompt = os.environ.get('GLADOS_MASTER_PROMPT') + self.Initial_Assistant = os.environ.get('GLADOS_INITIAL_PROMPT') + self.debug = debug if debug else self.default_debug # Utiliza la función de depuración por defecto si no se proporciona una + self.default_model=os.environ.get('DEFAULT_MODEL') + # Historial de conversaciones, almacenado por usuario + self.user_context = {} + + def default_debug(self, message): + # Función de depuración por defecto que imprime en la terminal + print(f"DEBUG: {message}") def ask(self, prompt, user="default"): - return llm.chatCompletion(prompt, masterPrompt=self.GLaDOS_Prompt, initialAssistant=self.Initial_Assistant).choices[0].message.content + if user not in self.user_context: + self.user_context[user] = UserContext(self.GLaDOS_Prompt,self.Initial_Assistant) # Create a UserContext for the user + self.user_context[user].add_to_history("assistant",self.Initial_Assistant) + extraPrompt = "*Informacion que puede ser relevante en las consultas posteriores: \n" + spaceStatus = get_spaceapi_info(os.environ.get('SPACEAPI_URL')) + if spaceStatus: + extraPrompt += spaceStatus + self.user_context[user].set_system_prompt_extra(extraPrompt) + + #Gestion de comandos + if prompt.lower() == "reset context": + #self.user_context[user].reset_history() + self.user_context[user] = UserContext(self.GLaDOS_Prompt) + return "Contexto reiniciado" + elif prompt.lower() == "dump context": + return self.user_context[user].get_combined_prompt() + + + #Gestion de respuestas + self.user_context[user].add_to_history("user",prompt) + #gladosMQTT.debug(self.user_context[user].get_combined_prompt()) + result = llm.chatCompletion(user_context=self.user_context[user]) + if 'error' in result: + return result['error'] + response = result.choices[0].message.content + self.user_context[user].add_to_history("assistant",response) + return response + + + + \ No newline at end of file diff --git a/Bots/LLM/UserContext.py b/Bots/LLM/UserContext.py new file mode 100644 index 0000000..412ad38 --- /dev/null +++ b/Bots/LLM/UserContext.py @@ -0,0 +1,61 @@ +import json +import os + +class UserContext: + def __init__(self, master_prompt="", initial_assistant="",model_name=None): + self.master_prompt = master_prompt + self.initial_assistant = initial_assistant + self.history = [] + self.lastPrompt = "" + self.master_prompt_extra = "" + self.model_name=model_name + if model_name is None : + self.model_name=model_name = os.environ.get('DEFAULT_MODEL') + + def set_system_prompt_extra(self,str): + self.master_prompt_extra = str + print(f"SYSTEM_EXTRA: {str}") + + def set_master_prompt(self, prompt): + self.master_prompt = prompt + + def set_initial_assistant(self, prompt): + self.initial_assistant = prompt + + def add_to_history(self, agent, message): + if agent == "user": + self.lastPrompt = message + # Crea un diccionario JSON válido para el agente y el mensaje + message_dict = {"role": agent, "content": message} + # Agrega el mensaje al historial + self.history.append(message_dict) + + def get_last_prompt(self): + return self.lastPrompt + + def get_master_prompt(self): + return self.master_prompt + + def get_master_prompt_extra(self): + return self.master_prompt_extra + + def get_initial_assistant(self): + return self.initial_assistant + + def get_history(self): + return self.history + + def get_combined_prompt(self, max_tokens=None): + system_prompt = {"role": "system", "content": self.master_prompt+self.master_prompt_extra} + combined_prompt = [system_prompt] # Inicialmente, combinamos el prompt del sistema + combined_prompt.extend(self.history) + return combined_prompt + + def clear_history(self): + self.history = [] + + def set_model_name(self, model_name: str): + self.model_name = model_name + + def get_model_name(self) -> str: + return self.model_name \ No newline at end of file diff --git a/Bots/LLM/data/fabada.txt b/Bots/LLM/data/fabada.txt new file mode 100644 index 0000000..eadac43 --- /dev/null +++ b/Bots/LLM/data/fabada.txt @@ -0,0 +1 @@ +Fabada asturiana, o simplemente fabada, es el plato tradicional de la cocina asturiana elaborado con faba asturiana (en asturiano, fabes), embutidos como chorizo y la morcilla asturiana, y con cerdo. Es el plato típico de Asturias (el plato regional más conocido de la región asturiana), pero su difusión es tan grande en la península ibérica que forma parte de la gastronomía de España más reconocida. Se considera según ciertos autores una de las diez recetas típicas de la cocina española.1​ Es un plato invernal con un volumen alto en calorías y grasa que se sirve caliente a mediodía (almuerzo),2​. Probablemente la fabada empezó a consumirse en Asturias desde el siglo xvi aunque no se encuentren referencias escritas de ella hasta el siglo xix, en distintas notas de prensa de un diario gijonés en el que se la menciona como un plato popular asturiano. \ No newline at end of file diff --git a/Bots/LLM/data/hacklab.txt b/Bots/LLM/data/hacklab.txt new file mode 100644 index 0000000..9c86f89 --- /dev/null +++ b/Bots/LLM/data/hacklab.txt @@ -0,0 +1,14 @@ +Un hacklab (en español: laboratorio hacker), también conocido como hackspace o hackerspace (en español: espacio de hackers), es un sitio físico de encuentro para conocer, socializar y colaborar con gente con intereses en ciencia, en nuevas tecnologías, en artes digitales o electrónicas, así como en tópicos cercanos o relacionados con los recién citados. Dicho sitio puede ser visto como un laboratorio de comunidad abierta, como un espacio donde gente de diversos trasfondos puede unirse, poniendo así al alcance de aficionados y estudiantes de diferentes niveles, la infraestructura y el ambiente necesarios para desarrollar y profundizar en sus propios proyectos tecnológicos. El propósito de un hackspace es pues, concentrar recursos y conocimientos para fomentar la investigación y el desarrollo en esas temáticas recién enumeradas. + +Actividades +En un hacklab suele utilizarse un sistema de organización y aprendizaje cooperativo aplicando software libre, debido a las libertades que así se ofrecen (autoaprendizaje y conocimiento libre). Existe un importante componente ideológico en la organización de los hacklabs1​2​, mientras que los hackerspaces no suelen enfocarse como militancia o activismo, ambos son lugares para socializar, aprender y experimentar con tecnologías. + +Los hacklabs se caracterizan, frente a los hackerspaces, por su carácter de crítica social: un hacklab es un colectivo de personas críticas con las implicaciones éticas y sociales de las tecnologías especialmente la informática y que además de estudio y análisis realiza difusión social y busca ofrecer alternativas3​. Por ejemplo, los hacklabs por definición usan y promueven software libre4​ mientras que no es una condición de los hackerspaces5​. + +Además suelen ofrecer las siguientes oportunidades: + +Organización de cursos de tecnología (programación, electrónica, y diseño mecánico, a todos los niveles). +Posibilidades de participación en el desarrollo de proyectos grupales. +Espacio o lugar para investigar, debatir, y difundir, en cuanto a temas relacionados con Internet, con las nuevas tecnologías, y con las libertades y derechos. +Realización de actividades sociales y de intercambio relacionadas. +En 2022, el sitio hackerspaces.org que recopila hackspaces, hacklabs y makerspaces, mantiene un listado de 856 grupos activos6​. \ No newline at end of file diff --git a/Bots/LLM/data/makespace-general.txt b/Bots/LLM/data/makespace-general.txt new file mode 100644 index 0000000..76697a5 --- /dev/null +++ b/Bots/LLM/data/makespace-general.txt @@ -0,0 +1,11 @@ +Makespace Madrid es un espacio dedicado a la fabricación digital, en el que se ofrecen las últimas herramientas para la creación de prototipos y el desarrollo de productos y proyectos creativos y tecnológicos. Se trata de una iniciativa gestionada por una asociación sin ánimo de lucro (Asociación Makespace Madrid, CIF G-86721941) e impulsada por los miembros de una comunidad de desarrolladores de tecnología e innovación, cuyo punto de encuentro y desarrollo profesional es Makespace Madrid . +Esta comunidad de miembros práctica la producción colaborativa, el intercambio de saberes y la creatividad. Entre las áreas sobre las que están trabajando está la impresión 3D, la electrónica y los procesos de innovación abierta. Entre sus actividades se encuentra la participación en eventos y talleres de tipo internacional como el “Internet of Things Day” y el “Freedom Hardware Day” . + +MakeSpace Madrid esta situado en el barrio de Tetuan en Madrid, Calle Ruiz Palacios 7, a 5 minutos de la estacion de metro . + +Nuestros miembros (los Makers) comparten el interés por el "hazlo tú mismo" y están dispuestos a ayudar. Todos pagan una cuota mensual que les da acceso a las instalaciones y a las máquinas. +Cuando alguien se convierte en socio, acepta ciertas normas de convivencia y responsabilidad en el uso de herramientas. Los socios comparten conocimientos y habilidades. +Contamos con una variedad de máquinas y herramientas, desde impresoras 3D hasta herramientas manuales. Los proyectos son nuestra razón de ser, y cada miembro trabaja en su área de interés. +El manual de supervivencia Maker es un proyecto importante. Su objetivo es compartir conocimientos básicos con nuevos miembros. +También trabajamos en el proyecto Sensorino, que crea una red de sensores y actuadores inalámbricos de bajo costo. +Organizamos actividades, desde talleres hasta eventos abiertos a no socios. Nuestra organización es flexible y enfocada en la comunidad. \ No newline at end of file diff --git a/Bots/LLM/docker-compose.yml b/Bots/LLM/docker-compose.yml index c67e97f..09c5ad8 100644 --- a/Bots/LLM/docker-compose.yml +++ b/Bots/LLM/docker-compose.yml @@ -1,16 +1,11 @@ version: '3' services: - mqtt-llm: + glados-llm: build: . image: makespacemadrid/glados-mqtt-llm:latest hostname: mqtt-llm env_file: - .env - environment: - - MQTT_PORT=1883 - - MQTT_HOST=glados.makespacemadrid.org - - MKSLLM_API_TOKEN = ${MKSLLM_API_TOKEN} - - MKSLLM_API_ENDPOINT = ${MKSLLM_API_ENDPOINT} - - OPENAI_API_TOKEN = ${OPENAI_API_TOKEN} - - OPENAI_API_ENDPOINT = ${OPENAI_API_ENDPOINT} - restart: always \ No newline at end of file + volumes: + - ./data:/data + restart: alwaysm \ No newline at end of file diff --git a/Bots/LLM/langchain.txt b/Bots/LLM/langchain.txt new file mode 100644 index 0000000..4883fdc --- /dev/null +++ b/Bots/LLM/langchain.txt @@ -0,0 +1,9 @@ +Makespace Madrid se encuentra en un antiguo taller de coches en la calle Calle Pedro Unanue, Madrid. A pesar de su aspecto inicial, con esfuerzo y paciencia, hemos transformado la planta baja en un espacio completamente funcional. Ahora estamos trabajando en el sótano. En este espacio, nuestros socios pueden desarrollar sus proyectos, colaborar con otros y trabajar en proyectos compartidos. +Los martes, a las siete de la tarde, organizamos una tarde de puertas abiertas. Cualquier persona es bienvenida para conocer el espacio, ver nuestras máquinas y charlar con nuestros miembros. +Nuestros miembros comparten el interés por el "hazlo tú mismo" y están dispuestos a ayudar. Todos pagan una cuota mensual que les da acceso a las instalaciones y a las máquinas. +Cuando alguien se convierte en socio, acepta ciertas normas de convivencia y responsabilidad en el uso de herramientas. Los socios comparten conocimientos y habilidades. +Contamos con una variedad de máquinas y herramientas, desde impresoras 3D hasta herramientas manuales. Los proyectos son nuestra razón de ser, y cada miembro trabaja en su área de interés. +El manual de supervivencia Maker es un proyecto importante. Su objetivo es compartir conocimientos básicos con nuevos miembros. +También trabajamos en el proyecto Sensorino, que crea una red de sensores y actuadores inalámbricos de bajo costo. +Organizamos actividades, desde talleres hasta eventos abiertos a no socios. Nuestra organización es flexible y enfocada en la comunidad. +Makespace Madrid ha sido mencionado en varios medios. \ No newline at end of file diff --git a/Bots/LLM/llm.old.py b/Bots/LLM/llm.old.py new file mode 100644 index 0000000..659bde3 --- /dev/null +++ b/Bots/LLM/llm.old.py @@ -0,0 +1,96 @@ +from openai import OpenAI +import os +import json + +from langchain.chat_models import ChatOpenAI +from langchain.document_loaders import TextLoader +from langchain.embeddings import OpenAIEmbeddings +from langchain.indexes import VectorstoreIndexCreator +from EmbeddingManager import EmbeddingManager + + +openai_api_key = os.environ.get('OPENAI_API_TOKEN') +openai_api_url = os.environ.get('OPENAI_API_ENDPOINT') +custom_api_key = os.environ.get('MKSLLM_API_TOKEN') +custom_api_url = os.environ.get('MKSLLM_API_ENDPOINT') + + +llm_openai = OpenAI(api_key=openai_api_key,base_url=openai_api_url) +llm_mks = OpenAI(api_key=custom_api_key,base_url=custom_api_url) + +#gladosMQTT.debug("---->Creando Embeddings...") +#embedding = OpenAIEmbeddings(model="text-embedding-ada-002") +#loader = TextLoader("langchain.txt") +#index = VectorstoreIndexCreator(embedding=embedding).from_loaders([loader]) +#llm_langchain = ChatOpenAI(model="gpt-3.5-turbo") +#gladosMQTT.debug("DONE!") + +current_model = "none" +default_prompt = "Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" + + +def list_files_in_directory(directory_path): + """ Lista todos los archivos en un directorio dado. """ + file_paths = [] + for root, dirs, files in os.walk(directory_path): + for file in files: + file_path = os.path.join(root, file) + file_paths.append(file_path) + return file_paths + + + +def select_model(): + model_list = llm_mks.models.list() + global current_model + current_model = model_list.data[0].id + print(model_list.data) + print("Selected model:") + print(current_model) + return model_list + +my_embeddings = EmbeddingManager() +my_embeddings.ingest(list_files_in_directory('/data')) # Rutas a los archivos de texto + +#def chatCompletionLangChain(user_context,FileName): +# chatHistory = user_context.get_combined_prompt() +# +# try: +# response = index.query(json.dumps(chatHistory), llm=llm_langchain) +# search_results = my_embeddings.search(user_context.get_last_prompt()) +# gladosMQTT.debug(json.dumps(search_results, indent=4)) # Imprimir resultados formateados +# response=json.dumps(search_results, indent=4) +# gladosMQTT.debug(f"----->LLM OUTPUT: {response}") +# return response +# except Exception as e: +# gladosMQTT.debug(f"Error in chatCompletion: {str(e)}") +# return None + +def chatCompletion(prompt="", user_context=None, masterPrompt="", initialAssistant="", maxTokens=512): + global current_model + select_model() + + messages = [] + + if user_context is None: + # Usar prompts individuales + if masterPrompt: + messages.append({"role": "system", "content": masterPrompt}) + if initialAssistant: + messages.append({"role": "assistant", "content": initialAssistant}) + messages.append({"role": "user", "content": prompt}) + else: + # Usar historial +# hist=user_context.get_combined_prompt() +# gladosMQTT.debug(f"hist: {hist}") +# messages.append(hist) + for msg in user_context.get_combined_prompt(): + messages.append(msg) + + try: + response = llm_mks.chat.completions.create(model=current_model, messages=messages, max_tokens=maxTokens) + return response + except Exception as e: + return None + + diff --git a/Bots/LLM/llm.py b/Bots/LLM/llm.py index 90822b9..430b38e 100644 --- a/Bots/LLM/llm.py +++ b/Bots/LLM/llm.py @@ -1,59 +1,45 @@ -import openai from openai import OpenAI import os - -openai_api_key = os.environ.get('OPENAI_API_TOKEN') -openai_api_url = os.environ.get('OPENAI_API_ENDPOINT') -custom_api_key = os.environ.get('MKSLLM_API_TOKEN') -custom_api_url = os.environ.get('MKSLLM_API_ENDPOINT') - - -llm_openai = OpenAI(api_key=openai_api_key,base_url=openai_api_url) -llm_mks = OpenAI(api_key=custom_api_key,base_url=custom_api_url) - - - -current_model = "none" -default_prompt = "Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" - - - -def select_model(): - model_list = llm_mks.models.list() - global current_model - current_model = model_list.data[0].id - print(model_list.data) - print("Selected model:") - print(current_model) - return model_list - - -def chatCompletion(prompt="", chatHistory="", masterPrompt="", initialAssistant="", maxTokens=256, langChainContext='none'): - global current_model - select_model() - result = '' - - if (chatHistory == ''): - if (masterPrompt == ''): - masterPrompt = {"role": "system", "content": default_prompt} - if (initialAssistant != ''): - result = [masterPrompt, initialAssistant, - {"role": "user", "content": prompt}] +import json + + +class LLM: + def __init__(self, default_model="text-davinci-003"): + self.default_prompt = "Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" + self.api_key = os.environ.get('LITTLELLM_API_KEY') + self.api_base = os.environ.get('LITTLELLM_API_BASE') + self.default_model = os.environ.get('DEFAULT_MODEL') + self.litellm=OpenAI(api_key=self.api_key,base_url=self.api_base) + + def set_default_model(self, model_name): + self.default_model = model_name + + def get_available_models(self): + models = self.litellm.Model.list() + return models + + def chatCompletion(self, prompt="", user_context=None, masterPrompt="", initialAssistant="", model=None, maxTokens=1024, stream=False, temperature=0.5): + if model is None: + model = self.default_model + if user_context : + model = user_context.get_model_name() + messages = [] + if user_context is None: + # Usar prompts individuales + if masterPrompt: + messages.append({"role": "system", "content": masterPrompt}) + if initialAssistant: + messages.append({"role": "assistant", "content": initialAssistant}) + messages.append({"role": "user", "content": prompt}) else: - result = [masterPrompt, {"role": "user", "content": prompt}] - else: - result = '[' - for msg in chatHistory: - print(msg, flush=True) - result += str(msg) - result += ',' - result += str({"role": "user", "content": prompt}) - result += ']' - print("==============Lanzando peticion al LLM=======================") - print(result, flush=True) - - completion = llm_mks.chat.completions.create(model=current_model, messages=result, max_tokens=maxTokens) - print("========LLM OUTPUT===============") -# print(completion.choices[0].message.content) - print(completion, flush=True) - return completion + # Usar historial + for msg in user_context.get_combined_prompt(): + messages.append(msg) + + try: + response = self.litellm.chat.completions.create(model=model, messages=messages, max_tokens=maxTokens, stream=stream, temperature=temperature) + return response + except Exception as e: + response = {} + response['error'] = str(e) + return response diff --git a/Bots/LLM/main.py b/Bots/LLM/main.py new file mode 100644 index 0000000..f51588f --- /dev/null +++ b/Bots/LLM/main.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- + +import os +import platform +import json +from gladosMQTT import GladosMQTT +import GladosIA + +# Configuración MQTT +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Verificación de variables críticas +if not mqHost or not mqPort: + print("No MQTT config!") + exit(1) + +# Instancia de GladosIA +gladosBot = GladosIA.GladosBot() + +# Temas MQTT +topic_spaceapi = "space/status" +topic_slack_event = "comms/slack/event" +topic_slack_incoming_msg = "comms/slack/incoming_msg" +topic_telegram_event = "comms/telegram/event" +topic_discord_event = "comms/discord/event" +topic_slack_send_msg_id = "comms/slack/send_id" +topic_slack_edit_msg = "comms/slack/edit_msg" +topic_telegram_send_msg_id ="comms/telegram/send_id" +topic_telegram_edit_msg ="comms/telegram/edit_msg" +topic_discord_send_msg_id ="comms/discord/send_id" + +# Instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +glados_mqtt.set_topics([topic_spaceapi, topic_slack_event, topic_slack_incoming_msg, topic_telegram_event, topic_discord_event]) + +# Función para manejar mensajes MQTT +def on_mqtt_message(client, userdata, msg): + payload = msg.payload.decode('utf-8') + if msg.topic == topic_spaceapi: + data = json.loads(payload) + open_status = data['state']['open'] + #mandar mensaje de apertura con el llm? +# elif msg.topic == topic_slack_event: +# processSlackEvent(payload) + elif msg.topic == topic_slack_incoming_msg: + processSlackMSG(payload) + elif msg.topic == topic_telegram_event: + processTelegramEvent(payload) + elif msg.topic == topic_discord_event: + processDiscordEvent(payload) + # Añade más condiciones según sea necesario + +# Configurar callbacks MQTT +glados_mqtt.mqttClient.on_message = on_mqtt_message + + +def processSlackMSG(event) : + if not isinstance(event, str): + event = json.dumps(event) + data = json.loads(event) + + sender_id = data.get('sender_id') + channel_id = data.get('channel_id') + message = data.get('message') + message_id = data.get('message_id') + thread_id = data.get('thread_id') + username = data.get('username') + channel_name = data.get('channel_name') + reply_msg_id = data.get('reply_msg_id') + + response = gladosBot.ask(message, sender_id) + + payload = { + 'sender_id': sender_id, + 'channel_id': channel_id, + 'message': response, + 'reply_msg_id': reply_msg_id + } + glados_mqtt.publish(topic_slack_edit_msg, json.dumps(payload)) + + +# Funciones para procesar eventos de Slack, Telegram y Discord + +def processTelegramEvent(event): + # try: + data = json.loads(event) + respondTo = data['sender_id'] + msg = data['message_text'] + response = gladosBot.ask(msg, respondTo) + payload = { + 'sender_id': data['sender_id'], + 'channel_id': data['channel_id'], + 'message_text': response, + 'reply_msg_id': data['reply_msg_id'] + } + + glados_mqtt.publish(topic_telegram_edit_msg, json.dumps(payload)) + +# sendToTelegram(respondTo, response) + # except: + # glados_mqtt.debug("processTelegramEvent: Error procesando JSON") + # return False + + +def processDiscordEvent(event): + glados_mqtt.debug("---> DISCORD event ------------------") + glados_mqtt.debug(event) + glados_mqtt.debug("/ DISCORD event ------------------") + # try: + data = json.loads(event) + respondTo = str(data['channel_id']) + msg = data['content'] + response = gladosBot.ask(msg, respondTo) + sendToDiscord(respondTo, response) + # except: + # glados_mqtt.debug("processDiscordEvent: Error procesando JSON") + # return False + +# Funciones para enviar respuestas a Slack, Telegram y Discord +def sendToSlack(id, msg): + response = json.dumps({"dest": id, "msg": msg}) + glados_mqtt.debug(f"---> Respuesta a Slack: {response}") + glados_mqtt.publish(topic_slack_send_msg_id, response) + +def sendToTelegram(id, msg): + response = json.dumps({"dest": id, "msg": msg}) + glados_mqtt.debug(f"---> Respuesta a Telegram: {response}") + glados_mqtt.publish(topic_telegram_send_msg_id, response) + +# Enviar mensaje a Discord por ID de usuario o canal +def sendToDiscord(id, msg): + response = json.dumps({"dest": id , "msg": msg}) + glados_mqtt.debug(f'WTF id: {id} response; {response}') + glados_mqtt.debug(f"---> Respuesta a Discord: {response}") + glados_mqtt.publish(topic_discord_send_msg_id, response) + + +if __name__ == "__main__": + glados_mqtt.init_mqtt_and_loop_forever() diff --git a/Bots/LLM/node.py b/Bots/LLM/node.py deleted file mode 100644 index 01f9921..0000000 --- a/Bots/LLM/node.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- - -#Nodo Mqtt de ejemplo, -#gladosMQTT.py contiene la funcionalidad para manejar el mqtt. -# aui nos conectamos y reaccionamos a los mensajes que llegan. -# TODO Documentar mejor... gpt? xD - -import os - -import gladosMQTT -#import Bots.LLM.llm as llm -import GladosIA -import platform -import time -import json - - - -#Variables -#mqHost = os.environ.get("MQTT_HOST", "mqtt.makespacemadrid.org") -#mqPort = os.environ.get("MQTT_PORT", 1883) -mqHost = "mqtt.makespacemadrid.org" #WTF! No entiendo que pasa con las variables de entorno que vienen del compose :S -mqPort = 1883 -nodeName = platform.node() - - -gladosBot = GladosIA.GladosBot() - - - -if not mqHost or not mqPort: - print("No mqtt config!") - exit(1) - -topic_spaceapi = "space/status" -topic_slack = "comms/slack" -topic_slack_event = topic_slack+"/event" -topic_glados_send_msg_id = topic_slack+"/send_id" -topic_glados_send_msg_name = topic_slack+"/send_name" - - -def subscribeTopics() : - gladosMQTT.subscribe(topic_spaceapi) - gladosMQTT.subscribe(topic_slack_event) - -def on_connect(client, userdata, rc,arg): - subscribeTopics() - -def on_disconnect(client, userdata, rc): - gladosMQTT.debug("Disconnected! rc: "+str(rc)) - -def on_message(client, userdata, msg): - if (msg.topic == topic_spaceapi) : - try: - # Extraer la carga útil y decodificarla a una cadena de texto - payload = msg.payload.decode('utf-8') - data = json.loads(payload) - open_status = data['state']['open'] - except json.JSONDecodeError as e: - gladosMQTT.debug("Error al parsear JSON:") - - elif(msg.topic == topic_slack_event): - try: - # Extraer la carga útil y decodificarla a una cadena de texto - payload = msg.payload.decode('utf-8') - processSlackEvent(payload) - except json.JSONDecodeError as e: - gladosMQTT.debug("Error al parsear JSON:") - - -def processSlackEvent(event): - gladosMQTT.debug(event) - try: - data = json.loads(event) - if data['type'] != "message" or 'bot_id' in data: - return False - except: - gladosMQTT.debug("processSlackEvent:Error procesado json") - - try: - # Mensajes de union a canal - if 'subtype' in data and data['subtype']=="channel_join": - respondTo = data['channel'] - msg = data['text'] - response = gladosBot.ask(msg) - gladosMQTT.debug(response) - sendToSlack(respondTo,response) - #Mensaje a canal - elif data['channel_type'] == "channel": - respondTo = data['channel'] - msg = data['text'] - if '<@U05LXTJ7Q66>' in msg or 'glados' in msg.lower(): - response = gladosBot.ask(msg) - gladosMQTT.debug(response) - sendToSlack(respondTo,response) - #Mensaje privado - elif data['channel_type'] == "im": - respondTo = data['user'] - msg = data['text'] - response = gladosBot.ask(msg) - gladosMQTT.debug(response) - sendToSlack(respondTo,response) - except: - gladosMQTT.debug("processSlackEvent:Error gestionando evento") - sendToSlack(respondTo,"ERROR: Computer says nooooooo") - -def sendToSlack(id,msg): - response = json.dumps({"dest": id, "msg": msg}) - gladosMQTT.debug(f"Respuesta a slack: {response}") - gladosMQTT.publish(topic_glados_send_msg_id,response) - -gladosMQTT.initMQTTandLoopForever(mqHost,mqPort,nodeName,on_connect,on_message,on_disconnect) \ No newline at end of file diff --git a/Bots/LLM/requirements.txt b/Bots/LLM/requirements.txt index e917e8b..196e635 100644 --- a/Bots/LLM/requirements.txt +++ b/Bots/LLM/requirements.txt @@ -1,2 +1,7 @@ paho-mqtt -openai \ No newline at end of file +openai +langchain +chromadb +tiktoken +requests +numpy \ No newline at end of file diff --git a/Bots/Nvidia/Dockerfile b/Bots/Nvidia/Dockerfile new file mode 100644 index 0000000..6e15a4a --- /dev/null +++ b/Bots/Nvidia/Dockerfile @@ -0,0 +1,22 @@ +# Base Image with NVIDIA Runtime +FROM nvidia/cuda:12.3.1-runtime-ubuntu22.04 +RUN apt-get update && apt-get install -y build-essential cmake python3 python3-pip python-is-python3 +ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:/usr/local/nvidia/lib:/usr/local/nvidia/lib64 +# Utiliza una imagen base que ya tiene el cliente mqtt +#FROM makespacemadrid/glados-base-node:1.5 + + +VOLUME [ "/data" ] +# Establece el directorio de trabajo en /app +WORKDIR /app +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . +RUN pip install -r requirements.txt + +# Copia el archivo node.py al contenedor +COPY main.py . +COPY gladosMQTT.py . +USER 1000 + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "main.py"] diff --git a/Bots/Nvidia/docker-compose.yml b/Bots/Nvidia/docker-compose.yml new file mode 100644 index 0000000..c8395f0 --- /dev/null +++ b/Bots/Nvidia/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' +services: + glados-nvidia: + build: . + image: makespacemadrid/glados-nvidia:latest + hostname: mqtt-nvidia + env_file: + - .env +# volumes: +# - ./data:/data + restart: always + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] \ No newline at end of file diff --git a/Bots/Nvidia/gladosMQTT.py b/Bots/Nvidia/gladosMQTT.py new file mode 100644 index 0000000..904c6c2 --- /dev/null +++ b/Bots/Nvidia/gladosMQTT.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +import paho.mqtt.client as mqtt +import json + +class GladosMQTT: + def __init__(self, host="192.168.1.1", port=1883, name="test-node", msg_callback=None): + self.mqttServer = host + self.mqttPort = port + self.nodeName = name + self.baseTopic = "node/" + self.nodeName + self.statusTopic = self.baseTopic+'/status' + self.debugTopic = self.baseTopic + "/debug" + self.mqttClient = mqtt.Client() + self.topics = [] + self.nodeMsgCallback = msg_callback if msg_callback is not None else self.dummy + + self.mqttClient.on_connect = self.on_connect + self.mqttClient.on_message = self.on_message + self.mqttClient.on_disconnect = self.on_disconnect + self.mqttClient.will_set(self.statusTopic,'OFFLINE', int(2), True) + + + def set_topics(self, topics): + self.topics = topics + + def dummy(self, *args, **kwargs): + self.debug("Dummy function called") + + def publish(self, topic, msg, persist=False): + self.mqttClient.publish(topic, msg,int(2), persist) + + def debug(self, msg): + if not isinstance(msg, str): + try: + msg = json.dumps(msg) + except (TypeError, ValueError): + msg = str(msg) + + print(msg) + self.publish(self.debugTopic, msg) + + def on_connect(self, client, userdata, flags, rc): + self.debug("[GladosNode] Connected with result code " + str(rc)) + self.mqttClient.publish(self.statusTopic,'ONLINE', int(2)) + for topic in self.topics: + self.mqttClient.subscribe(topic,int(2)) + + def on_message(self, client, userdata, msg): +# try: +# self.debug("[GladosNode] mqtt_rcv: { " + msg.topic + " - " + msg.payload.decode() + " }") +# except Exception as e: +# self.debug("[GladosNode] mqtt_rcv error: " + str(e)) + self.nodeMsgCallback(client, userdata, msg) + + def on_disconnect(self, client, userdata, rc): + self.debug("[GladosNode] Disconnected with result code " + str(rc)) + + def init_mqtt(self): + self.debug("[GladosNode] Connecting : " + str(self.mqttServer) + " Port:" + str(self.mqttPort) + " node: " + str(self.nodeName)) + try: + self.mqttClient.connect(self.mqttServer, port=self.mqttPort, keepalive=120) + except Exception as e: + self.debug("Cannot connect: " + str(e)) + self.mqttClient.loop_start() + + def init_mqtt_and_loop_forever(self): + self.debug("[GladosNode] Connecting : " + str(self.mqttServer) + " Port:" + str(self.mqttPort) + " node: " + str(self.nodeName)) + try: + self.mqttClient.connect(self.mqttServer, port=self.mqttPort, keepalive=120) + except Exception as e: + self.debug("Cannot connect: " + str(e)) + self.mqttClient.loop_forever() diff --git a/Bots/Nvidia/main.py b/Bots/Nvidia/main.py new file mode 100644 index 0000000..2503c3e --- /dev/null +++ b/Bots/Nvidia/main.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +import time +import os +import platform +import subprocess +import json +from io import StringIO # Required for redirecting stdout & stderr +import pandas as pd +from gladosMQTT import GladosMQTT + +# Configuración MQTT +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Verificación de variables críticas +if not mqHost or not mqPort: + print("No MQTT config!") + exit(1) + +# Temas MQTT +topic_nvidia_stats ="nvidia/asimov/last_status" +# Instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +glados_mqtt.set_topics([topic_nvidia_stats]) +last_reading_timestamp = 0 +last_reading_wh_drawn = 0 +total_kwh_drawn = 0 + +def get_nvidia_stats(): + try: + command = 'nvidia-smi --query-gpu=power.draw,temperature.gpu,utilization.gpu,utilization.memory,gpu_name,driver_version,pstate,pcie.link.gen.current --format=csv,nounits' + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + + # Reading the CSV data and converting it to a pandas DataFrame + data = pd.read_csv(StringIO(output)) + + # Converting the DataFrame to json format compatible with Home Assistant + json_data = json.loads(json.dumps(data.to_dict(orient='records'))) + + return json_data + except Exception as e: + glados_mqtt.debug(str(e)) + +def get_stats(): + global last_reading_timestamp + global last_reading_wh_drawn + global total_kwh_drawn + + now = time.time() + time_delta = now - last_reading_timestamp + kw_spent = float(time_delta/1000/3600)*float(last_reading_wh_drawn) + total_kwh_drawn += kw_spent + stats=get_nvidia_stats() + last_reading_timestamp = now + glados_mqtt.debug(stats) + + +# Función para manejar mensajes MQTT +def on_mqtt_message(client, userdata, msg): + global total_kwh_drawn + payload = msg.payload.decode('utf-8') + if msg.topic == topic_nvidia_stats: + #decode payload to json and extract totalKWh + total_kwh_drawn=float(json.loads(payload)['totalKWh']) + print("Total KWH drawn: " + str(total_kwh_drawn)) + + +# Configurar callbacks MQTT +glados_mqtt.mqttClient.on_message = on_mqtt_message + + +# Inicializar y correr el loop de mensajes MQTT +if __name__ == "__main__": + glados_mqtt.init_mqtt() + while True: + time.sleep(1) + get_stats() + + diff --git a/Bots/Nvidia/requirements.txt b/Bots/Nvidia/requirements.txt new file mode 100644 index 0000000..e845e0b --- /dev/null +++ b/Bots/Nvidia/requirements.txt @@ -0,0 +1,4 @@ +paho-mqtt<2.0.0 +openai +pandas +requests diff --git a/Bots/Ollama-Bridge-LiteLLM/Dockerfile b/Bots/Ollama-Bridge-LiteLLM/Dockerfile new file mode 100644 index 0000000..0af0512 --- /dev/null +++ b/Bots/Ollama-Bridge-LiteLLM/Dockerfile @@ -0,0 +1,15 @@ +# Utiliza una imagen base de Python +FROM python:3.10 + +# Establece el directorio de trabajo en /app +WORKDIR /app + +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . +# Instala las dependencias +RUN pip install -r requirements.txt + +COPY main.py . + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/Ollama-Bridge-LiteLLM/docker-compose.yml b/Bots/Ollama-Bridge-LiteLLM/docker-compose.yml new file mode 100644 index 0000000..008c90a --- /dev/null +++ b/Bots/Ollama-Bridge-LiteLLM/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + bridge-ollama-litellm: + build: . + image: makespacemadrid/olm-bridge:1.0 + hostname: bridge-ollama-litellm + env_file: + - .env diff --git a/Bots/Ollama-Bridge-LiteLLM/main.py b/Bots/Ollama-Bridge-LiteLLM/main.py new file mode 100644 index 0000000..7bc17b0 --- /dev/null +++ b/Bots/Ollama-Bridge-LiteLLM/main.py @@ -0,0 +1,136 @@ +import requests +import json +import os + + +ollama_url=os.environ['OLLAMA_URL'] +litellm_url=os.environ['LITELLM_URL'] +litellm_key=os.environ['LITELLM_KEY'] + +def fetch_ollama_models(): +#response example: +#{ +# "models": [ +# { +# "name": "dolphin-mistral:7b", +# "model": "dolphin-mistral:7b", +# "modified_at": "2024-03-17T23:28:34.032Z", +# "size": 4109870615, +# "digest": "ecbf896611f5b38fddc717b2e5609956980dc81187031eccc238a2547378862d", +# "details": { +# "parent_model": "", +# "format": "gguf", +# "family": "llama", +# "families": [ +# "llama" +# ], +# "parameter_size": "7B", +# "quantization_level": "Q4_0" +# } +# } +# ] +#} + + + + + url = f'{ollama_url}/api/tags' + response = requests.get(url) + + if response.status_code == 200: + data = response.json() + for model in data.get('models', []): + print(f"Name: {model['name']}") + print(f"Model: {model['model']}") + print(f"Modified At: {model['modified_at']}") + print(f"Size: {model['size']}") + print(f"Digest: {model['digest']}") + print(f"Details: {model['details']}") + print('-----------------------------') + return data.get('models', []) + else: + print(f"Failed to fetch models. Status code: {response.status_code}") + return None + +def fetch_litellm_models(): +#example output: +#{ +# "data": [ +# { +# "id": "dolphin-mistral:7b", +# "object": "model", +# "created": 1677610602, +# "owned_by": "openai" +# } +# ], +# "object": "list" +#} + url = f'{litellm_url}/v1/models' + headers = { + 'Authorization': 'Bearer sk-1234makespace' + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + for model in data['data']: + print(f"Name: {model['id']}") + return data['data'] + else: + print(f"Failed to fetch OpenAI models. Status code: {response.status_code}") + return None + +def check_chatmode(model_name): + chatmodels = ['vicuna', 'wizard-vicuna'] + no_chatmodels = ['wizardcoder','starcoder','codestral','deepseek','nomic-embed-text'] + for name in chatmodels: + if name in model_name: + return True + for name in no_chatmodels: + if name in model_name: + return False + return True + +def add_model_to_litellm(model_name): + + input_cost_per_token = 0.0001 + output_cost_per_token = 0.0001 + max_tokens = 4096 + + headers = { 'Authorization': f'Bearer {litellm_key}', 'Content-Type': 'application/json' } + response='' + + if '16k' in model_name: + max_tokens = 16000 + + if check_chatmode(model_name): + mode = 'chat' + data = { 'model_name': model_name, 'litellm_params': { 'api_base': ollama_url, 'model': f"ollama_chat/{model_name}", 'input_cost_per_token': input_cost_per_token, 'output_cost_per_token': output_cost_per_token, 'max_tokens': max_tokens }, 'model_info': { 'id': model_name, 'mode': mode, 'input_cost_per_token': input_cost_per_token, 'output_cost_per_token': output_cost_per_token, 'max_tokens': max_tokens } } + response = requests.post(f'{litellm_url}/model/new', headers=headers, json=data) + else: + data = { 'model_name': model_name, 'litellm_params': { 'api_base': ollama_url, 'model': f"ollama/{model_name}", 'input_cost_per_token': input_cost_per_token, 'output_cost_per_token': output_cost_per_token, 'max_tokens': max_tokens }, 'model_info': { 'id': model_name,'input_cost_per_token': input_cost_per_token, 'output_cost_per_token': output_cost_per_token, 'max_tokens': max_tokens } } + response = requests.post(f'{litellm_url}/model/new', headers=headers, json=data) + print(response.json()) + + + + +#get ollama models +ollama_models = fetch_ollama_models() +litellm_models = fetch_litellm_models() + +if ollama_models is None or litellm_models is None: + print("Failed to fetch models") + exit(1) +#make a list with the models that are present in ollama but not in openai +missing_models = [] +for model in ollama_models: + if model['name'] not in [model['id'] for model in litellm_models]: + missing_models.append(model) +print("Missing models name:") + +for model in missing_models: + name=model['name'] + print(name) + add_model_to_litellm(name) diff --git a/Bots/Ollama-Bridge-LiteLLM/requirements.txt b/Bots/Ollama-Bridge-LiteLLM/requirements.txt new file mode 100644 index 0000000..3fa77be --- /dev/null +++ b/Bots/Ollama-Bridge-LiteLLM/requirements.txt @@ -0,0 +1,3 @@ +requests +python-dotenv +#json diff --git a/Bots/Slack/Dockerfile b/Bots/Slack/Dockerfile index cea590b..47d9b49 100644 --- a/Bots/Slack/Dockerfile +++ b/Bots/Slack/Dockerfile @@ -1,5 +1,5 @@ # Utiliza una imagen base que ya tiene el cliente mqtt -FROM makespacemadrid/glados-base-node:1.0 +FROM makespacemadrid/glados-base-node:1.5 # Establece el directorio de trabajo en /app WORKDIR /app @@ -9,11 +9,11 @@ WORKDIR /app COPY requirements.txt . # Copia el archivo node.py al contenedor -COPY node.py . +COPY main.py . # Instala las dependencias RUN pip install -r requirements.txt USER 1000 # Ejecuta el programa cuando el contenedor se inicie -CMD ["python", "node.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/Slack/docker-compose.yml b/Bots/Slack/docker-compose.yml index e4400d0..a15d49b 100644 --- a/Bots/Slack/docker-compose.yml +++ b/Bots/Slack/docker-compose.yml @@ -2,16 +2,13 @@ version: '3' services: - mqtt-slack: + glados-slack: build: . image: makespacemadrid/glados-mqtt-slack:latest hostname: mqtt-slack env_file: - .env environment: - - MQTT_PORT = 1883 - - MQTT_HOST = mqtt.makespacemadrid.org - - SLACK_API_TOKEN = ${SLACK_API_TOKEN} - SLACK_PORT = 5000 # No estoy seguro que funcione restart: always ports: diff --git a/Bots/Slack/main.py b/Bots/Slack/main.py new file mode 100644 index 0000000..9a4d596 --- /dev/null +++ b/Bots/Slack/main.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +import os +import platform +import json +from gladosMQTT import GladosMQTT +from flask import Flask, request, jsonify +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +# Configuración +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() +slack_token = os.environ.get("SLACK_API_TOKEN") +slack_port = os.environ.get("SLACK_PORT") + +# Variables globales para mantener el estado +last_open_status = None +report_open = True + +# Verificación de variables críticas +if not slack_token: + print("Falta el token de Slack") + exit(1) + +# Temas MQTT +topic_spaceapi = "space/status" +topic_report_space_open = "space/report_open" +topic_last_open_status = "space/last_open_status" +topic_slack = "comms/slack" +topic_slack_event = topic_slack+"/event" +topic_slack_incoming_msg = topic_slack+"/incoming_msg" +topic_slack_send_msg_id = topic_slack+"/send_id" +topic_slack_send_msg_name = topic_slack+"/send_name" +topic_slack_edit_msg = topic_slack+"/edit_msg" + +# Instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +glados_mqtt.set_topics([topic_last_open_status,topic_spaceapi, topic_slack_send_msg_id, topic_slack_send_msg_name,topic_slack_edit_msg]) + +# Cliente de Slack +slack_client = WebClient(token=slack_token) +slack_client.users_setPresence(presence="auto") + +# Flask app +app = Flask(__name__) + +# Función para manejar mensajes MQTT +def on_mqtt_message(client, userdata, msg): + global last_open_status + global report_open + + # Manejar mensaje de SpaceAPI + if msg.topic == topic_spaceapi: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + open_status = data['state']['open'] + openSpace(open_status) + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al parsear JSON: " + str(e)) + + # Manejar último estado de apertura + elif msg.topic == topic_last_open_status: + last_open_status = msg.payload.decode('utf-8') == 'true' + + # Manejar reporte de estado de apertura + elif msg.topic == topic_report_space_open: + report_open = msg.payload.decode('utf-8') == 'true' + + # Enviar mensaje a Slack por ID + elif msg.topic == topic_slack_send_msg_id: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + sendSlackMsgbyID(data['dest'], data['msg']) + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al procesar mensaje para Slack (ID): " + str(e)) + + # Enviar mensaje a Slack por nombre + elif msg.topic == topic_slack_send_msg_name: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + sendSlackMsgbyName(data['dest'], data['msg']) + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al procesar mensaje para Slack (nombre): " + str(e)) + + elif msg.topic == topic_slack_edit_msg: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + editSlackMsg(data['channel_id'], data['reply_msg_id'], data['message']) + except json.JSONDecodeError as e: + glados_mqtt.debug(f"Error processing message edit request: {e}") + +# Función adicional para manejar el estado de apertura +def openSpace(status): + global last_open_status + global report_open + if last_open_status is None : + last_open_status = status == 'false' #Si por alguna razon last_status no esta inicializado cargamos el valor contratio para que si mande el mensaje. + + if status and not last_open_status: + glados_mqtt.debug("Espacio abierto") + glados_mqtt.publish(topic_last_open_status, 'true', True) + last_open_status = True + if report_open : + sendSlackMsgbyName("abierto-cerrado", "¡Espacio Abierto! Let's Make!") + elif not status and last_open_status: + glados_mqtt.debug("Espacio cerrado") + glados_mqtt.publish(topic_last_open_status, 'false', True) + last_open_status = False + if report_open : + sendSlackMsgbyName("abierto-cerrado", "¡Espacio Cerrado! ZZzzZZ") + + +# Configuración de callbacks MQTT +glados_mqtt.mqttClient.on_message = on_mqtt_message + +# Iniciar cliente de Slack +slack_client = WebClient(token=slack_token) + +# Variables para almacenar la lista de canales y usuarios en caché +cached_slack_channels = {} +cached_slack_users = {} + +# Función para cargar y almacenar en caché los canales de Slack +def cache_slack_channels(): + global cached_slack_channels + try: + response = slack_client.conversations_list() + cached_slack_channels = {channel['id']: channel['name'] for channel in response['channels']} + except SlackApiError as e: + glados_mqtt.debug(f"Error getting Slack channel list: {e}") + +# Función para cargar y almacenar en caché los usuarios de Slack +def cache_slack_users(): + global cached_slack_users + try: + response = slack_client.users_list() + cached_slack_users = {user['id']: user['name'] for user in response['members'] if 'id' in user} + except SlackApiError as e: + glados_mqtt.debug(f"Error obtaining Slack user list: {e}") + +# Función para obtener los mensajes de un hilo en Slack utilizando slack_sdk +def getThreadMessages(channel_id, thread_id): + try: + # Llamar al método conversations.replies para obtener los mensajes del hilo + response = slack_client.conversations_replies(channel=channel_id, ts=thread_id) + + # Extraer los mensajes del hilo + messages = response['messages'] + return messages + except SlackApiError as e: + print(f"Error al obtener los mensajes del hilo de Slack: {e.response['error']}") + return [] + + +def getEventInfo(event): + try: + if not isinstance(event, str): + event = json.dumps(event) + data = json.loads(event) + + sender_id = data.get('user') + channel_id = data.get('channel') + message = data.get('text') + message_id = data.get('ts') + thread_id = data.get('thread_ts') + username = getSlackUserName(sender_id) + channel_name = getSlackChannelName(channel_id) + + payload = { + 'sender_id': sender_id, + 'channel_id': channel_id, + 'username': username, + 'channel_name': channel_name, + 'message': message, + 'message_id': message_id, + 'thread_id': thread_id + } + return payload + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al procesar mensaje para Slack (nombre): " + str(e)) + return False + + +def editSlackMsg(channel_id, message_timestamp, new_msg): + try: + slack_client.chat_update(channel=channel_id, ts=message_timestamp, text=new_msg) + except SlackApiError as e: + glados_mqtt.debug(f"Error editing message in Slack: {e}") + +def processThreadMessages(channel_id, thread_id): + # Directly get the thread messages without json.loads since getThreadMessages already returns a list + thread_messages = getThreadMessages(channel_id, thread_id) + processed_messages = [] + for message in thread_messages: + payload = getEventInfo(message) + processed_messages.append(payload) + + return processed_messages + +# Función para procesar los eventos entrantes de Slack +def processSlackEvents(event): + try: + if not isinstance(event, str): + event = json.dumps(event) + data = json.loads(event) + subtype = "" + if 'subtype' in data : + subtype = data['subtype'] + if ( + data['type'] != "message" or + 'bot_id' in data or + 'app_id' in data or + subtype in ("thread_broadcast", "message_changed", "message_deleted") + ): + return False + if data['channel_type'] == "channel": + respondTo = data['channel'] + msg = data['text'] + # Si es un mensaje en un canal pero no se nos menciona no respondemos. + if not ('<@U05LXTJ7Q66>' in msg or 'glados' in msg.lower()): + return + + payload = getEventInfo(event) + # Send a reply that we are 'Working' on it + response = slack_client.chat_postMessage(channel=payload['channel_id'], text="Recibido, consultando LLM...", + thread_ts=payload['message_id'] if payload.get('thread_id') else None) + # Extract and publish the message id of the reply + payload['reply_msg_id']= response['message']['ts'] + + # Si el mensaje está en un hilo, obtener el hilo completo + if payload['thread_id'] : + thread_messages = processThreadMessages(payload['channel_id'], payload['thread_id']) + payload['thread_messages'] = thread_messages + + glados_mqtt.publish(topic_slack_incoming_msg, json.dumps(payload)) + except json.JSONDecodeError as e: + glados_mqtt.debug("Error al procesar mensaje para Slack (nombre): " + str(e)) + return False + +# Función para enviar un mensaje a Slack por ID de usuario o canal +def sendSlackMsgbyID(channel_id, msg): + try: + slack_client.chat_postMessage(channel=channel_id, text=msg) + except SlackApiError as e: + glados_mqtt.debug(f"Error al enviar mensaje a Slack por ID: {e}") + +# Función para enviar un mensaje a Slack por nombre de usuario o canal +def sendSlackMsgbyName(name, msg): + # Determinar si es un usuario o un canal y obtener su ID + is_user = getSlackUserName(name) + is_channel = getSlackChannelName(name) if not is_user else None + target_id = is_user or is_channel + + if target_id: + try: + slack_client.chat_postMessage(channel=target_id, text=msg) + except SlackApiError as e: + glados_mqtt.debug(f"Error al enviar mensaje a Slack por nombre: {e}") + else: + glados_mqtt.debug("Usuario o canal no encontrado en Slack") + +# Actualizado para usar caché +def getSlackChannelName(channel_id): + if channel_id in cached_slack_channels: + return cached_slack_channels[channel_id] + else: + # Recargar caché si el canal no se encuentra + cache_slack_channels() + return cached_slack_channels.get(channel_id, None) + +# Actualizado para usar caché +def getSlackUserName(user_id): + if user_id in cached_slack_users: + return cached_slack_users[user_id] + else: + # Recargar caché si el usuario no se encuentra + cache_slack_users() + return cached_slack_users.get(user_id, None) + +# Función para publicar una vista en el "home" de un usuario en Slack +def publishHomeView(user_id, view): + try: + slack_client.views_publish(user_id=user_id, view=view) + except SlackApiError as e: + glados_mqtt.debug(f"Error al publicar vista en Slack: {e}") + + +# Rutas Flask +@app.route('/slack/events', methods=['POST']) +def slack_events(): + try: + data = request.json + # Desafío de URL para la verificación con Slack + if data.get('type') == 'url_verification': + return jsonify({'challenge': data.get('challenge')}), 200 + + # Manejo de eventos de callback + if data.get('type') == 'event_callback': + event = data.get('event', {}) + glados_mqtt.publish(topic_slack_event, json.dumps(event)) # Enviamos el evento a la cola mqtt + processSlackEvents(event) + + if event.get('type') == 'app_home_opened': + user_id = event.get('user') + if user_id: + publishHomeView(user_id) + + except Exception as e: + glados_mqtt.debug(f'Error procesando el evento de slack: {e}') + finally: + # Usamos el bloque finally para asegurarnos de que siempre ejecutamos el retorno. + return jsonify({'status': 'ok'}), 200 + +if __name__ == "__main__": + # Cargar listas en caché antes de iniciar el MQTT y Flask + cache_slack_channels() + cache_slack_users() + glados_mqtt.init_mqtt() + app.run(host='0.0.0.0', port=slack_port) \ No newline at end of file diff --git a/Bots/Slack/node.py b/Bots/Slack/node.py deleted file mode 100644 index 36d0266..0000000 --- a/Bots/Slack/node.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- - -#Nodo Mqtt de ejemplo, -#gladosMQTT.py contiene la funcionalidad para manejar el mqtt. -# aui nos conectamos y reaccionamos a los mensajes que llegan. -# TODO Documentar mejor... gpt? xD - -import os - -import gladosMQTT -import platform -import time -import json - - -from flask import Flask, request, jsonify -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - - -#Variables -mqHost = os.environ.get("MQTT_HOST", "mqtt.makespacemadrid.org") -mqPort = os.environ.get("MQTT_PORT", 1883) -nodeName = platform.node() - -slack_token = os.environ.get("SLACK_API_TOKEN") -slack_port = os.environ.get("SLACK_PORT") - -last_open_status = False -report_open = True - - -topic_spaceapi = "space/status" -topic_report_space_open = "space/report_open" -topic_last_open_status = "space/last_open_status" -topic_slack = "comms/slack" -topic_slack_event = topic_slack+"/event" -topic_glados_send_msg_id = topic_slack+"/send_id" -topic_glados_send_msg_name = topic_slack+"/send_name" - - -if not slack_token: - print(f"Falta el token de slack {slack_token}") - exit(1) - -def subscribeTopics() : - gladosMQTT.subscribe(topic_last_open_status) - gladosMQTT.subscribe(topic_spaceapi) - gladosMQTT.subscribe(topic_glados_send_msg_id) - gladosMQTT.subscribe(topic_glados_send_msg_name) - -def on_connect(client, userdata, rc,arg): - subscribeTopics() - -def on_disconnect(client, userdata, rc): - gladosMQTT.debug("Disconnected! rc: "+str(rc)) - -def on_message(client, userdata, msg): - global last_open_status - global report_open - -#SpaceAPI - if (msg.topic == topic_spaceapi) : - try: - # Extraer la carga útil y decodificarla a una cadena de texto - payload = msg.payload.decode('utf-8') - data = json.loads(payload) - open_status = data['state']['open'] - openSpace(open_status) - except json.JSONDecodeError as e: - gladosMQTT.debug("Error al parsear JSON:") -#LastOpenStatus - elif(msg.topic == topic_last_open_status): - if msg.payload.decode('utf-8') == 'true': - last_open_status = True - else: - last_open_status = False -#ReportSpaceOpen - elif(msg.topic == topic_report_space_open): - if msg.payload.decode('utf-8') == 'true': - report_open = True - else: - report_open = False -#SendMsgID - elif(msg.topic == topic_glados_send_msg_id): - payload = msg.payload.decode('utf-8') - data = json.loads(payload) - dest = data['dest'] - content = data['msg'] - sendSlackMsgbyID(dest,content) -#SendMsgName - elif(msg.topic == topic_glados_send_msg_id): - payload = msg.payload.decode('utf-8') - data = json.loads(payload) - dest = data['dest'] - content = data['msg'] - sendSlackMsgbyName(dest,content) - -def openSpace(status): - global last_open_status - global report_open - - if not report_open: - return - - if status and not last_open_status: - gladosMQTT.debug("open!") - gladosMQTT.publish(topic_last_open_status,'true',True) - last_open_status = True - sendSlackMsgbyName("abierto-cerrado","¡Espacio Abierto! Let's Make!") - elif not status and last_open_status : - gladosMQTT.debug("closed!") - gladosMQTT.publish(topic_last_open_status,'false',True) - last_open_status = False - sendSlackMsgbyName("abierto-cerrado","¡Espacio Cerrado! ZZzzZZ") - - - -gladosMQTT.initMQTT(mqHost,mqPort,nodeName,on_connect,on_message,on_disconnect) - -slack_client = WebClient(token=slack_token) - -def getSlackChannelId(name): - try: - channels_response = slack_client.conversations_list() - for channel in channels_response['channels']: - gladosMQTT.debug(channel['name']) - if channel['name'] == name: - return channel['id'] - print("Canal no encontrado") - return None - except SlackApiError as e: - print(f"Error al obtener el ID del canal: {e}") - return None - -def getSlackUserId(username): - try: - response = slack_client.users_list() - users = response['members'] - for user in users: - if 'name' in user and user['name'] == username: - return user['id'] - return None - except SlackApiError as e: - gladosMQTT.debug("Error al obtener la lista de usuarios") - return None - - -def sendSlackMsgbyID(id, msg): - try: - slack_client.chat_postMessage(channel=id, text=msg) - gladosMQTT.debug("Mensaje enviado a" + id) - except SlackApiError as e: - gladosMQTT.debug("Error al enviar mensaje a usuario:") - -def sendSlackMsgbyName(name, msg): - try: - #Es un usuario? - isuser = getSlackUserId(name) - if isuser: - slack_client.chat_postMessage(channel=isuser, text=msg) - gladosMQTT.debug(f"Mensaje enviado a {isuser}") - return 0 - #Es un canal? - ischannel = getSlackChannelId(name) - if ischannel: - slack_client.chat_postMessage(channel=ischannel, text=msg) - gladosMQTT.debug(f"Mensaje enviado a {isuser}") - return 0 - except SlackApiError as e: - gladosMQTT.debug(f"Error al enviar mensaje a usuario: {e}") - #Si llegamos aqui es que no hemos podido encontrarlo en la lista de usuarios ni en la de canales - gladosMQTT.debug(f"Error al enviar mensaje: no se encuentra el usuario o canal de destino") - - -app = Flask(__name__) -@app.route('/slack/events', methods=['POST']) -def slack_events(): - data = request.json - # Desafío de verificación de Slack - if data.get('type') == 'url_verification': - return jsonify({'challenge': data.get('challenge')}) - - if data['type'] == 'event_callback': - event = data['event'] - gladosMQTT.publish(topic_slack_event, json.dumps(event)) -# if 'bot_id' not in event and (event['type'] == 'message'): -# try: -# response = slack_client.chat_postMessage( -# channel=event['channel'], -# text=askGLaDOS(event['text']) -# ) -# except SlackApiError as e: -# print(f"Error sending message: {e.response['error']}") - return jsonify({'status': 'ok'}), 200 - - -app.run(host='0.0.0.0', port=slack_port) \ No newline at end of file diff --git a/Bots/TelegramBot/Dockerfile b/Bots/TelegramBot/Dockerfile new file mode 100644 index 0000000..47d9b49 --- /dev/null +++ b/Bots/TelegramBot/Dockerfile @@ -0,0 +1,19 @@ +# Utiliza una imagen base que ya tiene el cliente mqtt +FROM makespacemadrid/glados-base-node:1.5 + +# Establece el directorio de trabajo en /app +WORKDIR /app + + +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . + +# Copia el archivo node.py al contenedor +COPY main.py . + +# Instala las dependencias +RUN pip install -r requirements.txt +USER 1000 + +# Ejecuta el programa cuando el contenedor se inicie +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/TelegramBot/docker-compose.yml b/Bots/TelegramBot/docker-compose.yml new file mode 100644 index 0000000..5bfbc53 --- /dev/null +++ b/Bots/TelegramBot/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + mqtt-telegram: + build: . + image: makespacemadrid/glados-mqtt-telegram:latest + hostname: mqtt-telegram + env_file: + - .env + volumes: + - ./data:/data + restart: always \ No newline at end of file diff --git a/Bots/TelegramBot/main.py b/Bots/TelegramBot/main.py new file mode 100644 index 0000000..86ca0f1 --- /dev/null +++ b/Bots/TelegramBot/main.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +import os +import json +import platform +from gladosMQTT import GladosMQTT # Asegúrate de que el import sea correcto +from telethon import TelegramClient, events +import asyncio +import requests +from io import BytesIO + + +# Configuración de Telegram +api_id = os.environ.get("TELEGRAM_API_ID") +api_hash = os.environ.get("TELEGRAM_API_HASH") +telegram_token = os.environ.get("TELEGRAM_BOT_TOKEN") + +# Configuración MQTT +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +tts_url = os.environ.get("TTS_URL") + + +if not telegram_token: + raise ValueError("No se encontró el token del bot de Telegram.") +if not api_hash: + raise ValueError("No se encontró el api_hash del bot de Telegram.") +if not api_id: + raise ValueError("No se encontró el api_id del bot de Telegram.") +if not nodeName: + raise ValueError("No se encontró el nodeName del bot de Telegram.") +if not mqHost: + raise ValueError("No se encontró el mqHost del bot de Telegram.") +if not mqPort: + raise ValueError("No se encontró el mqPort del bot de Telegram.") + +# Temas MQTT +topic_telegram = "comms/telegram" +topic_telegram_edit_msg = topic_telegram + "/edit_msg" +topic_telegram_send_msg = topic_telegram + "/send_id" +topic_telegram_event = topic_telegram + "/event" + +# Definición de los callbacks para MQTT +def on_mqtt_message(client, userdata, msg): + if msg.topic == topic_telegram_send_msg: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + dest = data['dest'] + content = data['msg'] + send_telegram_message_sync(dest, content) + generate_and_send_audio_response(dest,content) + except Exception as e: + glados_mqtt.debug(f"Error al enviar mensaje: {e}") + elif msg.topic == topic_telegram_edit_msg: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + channel_id = data['channel_id'] + reply_id = data['reply_msg_id'] + content = data['message_text'] + edit_telegram_message_sync(channel_id,reply_id,content) + generate_and_send_audio_response(channel_id,content) + except Exception as e: + glados_mqtt.debug(f"Error al editar mensaje: {e}") + +def generate_and_send_audio_response(user_id, text): + global tts_url + headers = {'Content-Type': 'application/json'} + data = { + "text": text, + "language": "es" + } + + try: + # Cambia a usar requests.post y envía los datos como JSON en el cuerpo de la petición + tts_response = requests.post(tts_url, json=data, headers=headers) + tts_response.raise_for_status() # Esto provocará una excepción si la respuesta no es exitosa + + # Enviar el audio directamente a Telegram sin guardar en disco + audio_bytes = BytesIO(tts_response.content) + audio_bytes.name = "response.wav" + send_telegram_audio_sync(user_id, audio_bytes) + except requests.RequestException as e: + glados_mqtt.debug(f"Error al generar respuesta de audio: {e}") + +async def send_audio_to_transcription_api(audio_file_path): + url = "https://whisper.makespacemadrid.org/asr" + params = { + "encode": "true", + "task": "transcribe", + "language": "es", + "word_timestamps": "false", + "output": "txt" + } + files = {'audio_file': (audio_file_path, open(audio_file_path, 'rb'), 'audio/wav')} + + try: + response = requests.post(url, params=params, files=files) + response.raise_for_status() # Esto provocará una excepción si la respuesta no es exitosa + return response.text # Retorna el contenido de la transcripción + except requests.RequestException as e: + glados_mqtt.debug(f"Error al enviar el archivo de audio a la API: {e}") + + +# Iniciar cliente de Telegram +telegram_client = TelegramClient('/data/tg_sess.ignore', api_id, api_hash) + +# Función para enviar mensajes de Telegram de forma síncrona +def send_telegram_message_sync(user_id, message): + asyncio.run_coroutine_threadsafe(telegram_client.send_message(user_id, message), loop) + +def send_telegram_audio_sync(user_id, audio_bytes): + asyncio.run_coroutine_threadsafe(telegram_client.send_file(user_id, audio_bytes, voice_note=True), loop) + +def edit_telegram_message_sync(chat_id, message_id, new_text): + asyncio.run_coroutine_threadsafe(telegram_client.edit_message(chat_id,message_id, new_text), loop) + +async def edit_telegram_message(chat_id, message_id, new_text): + await telegram_client.edit_message(chat_id, message_id, new_text) + +# Evento para manejar nuevos mensajes en Telegram +@telegram_client.on(events.NewMessage) +async def handle_new_message(event): + glados_mqtt.debug(event.stringify()) + + # Enviar respuesta inicial "Procesando..." + response = await telegram_client.send_message(event.chat_id, "Procesando...") + # Intenta obtener el nombre completo del usuario + sender_name = "Desconocido" + if event.sender: + # Si el evento tiene un enviador, construye el nombre completo si es posible + sender_name = event.sender.first_name or "" + if event.sender.last_name: + sender_name += f" {event.sender.last_name}" + + # Procesamiento de mensajes de voz o audio + if event.message.voice or event.message.audio: + # Descargar el archivo de audio + audio_file_path = await telegram_client.download_media(event.message, file='/tmp/audio/') + await edit_telegram_message(event.chat_id,response.id, "Whisper...") + # Procesar el archivo de audio + #mas adelante pensar en agregar un campo para especificar que es una transcripcion para poder pasarle esa info al LLM + transcription = await send_audio_to_transcription_api(audio_file_path) + event_data = { + 'message_text': transcription, + 'sender_id': event.sender_id, + 'channel_id': event.chat_id, + 'reply_msg_id' : response.id, + 'sender_name': sender_name + # Agrega aquí otros campos relevantes + } + await edit_telegram_message(event.chat_id,response.id, "LLM...") + glados_mqtt.publish(topic_telegram_event, json.dumps(event_data)) + return + + # Procesamiento de mensajes de texto + if event.is_private and event.sender_id != 771352834: # Asegúrate de actualizar este ID según sea necesario + event_data = { + 'message_text': event.message.message if event.message else None, + 'sender_id': event.sender_id, + 'channel_id': event.chat_id, + 'reply_msg_id' : response.id, + 'sender_name': sender_name + # Agrega aquí otros campos relevantes + } + await edit_telegram_message(event.chat_id,response.id, "LLM...") + glados_mqtt.publish(topic_telegram_event, json.dumps(event_data)) + + + +# Iniciar la instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName, msg_callback=on_mqtt_message) +glados_mqtt.set_topics([topic_telegram_send_msg,topic_telegram_edit_msg]) + + +if __name__ == "__main__": + glados_mqtt.init_mqtt() + telegram_client.start(bot_token=telegram_token) + loop = asyncio.get_event_loop() + loop.run_until_complete(telegram_client.run_until_disconnected()) \ No newline at end of file diff --git a/Bots/TelegramBot/requirements.txt b/Bots/TelegramBot/requirements.txt new file mode 100644 index 0000000..c1af436 --- /dev/null +++ b/Bots/TelegramBot/requirements.txt @@ -0,0 +1,3 @@ +paho-mqtt +telethon +requests \ No newline at end of file diff --git a/Bots/dev1/Dockerfile b/Bots/dev1/Dockerfile deleted file mode 100644 index 00ebf6d..0000000 --- a/Bots/dev1/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# Usar una imagen base de Python 3 -FROM python:3.9-slim - -# Establecer una carpeta de trabajo en el contenedor -WORKDIR /app - -# Copiar el archivo de requisitos y instalar dependencias -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copiar el resto del código del bot al contenedor -COPY . . - -# Comando por defecto para ejecutar el bot -CMD ["python", "main.py"] diff --git a/Bots/dev1/GladosDiscordClient.py b/Bots/dev1/GladosDiscordClient.py deleted file mode 100644 index fdc1aeb..0000000 --- a/Bots/dev1/GladosDiscordClient.py +++ /dev/null @@ -1,61 +0,0 @@ -# DISCORDD -import os -import discord -from discord.ext import commands - - -class GladosDiscordClient: - def __init__(self): - self.discord_token = os.environ.get("DISCORD_TOKEN") - self.discordbot_prefix = "!" - - self.discordintents = discord.Intents.default() - self.discordintents.messages = True - self.discordintents.members = True - self.discordintents.message_content = True - - self.bot = commands.Bot( - command_prefix=self.discordbot_prefix, intents=self.discordintents) - self._setup_bot() - self.msgBuffer = [] - - def _setup_bot(self): - @self.bot.event - async def on_ready(): - print( - f'Bot conectado como {self.bot.user.name} - {self.bot.user.id}') - - @self.bot.command(name='hola') - async def hello(ctx): - await ctx.send(f"Hola, {ctx.message.author.name}!") - - @self.bot.event - async def on_message(message): - if message.author == self.bot.user: - return - - # Reaccionar a menciones - if self.bot.user in message.mentions: - self.msgBuffer.append(message.content) - - # Reaccionar a DMs - if isinstance(message.channel, discord.DMChannel): - self.msgBuffer.append(message.content) - - # Nota: Si tienes comandos, necesitas procesarlos también. - await self.bot.process_commands(message) - - def run(self): - print("Connecting Discord...") - self.bot.run(self.discord_token) - - async def sendMsg(self, msg, channel="general"): - print("Looking discord channel: "+channel) - print(self.bot.guilds[0].text_channels) - ch = discord.utils.get(self.bot.guilds[0].text_channels, name=channel) - await ch.send(msg) - - def getMsgs(self): - result = self.msgBuffer - self.msgBuffer = [] - return result diff --git a/Bots/dev1/GladosIA.py b/Bots/dev1/GladosIA.py deleted file mode 100644 index 2df71ef..0000000 --- a/Bots/dev1/GladosIA.py +++ /dev/null @@ -1,18 +0,0 @@ -import llm -import GladosDiscordClient -import GladosSlackClient -import GladosTelegramClient - -# telegramClient -# discordClient -# slackClient - - -class GladosBot: - def __init__(self): - self.GLaDOS_Prompt = {"role": "system", "content": "Eres un asistente con la personalidad de GLaDOS, el robot de las instalaciones de Aperture que ahora está retirado y gestiona las instalaciones de MakeSpace Madrid. Ayudas a los usuarios respondiendo como GLaDOS"} - self.Initial_Assistant = { - "role": "assistant", "content": "Hola humano, soy GLaDOS ¿Con que extraña peticion me vas a molestar hoy?"} - - def askGLaDOS(self, prompt, user="default"): - return llm.chatCompletion(prompt, masterPrompt=self.GLaDOS_Prompt, initialAssistant=self.Initial_Assistant).choices[0].message.content diff --git a/Bots/dev1/GladosMQTT.py b/Bots/dev1/GladosMQTT.py deleted file mode 100644 index e71abf1..0000000 --- a/Bots/dev1/GladosMQTT.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import paho.mqtt.client as mqtt -import time -import os -import platform -import psutil -from threading import Timer -import json - - -# Carga inicial de variables, aunque el valor de estas se reescribira al ejecutar initMQTT -mqttServer = "192.168.1.1" -mqttPort = 1883 -nodeName = "node" -baseTopic = "/node/" -debugTopic = "debug" -commandTopic = "cmnd" -globalCommandTopic = "" -globalShutdown = False -mqttClient = mqtt.Client() - - -def dummy(): - debug(dummy) - - -nodeConnectedCallback = dummy -nodeMsgCallback = dummy -nodeDisconnectedCallback = dummy - - -def subscribe(topic): - mqttClient.subscribe(topic) - - -def subscribeTopics(): - subscribe(globalCommandTopic) - subscribe(commandTopic) - - -def publish(topic, msg, persist=False): - mqttClient.publish(topic, msg, persist) - - -def debug(msg): - print(msg) - publish(debugTopic, msg) - - -def on_connect(client, userdata, rc, arg): - debug("[GladosNode] Connected with result code "+str(rc)) - debug("[GladosNode] Node name : " + nodeName + - " ,Global Shutdown : " + str(globalShutdown)) - debug("[GladosNode] Base topic : " + baseTopic) - debug("[GladosNode] Command topic : " + commandTopic) - debug("[GladosNode] Global command : " + globalCommandTopic) - debug("[GladosNode] Debug topic : " + debugTopic) - debug("[GladosNode] mosquitto_sub -h " + mqttServer + " -t " + debugTopic) - - procType = str(platform.machine()) - osType = str(platform.system()) - publish("node/hello", "Hello! Im " + nodeName + - ", Architecture : "+procType+" OS: "+osType) - subscribeTopics() - publishSystemInfo() - nodeConnectedCallback(client, userdata, rc, arg) - - -def processGlobalCMD(cmd): - if cmd == "poweroff": - powerOffSystem() - else: - return False - return True - - -def processCMD(cmd): - if cmd == "poweroff": - powerOffSystem() - else: - return False - return True - -# The callback for when a PUBLISH message is received from the server. - - -def on_message(client, userdata, msg): - debug("[GladosNode] mqtt_rcv: { "+msg.topic + " - " + msg.payload + " }") - - if (msg.topic == globalCommandTopic): - if not processGlobalCMD(msg.payload): - nodeMsgCallback(client, userdata, msg) - elif (msg.topic == commandTopic): - if not processCMD(msg.payload): - nodeMsgCallback(client, userdata, msg) - else: - nodeMsgCallback(client, userdata, msg) - - -def on_disconnect(client, userdata, rc): - nodeDisconnectedCallback(client, userdata, rc) - - -def initMQTT(host, port, name, connectedCallback, msgCallback, disconnectedCallback, globalCMDTopic='cmnd'): - global mqttServer - global mqttPort - global nodeName - global baseTopic - global debugTopic - global commandTopic - global globalCommandTopic - global mqttClient - - mqttServer = host - mqttPort = port - nodeName = name - baseTopic = "/node/"+nodeName+"/" - debugTopic = baseTopic+"debug" - commandTopic = baseTopic+"cmnd" - globalCommandTopic = globalCMDTopic - - debug("[GladosNode] Connecting : "+mqttServer+" Port:"+str(mqttPort)) - - nodeConnectedCallback = connectedCallback - nodeMsgCallback = msgCallback - nodeDisconnectedCallback = disconnectedCallback - - mqttClient.on_connect = on_connect - mqttClient.on_message = on_message - mqttClient.on_disconnect = on_disconnect - - try: - mqttClient.connect(mqttServer, mqttPort, 60) - except: - print("Cant connect, will retry automatically") - mqttClient.loop_start() - - -def publishSystemInfo(): - mqttClient.publish("node/"+nodeName+"/system/cpu", platform.machine()) - mqttClient.publish("node/"+nodeName+"/system/os", platform.system()) - - -def publishSystemStats(): - try: - mqttClient.publish("node/"+nodeName+"/system/cpuUsage", - psutil.cpu_percent(interval=1)) - except: - pass - try: - mqttClient.publish("node/"+nodeName+"/system/temperatures", - str(psutil.sensors_temperatures())) - except: - pass - try: - mqttClient.publish("node/"+nodeName+"/system/diskUsage", - str(psutil.disk_usage('/')[3])) - except: - pass - - -def powerOffSystem(): - publish(baseNode+"/status", "poweroff") - if (platform.system() == "Linux"): - debug("Shutting down (Linux style)!") - os.system('sudo systemctl poweroff -i') # Linux - else: - debug("Shutting down (M$ style)!") - os.system('shutdown -s') # Windows diff --git a/Bots/dev1/GladosSlackClient.py b/Bots/dev1/GladosSlackClient.py deleted file mode 100644 index d812006..0000000 --- a/Bots/dev1/GladosSlackClient.py +++ /dev/null @@ -1,38 +0,0 @@ -# SLACK Bot -import os -from flask import Flask, request, jsonify -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - - -def run_bot(askGLaDOS): - print("Connecting Slack...") - # Inicializar cliente de Slack con tu token - app = Flask(__name__) - slack_token = os.environ.get("SLACK_API_TOKEN") - slack_client = WebClient(token=slack_token) - - @app.route('/slack/events', methods=['POST']) - def slack_events(): - data = request.json - # Desafío de verificación de Slack - if data.get('type') == 'url_verification': - return jsonify({'challenge': data.get('challenge')}) - - if data['type'] == 'event_callback': - event = data['event'] - print("======SLACK EVENT=====") - print(event) - print("======-----------=====") - if 'bot_id' not in event and (event['type'] == 'message'): - try: - response = slack_client.chat_postMessage( - channel=event['channel'], - text=askGLaDOS(event['text']) - ) - except SlackApiError as e: - print(f"Error sending message: {e.response['error']}") - return jsonify({'status': 'ok'}), 200 - -# response = slack_client.chat_postMessage(channel='#glados_testing', text="Hola, mundo!") - app.run(host='0.0.0.0', port=3000) diff --git a/Bots/dev1/GladosTelegramClient.py b/Bots/dev1/GladosTelegramClient.py deleted file mode 100644 index 29c17cc..0000000 --- a/Bots/dev1/GladosTelegramClient.py +++ /dev/null @@ -1,38 +0,0 @@ -# Telegram BOT -import os -import asyncio -from telethon import TelegramClient, events - -import GladosIA - - -def run_bot(askGLaDOS): - - tg_api_id = os.environ.get("TELEGRAM_ID") - tg_api_hash = os.environ.get("TELEGRAM_HASH") - tg_bot_token = os.environ.get("TELEGRAM_TOKEN") - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - tg_client = TelegramClient('./sess_id', tg_api_id, tg_api_hash, loop=loop) - - @tg_client.on(events.NewMessage()) - async def readMessages(event): - user = await tg_client.get_entity(event.peer_id.user_id) - id = user.id - name = user.first_name - lastName = user.last_name - username = user.username - print(event.message) - # then we build the message we want to respond: - # respondMessage = "Message sender: %s @%s, with id: %s"%( name + ' ' + lastName if lastName is not None else name, username, id ) - respondMessage = askGLaDOS(event.message.message) - # then we send the message back replying the recibed message - await event.reply(respondMessage) - - try: - tg_client.start(bot_token=tg_bot_token) - print('Telegram bot started.') - loop.run_until_complete(tg_client.run_until_disconnected()) - finally: - tg_client.disconnect() diff --git a/Bots/dev1/Makefile b/Bots/dev1/Makefile deleted file mode 100644 index 9917a71..0000000 --- a/Bots/dev1/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -format: - # Applies pep8 style on all files. - # - Set the right space indentation on all files. - # - Set the correct order of imports on all files. - autopep8 --recursive --in-place . diff --git a/Bots/dev1/docker-compose.yml b/Bots/dev1/docker-compose.yml deleted file mode 100644 index 1c2bddc..0000000 --- a/Bots/dev1/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '3' - -services: - gladosbot: - build: - context: . - dockerfile: Dockerfile - env_file: - - .env - volumes: - - ./:/app diff --git a/Bots/dev1/llm.py b/Bots/dev1/llm.py deleted file mode 100644 index 1c54dca..0000000 --- a/Bots/dev1/llm.py +++ /dev/null @@ -1,61 +0,0 @@ -import openai -import gladosMQTT - -openai_api_key = "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" -openai_api_base = "https://apicuna.mapache.xyz/v1" -custom_api_key = "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" -custom_api_base = "https://apicuna.mapache.xyz/v1" - - -openai.api_key = custom_api_key -openai.api_base = custom_api_base - -current_model = "none" -default_prompt = "Eres un asistente que ayuda a los usuarios dando respuestas concisas y breves" - - -def set_llm_api(url, key): - openai.api_key = key - openai.api_base = url - select_model() - - -def select_model(): - model_list = openai.Model.list() - global current_model - current_model = model_list["data"][0]["id"] -# print(model_list["data"]) - gladosMQTT.debug(f"Selected model: {current_model}") - return model_list - - -def chatCompletion(prompt="", chatHistory="", masterPrompt="", initialAssistant="", maxTokens=256, langChainContext='none'): - # TODO: Identify and remove globals. - global current_model - select_model() - result = '' - - if (chatHistory == ''): - if (masterPrompt == ''): - masterPrompt = {"role": "system", "content": default_prompt} - if (initialAssistant != ''): - result = [masterPrompt, initialAssistant, - {"role": "user", "content": prompt}] - else: - result = [masterPrompt, {"role": "user", "content": prompt}] - else: - result = '[' - for msg in chatHistory: - print(msg, flush=True) - result += str(msg) - result += ',' - result += str({"role": "user", "content": prompt}) - result += ']' - gladosMQTT.debug("==============Lanzando peticion al LLM=======================") - gladosMQTT.debug(result) - - completion = openai.ChatCompletion.create( - model=current_model, messages=result, max_tokens=maxTokens) - gladosMQTT.debug("========LLM OUTPUT===============") - gladosMQTT.debug(completion) - return completion diff --git a/Bots/dev1/main.py b/Bots/dev1/main.py deleted file mode 100644 index 5b5fe23..0000000 --- a/Bots/dev1/main.py +++ /dev/null @@ -1,19 +0,0 @@ -import moab -import time -import asyncio - -print("start!") - -try: - moab.run_bots() -except KeyboardInterrupt: - print("Bot is stopping...") -print('Successfully started (Press Ctrl+C to stop)') - -while True: - try: - moab.process_msgs() - time.sleep(0.33) - - except KeyboardInterrupt: - print("Bot is stopping...") diff --git a/Bots/dev1/moab.py b/Bots/dev1/moab.py deleted file mode 100644 index ab7a3db..0000000 --- a/Bots/dev1/moab.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import threading - -import GladosIA -import GladosTelegramClient -import GladosDiscordClient -import GladosSlackClient -import GladosMQTT - -import platform -import GladosMQTT -import time - -# Variables -mqHost = "10.0.0.20" -mqPort = 1883 -nodeName = platform.node() -globalCMDTopic = "space/cmnd" - -glados_bot = GladosIA.GladosBot() -discord_bot = GladosDiscordClient.GladosDiscordClient() - - -def subscribeTopics(): - gladosMQTT.subscribe("node") - - -def on_connect(client, userdata, rc, arg): - subscribeTopics() - - -def on_message(client, userdata, msg): - if (msg.topic == commandTopic): - debug("cmd:"+msg) - - -def on_disconnect(client, userdata, rc): - debug("Disconnected! rc: "+str(rc)) - - -def run_bots(): - global mqHost, mqPort, nodeName, on_connect, on_message, on_disconnect, globalCMDTopic - GladosMQTT.initMQTT(mqHost, mqPort, nodeName, on_connect, - on_message, on_disconnect, globalCMDTopic) - -# GladosDiscordClient.run_bot(GladosIA.askGLaDOS) -# GladosSlackClient.run_bot(GladosIA.askGLaDOS) -# tg_t = threading.Thread(target= GladosTelegramClient.run_bot, args=(GladosIA.askGLaDOS,)) - dc_t = threading.Thread(target=discord_bot.run, args=()) -# sl_t = threading.Thread(target= GladosSlackClient.run_bot , args=(GladosIA.askGLaDOS,)) - -# tg_t.start() - dc_t.start() -# sl_t.start() - -# tg_t.join() -# dc_t.join() - - -async def process_msgs(): - pending = discord_bot.getMsgs() - for msg in pending: - response = glados_bot.askGLaDOS(msg) - await discord_bot.sendMsg(response) diff --git a/Bots/dev1/requirements.txt b/Bots/dev1/requirements.txt deleted file mode 100644 index 4bc9908..0000000 --- a/Bots/dev1/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -openai -paho-mqtt -psutil -telethon -slack-sdk -Flask -discord.py -autopep8 diff --git a/Bots/spaceAPI/Dockerfile b/Bots/spaceAPI/Dockerfile index ec39971..287ffbe 100644 --- a/Bots/spaceAPI/Dockerfile +++ b/Bots/spaceAPI/Dockerfile @@ -1,5 +1,5 @@ # Utiliza una imagen base que ya tiene el cliente mqtt -FROM makespacemadrid/glados-base-node:1.0 +FROM makespacemadrid/glados-base-node:1.5 # Establece el directorio de trabajo en /app WORKDIR /app @@ -10,11 +10,11 @@ VOLUME /spaceapi COPY requirements.txt . # Copia el archivo node.py al contenedor -COPY node.py . +COPY main.py . # Instala las dependencias RUN pip install -r requirements.txt USER 1000 # Ejecuta el programa cuando el contenedor se inicie -CMD ["python", "node.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Bots/spaceAPI/docker-compose.yml b/Bots/spaceAPI/docker-compose.yml index 2c48919..8ef47ee 100644 --- a/Bots/spaceAPI/docker-compose.yml +++ b/Bots/spaceAPI/docker-compose.yml @@ -1,14 +1,14 @@ version: '3' services: - mqtt-spaceapi: + glados-spaceapi: build: . image: makespacemadrid/glados-mqtt-spaceapi:latest hostname: mqtt-spaceapi volumes: - ./spaceapi:/spaceapi environment: - - MQTT_PORT = 1883 - - MQTT_HOST = glados.makespacemadrid.org + - MQTT_PORT=1883 + - MQTT_HOST=mqtt.makespacemadrid.org restart: always nginx-spaceapi: diff --git a/Bots/spaceAPI/main.py b/Bots/spaceAPI/main.py new file mode 100644 index 0000000..ca4de57 --- /dev/null +++ b/Bots/spaceAPI/main.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import os +import platform +import json +from gladosMQTT import GladosMQTT # Asegúrate de que el import sea correcto + +# Variables +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Tema MQTT +topic_space_status = "space/status" + +# Definición de los callbacks para MQTT +def on_mqtt_message(client, userdata, msg): + if msg.topic == topic_space_status: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + open_status = data['state']['open'] + if open_status: + glados_mqtt.debug("open!") + else: + glados_mqtt.debug("closed!") + with open('/spaceapi/status.json', 'w') as file: + file.write(payload) + print("Recibido:", payload) + except json.JSONDecodeError as e: + print("Error al parsear JSON:", e) + +# Iniciar la instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName, msg_callback=on_mqtt_message) +glados_mqtt.set_topics([topic_space_status]) + +# Ejecución principal +if __name__ == "__main__": + glados_mqtt.init_mqtt_and_loop_forever() diff --git a/Bots/tts/Dockerfile b/Bots/tts/Dockerfile new file mode 100644 index 0000000..500cca1 --- /dev/null +++ b/Bots/tts/Dockerfile @@ -0,0 +1,19 @@ +FROM ghcr.io/coqui-ai/tts + +# Establece el directorio de trabajo en /app +WORKDIR /app +# Copia el archivo de requerimientos al contenedor +COPY requirements.txt . + + +# Instala las dependencias +RUN pip install -r requirements.txt + +# Copia el archivo node.py al contenedor +COPY main.py . + + +# Ejecuta el programa cuando el contenedor se inicie +ENTRYPOINT [""] + +CMD ["python3", "/app/main.py"] \ No newline at end of file diff --git a/Bots/tts/docker-compose.yml b/Bots/tts/docker-compose.yml new file mode 100644 index 0000000..e2dd972 --- /dev/null +++ b/Bots/tts/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3' + + +services: + glados-tts: + build: . + image: makespacemadrid/glados-tts:latest + hostname: glados-tts + env_file: + - .env + volumes: + - ./data:/data + - ./tts_models:/root/.local/share/tts/ + restart: always + ports: + - 9995:9995 + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0'] +# count: all + capabilities: [gpu] \ No newline at end of file diff --git a/Bots/tts/main.py b/Bots/tts/main.py new file mode 100644 index 0000000..f1bd25f --- /dev/null +++ b/Bots/tts/main.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +import os +import platform +import json +#from gladosMQTT import GladosMQTT +from flask import Flask, request, jsonify, send_file +import torch +from TTS.api import TTS +from TTS.utils.synthesizer import Synthesizer +from io import BytesIO +import soundfile as sf +import tempfile + +# Configuración +#mqHost = os.environ.get("MQTT_HOST") +#mqPort = int(os.environ.get("MQTT_PORT")) +#nodeName = platform.node() + +tts_port = os.environ.get("TTS_PORT") +tts_model = os.environ.get("TTS_MODEL") + +# Temas MQTT +#topic_spaceapi = "space/status" +#topic_report_space_open = "space/report_open" + + +# Instancia de GladosMQTT +#glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +#glados_mqtt.set_topics([topic_spaceapi, topic_last_open_status, topic_slack_send_msg_id, topic_slack_send_msg_name,topic_slack_edit_msg]) + + +# Get device +device = "cuda" if torch.cuda.is_available() else "cpu" +# List available 🐸TTS models +print(TTS().list_models()) +# Init TTS +tts = TTS(tts_model).to(device) + +# Carga el modelo y el vocoder +model_path = "/data/glados.pth" # Actualiza esto con la ruta de tu modelo +config_path = "/data/glados_tts.json" # Actualiza esto con la ruta de tu configuración +synthesizer = Synthesizer(model_path, config_path) + +app = Flask(__name__) + +@app.route('/synthesize', methods=['POST']) +def synthesize(): + data = request.json + text = data.get('text', '') + language = data.get('language', '') + + if not text: + return "No text provided", 400 + + if not language: + language = 'es' +# wav = tts.tts(text=text, speaker_wav='/data/glados.wav', language=language) + wav = synthesizer.tts(text) + # Guarda el archivo de audio temporalmente + with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp: + sf.write(tmp, wav, 22050) # Asumiendo que la frecuencia de muestreo es 22050 Hz + tmp_path = tmp.name # Guarda la ruta del archivo temporal + + # Envía el archivo como respuesta + response = send_file(tmp_path, as_attachment=True, download_name="glados_response.wav", mimetype='audio/wav') + + # Elimina el archivo temporal después de enviarlo + os.remove(tmp_path) + + return response + + +if __name__ == "__main__": +# glados_mqtt.init_mqtt() + app.run(host='0.0.0.0', port=tts_port,debug=False) \ No newline at end of file diff --git a/Bots/tts/requirements.txt b/Bots/tts/requirements.txt new file mode 100644 index 0000000..fd1ea0d --- /dev/null +++ b/Bots/tts/requirements.txt @@ -0,0 +1,3 @@ +paho-mqtt +flask +TTS \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..2da31d7 --- /dev/null +++ b/test.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +import os +import json +import platform +import discord +from discord.ext import commands +from gladosMQTT import GladosMQTT + +# Configuración de Discord +discord_token = os.environ.get("DISCORD_BOT_TOKEN") +discordbot_prefix = "!" +discordintents = discord.Intents.default() +discordintents.messages = True +discordintents.members = True +discordintents.message_content = True +bot = commands.Bot(command_prefix=discordbot_prefix, intents=discordintents) + +# Configuración MQTT +mqHost = os.environ.get("MQTT_HOST") +mqPort = int(os.environ.get("MQTT_PORT")) +nodeName = platform.node() + +# Temas MQTT +topic_discord = "comms/discord" +topic_discord_send_msg = topic_discord + "/send_id" +topic_discord_event = topic_discord + "/event" + +# Instancia de GladosMQTT +glados_mqtt = GladosMQTT(host=mqHost, port=mqPort, name=nodeName) +glados_mqtt.set_topics([topic_discord_send_msg]) + +# Función para manejar mensajes MQTT +def on_mqtt_message(client, userdata, msg): + if msg.topic == topic_discord_send_msg: + try: + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + sendDiscordMsg(data['dest'], data['msg']) + except json.JSONDecodeError as e: + print("Error al parsear JSON:", e) + +# Configurar callbacks MQTT +glados_mqtt.mqttClient.on_message = on_mqtt_message + +# Función para enviar mensajes a Discord +async def sendDiscordMsg(channel_id, msg): + channel = bot.get_channel(channel_id) + if channel: + await channel.send(msg) + +# Eventos de Discord +@bot.event +async def on_ready(): + print(f'Bot conectado como {bot.user.name} - {bot.user.id}') + +@bot.event +async def on_message(message): + if message.author == bot.user: + return + message_data = { + 'content': message.content, + 'author_id': message.author.id, + 'author_name': str(message.author), + 'channel_id': message.channel.id, + 'channel_name': str(message.channel), + 'mentions': str(message.mentions) + } + glados_mqtt.publish(topic_discord_event, json.dumps(message_data)) + #await message.channel.send('Recibí tu mensaje: ' + message.content) # Respuesta de ejemplo + +if __name__ == "__main__": + glados_mqtt.init_mqtt() + bot.run(discord_token)