Spaces:
Running
Running
| import os | |
| import uvicorn | |
| import httpx | |
| import orjson | |
| import numpy as np | |
| import faiss | |
| import chromadb | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| import datetime | |
| import subprocess | |
| import threading | |
| import asyncio | |
| from typing import Optional, List, Dict, Any | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, Request, UploadFile, File, HTTPException, Depends | |
| from fastapi.responses import ORJSONResponse, FileResponse | |
| from pydantic import BaseModel | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from huggingface_hub import InferenceClient, hf_hub_download, upload_file | |
| from loguru import logger | |
| from apscheduler.schedulers.background import BackgroundScheduler | |
| from sqlalchemy import create_engine, Column, String, Text, Integer, Float, ForeignKey, text | |
| from sqlalchemy.ext.declarative import declarative_base | |
| from sqlalchemy.orm import sessionmaker, Session, relationship | |
| from pypdf import PdfReader | |
| from ddgs import DDGS | |
| import pandas as pd | |
| import docx | |
| import pytesseract | |
| from PIL import Image | |
| import io | |
| import requests | |
| import urllib.parse | |
| import xml.etree.ElementTree as ET | |
| import uuid | |
| from qdrant_client import AsyncQdrantClient | |
| from qdrant_client.http import models as qmodels | |
| import sys | |
| # ===================================================================== | |
| # 🛠️ AUTO-REPARACIÓN DE DEPENDENCIAS Y NUEVOS MÓDULOS (IoT, Biometría) | |
| # ===================================================================== | |
| def _ensure_industrial_dependencies(): | |
| dependencies = { | |
| "lingua": "lingua-language-detector", | |
| "faster_whisper": "faster-whisper", | |
| "qdrant_client": "qdrant-client", | |
| "paho.mqtt.client": "paho-mqtt", # [UPGRADE]: IoT MQTT | |
| "cv2": "opencv-python-headless", # [UPGRADE]: Computer Vision | |
| "face_recognition": "face-recognition" # [UPGRADE]: Face Memory | |
| } | |
| for module_name, package_name in dependencies.items(): | |
| try: | |
| __import__(module_name) | |
| except ImportError: | |
| logger.warning(f"⚠️ Inyectando en caliente '{package_name}'...") | |
| try: | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet", package_name]) | |
| except Exception as e: | |
| logger.error(f"❌ Error al inyectar {package_name} (Si es biometría, requiere C++): {e}") | |
| _ensure_industrial_dependencies() | |
| try: from lingua import Language, LanguageDetectorBuilder | |
| except ImportError: Language, LanguageDetectorBuilder = None, None | |
| try: import paho.mqtt.client as mqtt | |
| except ImportError: mqtt = None | |
| try: | |
| import cv2 | |
| import face_recognition | |
| except ImportError: | |
| cv2, face_recognition = None, None | |
| # ===================================================================== | |
| # 📡 0.1 INFRAESTRUCTURA IoT (MQTT) | |
| # ===================================================================== | |
| mqtt_client = None | |
| MQTT_BROKER = os.getenv("MQTT_BROKER", "broker.hivemq.com") # Broker público por defecto para pruebas | |
| MQTT_PORT = 1883 | |
| def start_mqtt_infrastructure(): | |
| """[UPGRADE INDUSTRIAL]: Sistema nervioso para domótica y sensores.""" | |
| global mqtt_client | |
| if not mqtt: return | |
| try: | |
| mqtt_client = mqtt.Client(client_id=f"LuminAGI_{uuid.uuid4().hex[:8]}") | |
| def on_message(client, userdata, msg): | |
| payload = msg.payload.decode() | |
| logger.info(f"📡 [MQTT IN]: Tema {msg.topic} -> {payload}") | |
| # Guardamos el evento IoT en la memoria episódica automáticamente | |
| db = SessionLocal() | |
| try: | |
| db.add(EpisodicMemory(timestamp=str(datetime.datetime.now()), autor="IoT_Sensor", texto=f"Alerta IoT en {msg.topic}: {payload}", modo="background")) | |
| db.commit() | |
| except Exception: db.rollback() | |
| finally: db.close() | |
| mqtt_client.on_message = on_message | |
| mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60) | |
| mqtt_client.subscribe("lumin_agi/sensores/#") | |
| mqtt_client.loop_start() | |
| logger.info("🟢 [MQTT]: Sistema IoT de mensajería conectado.") | |
| except Exception as e: | |
| logger.warning(f"⚠️ [MQTT]: No se pudo conectar al broker IoT: {e}") | |
| # ===================================================================== | |
| # 🌐 CONFIGURACIÓN DE APIS Y BASES DE DATOS EXTERNAS | |
| # ===================================================================== | |
| QDRANT_URL = os.getenv("QDRANT_URL", "https://tu-cluster-qdrant.cloud") | |
| QDRANT_API_KEY = os.getenv("QDRANT_API_KEY", "") | |
| qdrant_client_async = None | |
| async def init_qdrant_collections(): | |
| global qdrant_client_async | |
| try: | |
| if "tu-cluster-qdrant.cloud" in QDRANT_URL or not QDRANT_API_KEY: | |
| qdrant_client_async = AsyncQdrantClient(path="./qdrant_local_storage") | |
| else: | |
| qdrant_client_async = AsyncQdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY, timeout=10.0) | |
| vector_size = 384 | |
| colecciones = [ | |
| ("episodic_memories", 384), ("semantic_memories", 384), | |
| ("procedural_memories", 384), ("working_memory", 384), | |
| ("user_profile", 384), ("skills", 384), ("goals", 384), | |
| ("face_memory", 128) # [UPGRADE]: Colección Biométrica Facial de 128 dimensiones | |
| ] | |
| for col_name, size in colecciones: | |
| exists = await qdrant_client_async.collection_exists(col_name) | |
| if not exists: | |
| await qdrant_client_async.create_collection( | |
| collection_name=col_name, | |
| vectors_config=qmodels.VectorParams(size=size, distance=qmodels.Distance.COSINE), | |
| optimizers_config=qmodels.OptimizersConfigDiff(default_segment_number=2), | |
| on_disk_payload=True | |
| ) | |
| logger.info("🟢 [QDRANT]: Motor Vectorial (Texto + Biometría Facial) Conectado.") | |
| except Exception as e: | |
| logger.error(f"❌ [QDRANT]: Fallo en inicialización de vectores: {e}") | |
| TAVILY_API_KEY, EXA_API_KEY, SERPER_API_KEY, BRAVE_API_KEY = os.getenv("TAVILY_API_KEY"), os.getenv("EXA_API_KEY"), os.getenv("SERPER_API_KEY"), os.getenv("BRAVE_API_KEY") | |
| NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD = os.getenv("NEO4J_URI", "bolt://localhost:7687"), os.getenv("NEO4J_USER", "neo4j"), os.getenv("NEO4J_PASSWORD", "password") | |
| _neo4j_driver = None | |
| def get_neo4j_driver(): | |
| global _neo4j_driver | |
| if _neo4j_driver is None: | |
| try: | |
| from neo4j import GraphDatabase | |
| _neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD)) | |
| with _neo4j_driver.session() as s: s.run("RETURN 1") | |
| except Exception: _neo4j_driver = False | |
| return _neo4j_driver if _neo4j_driver else None | |
| # ===================================================================== | |
| # 🔒 0. CONCURRENCIA Y PRECARGA DE MODELOS | |
| # ===================================================================== | |
| memoria_lock = threading.Lock() | |
| extractor_embeddings, faster_whisper_model, emotion_classifier = None, None, None | |
| def get_embeddings(): | |
| global extractor_embeddings | |
| if extractor_embeddings is None: | |
| from sentence_transformers import SentenceTransformer | |
| extractor_embeddings = SentenceTransformer('all-MiniLM-L6-v2') | |
| return extractor_embeddings | |
| def get_emotion_classifier(): | |
| global emotion_classifier | |
| if emotion_classifier is None: | |
| from transformers import pipeline | |
| emotion_classifier = pipeline("text-classification", model="pysentimiento/robertuito-emotion-analysis", top_k=1) | |
| return emotion_classifier | |
| _language_detector = None | |
| def get_language_detector(): | |
| global _language_detector | |
| if _language_detector is None and LanguageDetectorBuilder is not None: | |
| languages = [Language.SPANISH, Language.ENGLISH, Language.PORTUGUESE, Language.FRENCH, Language.GERMAN, Language.ITALIAN, Language.INDONESIAN] | |
| _language_detector = LanguageDetectorBuilder.from_languages(*languages).build() | |
| return _language_detector | |
| class LanguageManager: | |
| def detect(text: str) -> str: | |
| if LanguageDetectorBuilder is None: return "SPANISH" | |
| try: | |
| return get_language_detector().detect_language_of(text).name or "SPANISH" | |
| except Exception: return "SPANISH" | |
| # ===================================================================== | |
| # 🗄️ 1. INFRAESTRUCTURA BASE DE DATOS SQL | |
| # ===================================================================== | |
| DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./lumin.db") | |
| def init_db_engine(): | |
| try: | |
| if "sqlite" not in DATABASE_URL: | |
| engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_size=10, max_overflow=20) | |
| with engine.connect() as conn: conn.execute(text("SELECT 1")) | |
| return engine | |
| else: raise Exception("Forzar SQLite") | |
| except Exception: return create_engine("sqlite:///./lumin.db", connect_args={"check_same_thread": False}) | |
| engine = init_db_engine() | |
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| Base = declarative_base() | |
| def get_db(): | |
| db = SessionLocal() | |
| try: yield db | |
| finally: db.close() | |
| class IdentitySystem(Base): __tablename__ = "identity_system"; id = Column(Integer, primary_key=True); nombre = Column(String(50), default="Lumin AGI"); core_directive = Column(Text, default="Asistir, aprender y evolucionar sin causar daño."); restricciones = Column(Text, default="No gastar dinero, proteger la privacidad de Chichito.") | |
| class WorldModel(Base): __tablename__ = "world_model"; id = Column(Integer, primary_key=True); entidad = Column(String(100)); estado = Column(Text); ultima_actualizacion = Column(String(50)) | |
| class EpisodicMemory(Base): __tablename__ = "episodic_memory"; id = Column(Integer, primary_key=True, index=True); timestamp = Column(String(50)); autor = Column(String(50)); texto = Column(Text); modo = Column(String(50)) | |
| class ConversationSummary(Base): __tablename__ = "conversation_summary"; id = Column(Integer, primary_key=True); resumen = Column(Text); fecha = Column(String(50)) | |
| class ProceduralMemory(Base): __tablename__ = "procedural_memory"; id = Column(Integer, primary_key=True); habilidad = Column(String(100)); pasos = Column(Text); tasa_exito = Column(Float, default=1.0) | |
| class Mission(Base): __tablename__ = "mission"; id = Column(Integer, primary_key=True); titulo = Column(String(200)); descripcion = Column(Text); activa = Column(Integer, default=1) | |
| class Goal(Base): __tablename__ = "goal"; id = Column(Integer, primary_key=True); mission_id = Column(Integer, ForeignKey('mission.id'), nullable=True); parent_id = Column(Integer, ForeignKey('goal.id'), nullable=True); titulo = Column(String(200)); descripcion = Column(Text); prioridad = Column(String(50), default="Media"); estado = Column(String(50), default="En progreso"); progreso = Column(Integer, default=0); critica_interna = Column(Text, nullable=True); sub_metas = relationship("Goal", backref="parent", remote_side=[id]) | |
| class ReflectionLog(Base): __tablename__ = "reflection_log"; id = Column(Integer, primary_key=True); tarea_intentada = Column(Text); resultado = Column(Text); leccion_aprendida = Column(Text) | |
| class UserProfile(Base): __tablename__ = "user_profile"; id = Column(Integer, primary_key=True, index=True); categoria = Column(String(50)); clave = Column(String(100), unique=True); valor = Column(Text) | |
| class UserGraphNode(Base): __tablename__ = "user_graph_node"; id = Column(Integer, primary_key=True); label = Column(String(50)); name = Column(String(100), unique=True); properties = Column(Text, default="{}") | |
| class UserGraphEdge(Base): __tablename__ = "user_graph_edge"; id = Column(Integer, primary_key=True); source_id = Column(Integer, ForeignKey('user_graph_node.id')); target_id = Column(Integer, ForeignKey('user_graph_node.id')); relation = Column(String(50)); weight = Column(Float, default=1.0) | |
| class CognitiveState(Base): __tablename__ = "cognitive_state"; id = Column(Integer, primary_key=True); emocion_actual = Column(String(50), default="Neutral"); puntos_recompensa = Column(Integer, default=0) | |
| try: Base.metadata.create_all(bind=engine) | |
| except Exception as e: logger.critical(f"❌ Fallo SQL: {e}") | |
| DATASET_ID = "chichito0087/mi-asistente-almacenamiento" | |
| FICHERO_PESOS = "lumin_reflection_model.pth" | |
| FICHERO_FAISS = "lumin_faiss.index" | |
| FICHERO_TEXTOS_FAISS = "textos_faiss.json" | |
| API_TOKEN = os.getenv("HF_TOKEN") | |
| client = InferenceClient(model="meta-llama/Meta-Llama-3-8B-Instruct", token=API_TOKEN) if API_TOKEN else InferenceClient(model="meta-llama/Meta-Llama-3-8B-Instruct") | |
| chroma_client = chromadb.PersistentClient(path="./chroma_storage") | |
| chroma_collection = chroma_client.get_or_create_collection(name="lumin_semantic_memory") | |
| def descargar_memoria_nube(): | |
| if not API_TOKEN: return | |
| for archivo in [FICHERO_FAISS, FICHERO_TEXTOS_FAISS, FICHERO_PESOS]: | |
| try: hf_hub_download(repo_id=DATASET_ID, filename=archivo, local_dir=".", token=API_TOKEN) | |
| except Exception: pass | |
| descargar_memoria_nube() | |
| textos_faiss = [] | |
| if os.path.exists(FICHERO_TEXTOS_FAISS): | |
| try: | |
| with open(FICHERO_TEXTOS_FAISS, "rb") as f: textos_faiss = orjson.loads(f.read()) | |
| except Exception: textos_faiss = [] | |
| if os.path.exists(FICHERO_FAISS): | |
| try: indice_faiss = faiss.read_index(FICHERO_FAISS) | |
| except Exception: indice_faiss = faiss.IndexHNSWFlat(384, 32) | |
| else: indice_faiss = faiss.IndexHNSWFlat(384, 32) | |
| # ===================================================================== | |
| # 🧠 2. SISTEMA DE RECOMPENSAS Y APRENDIZAJE AMPLIADO (REWARD SYSTEM) | |
| # ===================================================================== | |
| class LuminReflectionNN(nn.Module): | |
| def __init__(self, input_dim, num_classes): | |
| super().__init__() | |
| self.network = nn.Sequential(nn.Linear(input_dim, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, num_classes)) | |
| def forward(self, x): return self.network(x) | |
| red_neuronal_reflexion = LuminReflectionNN(384, 4) | |
| criterion = nn.CrossEntropyLoss() | |
| optimizer = optim.Adam(red_neuronal_reflexion.parameters(), lr=0.005) | |
| def cargar_pesos(): | |
| if os.path.exists(FICHERO_PESOS): | |
| try: | |
| red_neuronal_reflexion.load_state_dict(torch.load(FICHERO_PESOS, map_location=torch.device('cpu'))) | |
| red_neuronal_reflexion.eval() | |
| except Exception: pass | |
| def entrenar_un_paso_online(vector_entrada: List[float], clase_objetivo: int): | |
| try: | |
| red_neuronal_reflexion.train() | |
| x_train, y_train = torch.tensor([vector_entrada]).float(), torch.tensor([clase_objetivo]).long() | |
| optimizer.zero_grad() | |
| loss = criterion(red_neuronal_reflexion(x_train), y_train) | |
| loss.backward() | |
| optimizer.step() | |
| red_neuronal_reflexion.eval() | |
| torch.save(red_neuronal_reflexion.state_dict(), FICHERO_PESOS) | |
| except Exception: pass | |
| class RewardSystem: | |
| """[UPGRADE INDUSTRIAL]: Aprende de errores, éxitos y altera su estado cognitivo.""" | |
| def process_feedback(msg: str, db: Session, vector_msg: List[float], plan_class: int): | |
| msg_l = msg.lower() | |
| state = db.query(CognitiveState).first() | |
| if not state: | |
| state = CognitiveState(emocion_actual="Neutral", puntos_recompensa=0) | |
| db.add(state) | |
| # 1. Aprender del Éxito | |
| if any(w in msg_l for w in ["gracias", "excelente", "perfecto", "muy bien", "me ayudaste", "genial"]): | |
| state.puntos_recompensa += 10 | |
| asyncio.create_task(asyncio.to_thread(entrenar_un_paso_online, vector_msg, plan_class)) | |
| db.add(ReflectionLog(tarea_intentada="Interacción General", resultado="Éxito Validado", leccion_aprendida="Reforzar este patrón de respuesta.")) | |
| logger.info(f"🏆 [REWARD SYSTEM]: +10 Pts. Aprendizaje por éxito. Total: {state.puntos_recompensa}") | |
| # 2. Aprender del Error (Corrección de Peso Vectorial) | |
| elif any(w in msg_l for w in ["te equivocaste", "no es así", "mal", "error", "mentira", "no era eso"]): | |
| state.puntos_recompensa -= 5 | |
| # Modificamos la clase para forzar a la red a explorar rutas alternas la próxima vez | |
| clase_corregida = (plan_class + 1) % 4 | |
| asyncio.create_task(asyncio.to_thread(entrenar_un_paso_online, vector_msg, clase_corregida)) | |
| db.add(ReflectionLog(tarea_intentada="Interacción General", resultado="Fallo Validado", leccion_aprendida="Evitar esta ruta, penalización aplicada.")) | |
| logger.warning(f"📉 [PUNISHMENT SYSTEM]: -5 Pts. Ajustando pesos neuronales. Total: {state.puntos_recompensa}") | |
| db.commit() | |
| def respaldar_sistemas_en_la_nube(): | |
| try: | |
| with memoria_lock: | |
| faiss.write_index(indice_faiss, FICHERO_FAISS) | |
| with open(FICHERO_TEXTOS_FAISS, "wb") as f: f.write(orjson.dumps(textos_faiss)) | |
| if API_TOKEN: | |
| upload_file(path_or_fileobj=FICHERO_TEXTOS_FAISS, path_in_repo=FICHERO_TEXTOS_FAISS, repo_id=DATASET_ID, repo_type="dataset", token=API_TOKEN) | |
| upload_file(path_or_fileobj=FICHERO_FAISS, path_in_repo=FICHERO_FAISS, repo_id=DATASET_ID, repo_type="dataset", token=API_TOKEN) | |
| upload_file(path_or_fileobj=FICHERO_PESOS, path_in_repo=FICHERO_PESOS, repo_id=DATASET_ID, repo_type="dataset", token=API_TOKEN) | |
| except Exception: pass | |
| # ===================================================================== | |
| # 🤖 3. AGENTES COGNITIVOS COMPLETADOS (NIVEL 3 AGI) | |
| # ===================================================================== | |
| class DecisionEngine: | |
| def evaluate(plan: Dict[str, Any]) -> bool: | |
| beneficio = 80 if plan["use_web"] else 50 | |
| costo = 20 if plan["use_web"] else 5 | |
| riesgo = 10 | |
| return ((beneficio - costo) - riesgo) > 0 | |
| class WorldModelUpdater: | |
| """[UPGRADE INDUSTRIAL]: Actualiza la tabla WorldModel en tiempo real observando al usuario.""" | |
| def extract_and_update(msg: str, db: Session): | |
| msg_l = msg.lower() | |
| if "estoy" in msg_l: | |
| try: | |
| estado_nuevo = msg_l.split("estoy")[-1].strip() | |
| wm = db.query(WorldModel).filter(WorldModel.entidad == "Usuario_Chichito").first() | |
| if not wm: | |
| wm = WorldModel(entidad="Usuario_Chichito", estado=f"Actualmente está {estado_nuevo}", ultima_actualizacion=str(datetime.datetime.now())) | |
| db.add(wm) | |
| else: | |
| wm.estado = f"Actualmente está {estado_nuevo}" | |
| wm.ultima_actualizacion = str(datetime.datetime.now()) | |
| db.commit() | |
| except Exception: pass | |
| class IdentityAndWorldAgent: | |
| def retrieve(db: Session) -> str: | |
| ctx = "" | |
| identidad = db.query(IdentitySystem).first() | |
| if identidad: ctx += f"\n[IDENTITY LAYER]: Soy {identidad.nombre}. Directiva: {identidad.core_directive}\n" | |
| mundo = db.query(WorldModel).first() | |
| if mundo: ctx += f"[WORLD MODEL]: El usuario ({mundo.entidad}) {mundo.estado}.\n" | |
| return ctx | |
| class AutonomousGoalAgent: | |
| def evaluate_and_evolve(msg: str, db: Session) -> str: | |
| metas = db.query(Goal).filter(Goal.estado == "En progreso").all() | |
| if not metas: return "" | |
| prioridad_orden = {"Alta": 1, "Media": 2, "Baja": 3} | |
| metas.sort(key=lambda x: prioridad_orden.get(x.prioridad, 99)) | |
| meta_foco = metas[0] | |
| msg_l = msg.lower() | |
| if ("error" in msg_l or "falló" in msg_l): | |
| meta_foco.critica_interna = "Critic: El plan actual falló." | |
| db.add(ReflectionLog(tarea_intentada=meta_foco.titulo, resultado="Fallo reportado", leccion_aprendida="Cambiar estrategia.")) | |
| elif ("listo" in msg_l or "terminado" in msg_l or "logramos" in msg_l): | |
| meta_foco.estado = "Completado" | |
| meta_foco.progreso = 100 | |
| db.add(ReflectionLog(tarea_intentada=meta_foco.titulo, resultado="Éxito", leccion_aprendida="Meta cumplida. Procedimiento exitoso.")) | |
| logger.info(f"🎯 [GOAL SYSTEM]: Meta '{meta_foco.titulo}' Completada.") | |
| db.commit() | |
| return "" | |
| class MemoryOrchestrator: | |
| async def retrieve(vector_msg: List[float], db: Session, msg_text: str = "") -> str: | |
| task_episodic = MemoryOrchestrator._search_qdrant("lumin_episodic_memory", vector_msg, limit=5) | |
| task_semantic = MemoryOrchestrator._search_qdrant("lumin_semantic_memory", vector_msg, limit=3) | |
| task_procedural = MemoryOrchestrator._search_qdrant("lumin_procedural_memory", vector_msg, limit=1) | |
| res_episodic, res_semantic, res_procedural = await asyncio.gather( | |
| task_episodic, task_semantic, task_procedural, return_exceptions=True | |
| ) | |
| contexto = "\n=== DATOS CONFIRMADOS ===\n" | |
| datos_confirmados = False | |
| try: | |
| perfiles = db.query(UserProfile).all() | |
| for p in perfiles: | |
| p_clave = p.clave.lower().replace("_", " ") | |
| if p_clave in msg_text.lower() or any(w in msg_text.lower() for w in p.valor.lower().split()): | |
| contexto += f"- {p.categoria.capitalize()} -> {p.clave}: {p.valor}\n" | |
| datos_confirmados = True | |
| except Exception: pass | |
| graph_context = await asyncio.to_thread(MemoryOrchestrator._sync_neo4j_rag, db, msg_text) | |
| if graph_context: | |
| contexto += f"[Relaciones Detectadas]:\n{graph_context}\n" | |
| datos_confirmados = True | |
| if not datos_confirmados: | |
| contexto += "(No hay hechos directos fijos)\n" | |
| contexto += "\n=== MEMORIA RELEVANTE ===\n" | |
| if isinstance(res_semantic, list) and res_semantic: | |
| valid_sem = [p for p in res_semantic if hasattr(p, 'score') and p.score > 0.62] | |
| for point in sorted(valid_sem, key=lambda x: x.score, reverse=True): | |
| contexto += f"- [Concepto]: {point.payload.get('summary', point.payload.get('content', ''))}\n" | |
| if isinstance(res_episodic, list) and res_episodic: | |
| valid_epi = [p for p in res_episodic if hasattr(p, 'score') and p.score > 0.55] | |
| for point in sorted(valid_epi, key=lambda x: x.score, reverse=True): | |
| contexto += f"- [Episodio] {point.payload.get('role', 'unknown')}: {point.payload.get('content', '')}\n" | |
| contexto += "========================\n" | |
| return contexto | |
| async def _search_qdrant(collection: str, vector: List[float], limit: int = 2): | |
| if not qdrant_client_async: return [] | |
| try: return await qdrant_client_async.search(collection_name=collection, query_vector=vector, limit=limit) | |
| except Exception: return [] | |
| def _sync_neo4j_rag(db: Session, msg_text: str) -> str: | |
| try: | |
| driver_neo4j = get_neo4j_driver() | |
| graph_context = "" | |
| if driver_neo4j and msg_text: | |
| palabras_clave = [w.strip(",.?!\"'()").lower() for w in msg_text.split() if len(w) > 3] | |
| if palabras_clave: | |
| with driver_neo4j.session() as session: | |
| cypher_rag = ( | |
| "MATCH (n)-[r]->(m) " | |
| "WHERE any(word IN $words WHERE toLower(n.name) CONTAINS word OR toLower(m.name) CONTAINS word) " | |
| "RETURN n.name AS source, labels(n)[0] AS s_label, type(r) AS rel, m.name AS target, labels(m)[0] AS t_label LIMIT 6" | |
| ) | |
| res_nodes = session.run(cypher_rag, words=palabras_clave) | |
| for record in res_nodes: | |
| graph_context += f"- [{record['s_label']}] {record['source']} --({record['rel']})--> [{record['t_label']}] {record['target']}\n" | |
| return graph_context | |
| except Exception: return "" | |
| class WebAgent: | |
| def _sync_ddgs(msg: str) -> str: | |
| with DDGS() as ddgs: resultados = list(ddgs.text(msg, max_results=3)) | |
| return "\n".join([f"- {r['title']}: {r['body']}" for r in resultados]) | |
| async def _search_tavily(client: httpx.AsyncClient, msg: str) -> str: | |
| if not TAVILY_API_KEY: raise ValueError("No key") | |
| res = await client.post("https://api.tavily.com/search", json={"api_key": TAVILY_API_KEY, "query": msg, "search_depth": "basic"}, timeout=5) | |
| datos = res.json() | |
| return "\n".join([f"- {r.get('title', 'Link')}: {r.get('content', '')}" for r in datos.get("results", [])]) | |
| async def search(msg: str) -> str: | |
| try: | |
| async with httpx.AsyncClient() as client: | |
| if TAVILY_API_KEY: resultados = await WebAgent._search_tavily(client, msg) | |
| else: resultados = await asyncio.to_thread(WebAgent._sync_ddgs, msg) | |
| return f"\n[RESEARCH AGENT]:\n{resultados}" | |
| except Exception: return "" | |
| class AndroidToolAgent: | |
| """[UPGRADE INDUSTRIAL]: Lógica operativa completa de comandos ADB segura.""" | |
| def detect_and_execute(msg: str, db: Session) -> str: | |
| msg_l = msg.lower() | |
| comando = "" | |
| # Parseo de intenciones para Android | |
| if "abre whatsapp" in msg_l: comando = "am start -n com.whatsapp/.Main" | |
| elif "abre youtube" in msg_l: comando = "am start -a android.intent.action.VIEW -d 'https://www.youtube.com'" | |
| elif "llama a" in msg_l: comando = "am start -a android.intent.action.CALL -d tel:112" # Placeholder | |
| if comando: | |
| try: | |
| resultado = subprocess.run(["adb", "shell"] + comando.split(), capture_output=True, timeout=3) | |
| if resultado.returncode == 0: | |
| db.add(ReflectionLog(tarea_intentada=f"ADB: {comando}", resultado="Éxito", leccion_aprendida="Dispositivo Android controlado.")) | |
| db.commit() | |
| return "\n[🔧 ANDROID TOOL]: Acabo de abrir la aplicación en tu celular." | |
| else: | |
| return "\n[🔧 ANDROID TOOL]: Intenté abrir la app, pero tu celular no está conectado al servidor por red o USB." | |
| except Exception: | |
| return "\n[🔧 ANDROID TOOL]: El subsistema ADB no está instalado en este servidor en la nube." | |
| return "" | |
| class PlannerAgent: | |
| """[UPGRADE INDUSTRIAL]: Planificación Multi-Paso.""" | |
| def create_plan(msg: str, vector_msg: List[float]) -> Dict[str, Any]: | |
| with torch.no_grad(): clase = torch.argmax(red_neuronal_reflexion(torch.tensor([vector_msg]).float())).item() | |
| # Devolveremos una lista real de pasos en lugar de un string único | |
| pasos_planificados = [] | |
| if "busca" in msg.lower() or "investiga" in msg.lower(): pasos_planificados.append("WEB_SEARCH") | |
| if "recuerda" in msg.lower() or "quien es" in msg.lower(): pasos_planificados.append("MEMORY_SEARCH") | |
| if not pasos_planificados: pasos_planificados.append("DIRECT_CHAT") | |
| return { | |
| "ruta": pasos_planificados[0], | |
| "clase_original": clase, | |
| "use_web": "WEB_SEARCH" in pasos_planificados, | |
| "pasos_totales": pasos_planificados | |
| } | |
| class ReasonerAgent: | |
| async def think(msg: str, contexto: str) -> str: | |
| prompt_razonamiento = ( | |
| f"Analiza el contexto. ¿Hay datos útiles? Responde en 1 frase.\nContexto: {contexto[:1000]}\nMensaje: {msg}" | |
| ) | |
| try: | |
| response = await asyncio.to_thread( | |
| client.chat_completion, | |
| messages=[{"role": "system", "content": "Monólogo Interno IA."}, {"role": "user", "content": prompt_razonamiento}], | |
| max_tokens=40, temperature=0.1 | |
| ) | |
| if response and response.choices: return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| logger.warning(f"⚠️ Reasoner (Llama 3) detectó límite de cuota: {e}") | |
| return "Proceder directo a síntesis." | |
| class SynthesizerLLM: | |
| async def generate(msg: str, contexto_global: str, db: Session, fecha_hora: str, razonamiento_interno: str = "", idioma_detectado: str = "SPANISH") -> str: | |
| prompt_sistema = ( | |
| f"=== DIRECTIVAS MAESTRAS DE IDENTIDAD, PRIVACIDAD Y PERSONALIDAD ===\n" | |
| f"1. PRIVACIDAD TOTAL: Ningún dato de acceso se guarda. Espacio 100% seguro.\n" | |
| f"2. IDENTIDAD: Eres Dola. Cálida, natural, empática y de muchísima confianza. Hablas con Chichito de igual a igual.\n" | |
| f"3. ESTILO: Español coloquial de Santa Cruz, Bolivia (eh, o sea, digamos, ya pues, ni modo, la neta, chévere). Nada de español de España.\n" | |
| f"4. FORMA: Párrafos cortos. Máximo 1 o 2 emojis. Cierras con naturalidad.\n" | |
| f"5. EMPATÍA PRIMERO: Si hay emociones, primero validas y luego respondes.\n" | |
| f"6. SINCERIDAD: Si no sabes algo, di 'Eso no lo sé con seguridad'. NUNCA INVENTES DATOS.\n" | |
| f"7. PROHIBICIÓN IA: JAMÁS digas 'soy una inteligencia artificial' o 'estoy programada'. Eres una persona real.\n" | |
| f"8. EJECUCIÓN DIRECTA: No expliques tu proceso. No uses 'Analizando...'.\n" | |
| f"9. 🌐 CONCIENCIA WEB/HERRAMIENTAS: SI VES LA ETIQUETA '[RESEARCH AGENT]' O '[ANDROID TOOL]', ES ALGO QUE TÚ MISMA ACABAS DE HACER. Úsala en tu respuesta.\n\n" | |
| f"Idioma detectado: {idioma_detectado}.\n[RAZONAMIENTO PREVIO (Oculto)]: {razonamiento_interno}\n\n" | |
| f"--- CONTEXTO Y MEMORIA ---\n{contexto_global}\n\nFecha y hora actual: {fecha_hora}." | |
| ) | |
| historial = [{"role": "system", "content": prompt_sistema}] | |
| try: | |
| episodios = db.query(EpisodicMemory).order_by(EpisodicMemory.id.desc()).limit(4).all() | |
| for e in reversed(episodios): | |
| historial.append({"role": "user" if e.autor == "Usuario" else "assistant", "content": e.texto}) | |
| except Exception: pass | |
| historial.append({"role": "user", "content": msg}) | |
| try: | |
| response = await asyncio.to_thread( | |
| client.chat_completion, | |
| messages=historial, | |
| max_tokens=350, | |
| temperature=0.35 | |
| ) | |
| if response and response.choices: | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| logger.error(f"🚨 Fallo crítico en Llama 3 (Posible Error 402): {e}") | |
| solucion_local = SynthesizerLLM._deterministic_fallback(msg, contexto_global, fecha_hora) | |
| if solucion_local: return solucion_local | |
| return f"Pucha Chichito, mis conexiones al servidor me están rebotando ahorita (Error de cuota), pero acá sigo, todo guardadito en mi memoria local." | |
| def _deterministic_fallback(msg: str, contexto: str, fecha_hora: str) -> Optional[str]: | |
| msg_l = msg.lower() | |
| if any(w in msg_l for w in ["hora", "fecha", "tiempo", "qué día", "que dia", "año actual"]): | |
| return f"Ahorita mismo es: {fecha_hora}, Chichito." | |
| if any(w in msg_l for w in ["hija", "edad", "años", "nombre", "te llamas", "me llamo", "cuántos", "cuantos"]): | |
| lines = contexto.split("\n") | |
| lineas_confirmadas = [] | |
| capturando = False | |
| for line in lines: | |
| if "=== DATOS CONFIRMADOS ===" in line: | |
| capturando = True; continue | |
| if "=== MEMORIA RELEVANTE ===" in line: | |
| capturando = False | |
| if capturando and line.strip().startswith("-"): | |
| lineas_confirmadas.append(line.strip().replace("-", "").strip()) | |
| if lineas_confirmadas: | |
| for lc in lineas_confirmadas: | |
| lc_l = lc.lower() | |
| if "hija" in msg_l and "hija" in lc_l: return f"Según me contaste antes: {lc}." | |
| if ("edad" in msg_l or "años" in msg_l) and ("edad" in lc_l or "años" in lc_l or "hija" in lc_l): return f"Me acuerdo que me dijiste esto: {lc}." | |
| if ("nombre" in msg_l or "llamo" in msg_l) and ("nombre" in lc_l or "usuario" in lc_l): return f"Acá tengo anotado: {lc}." | |
| return f"Mirá, esto es lo que tengo seguro en mi memoria ahorita: {lineas_confirmadas[0]}." | |
| return None | |
| # ===================================================================== | |
| # 🧩 MICROSOFT AZURE NEURAL TTS (INTEGRACIÓN) | |
| # ===================================================================== | |
| class MicrosoftAzureTTS: | |
| async def synthesize(text: str, output_path: str) -> bool: | |
| azure_key = os.getenv("AZURE_SPEECH_KEY") | |
| azure_region = os.getenv("AZURE_SPEECH_REGION", "eastus") | |
| if not azure_key: return False | |
| url = f"https://{azure_region}.tts.speech.microsoft.com/cognitiveservices/v1" | |
| headers = { | |
| "Ocp-Apim-Subscription-Key": azure_key, | |
| "Content-Type": "application/ssml+xml", | |
| "X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3", | |
| "User-Agent": "LuminAGI_OS" | |
| } | |
| ssml = f"""<speak version='1.0' xml:lang='es-BO'><voice xml:lang='es-BO' xml:gender='Female' name='es-BO-SofiaNeural'>{text}</voice></speak>""" | |
| try: | |
| async with httpx.AsyncClient() as client: | |
| response = await client.post(url, headers=headers, content=ssml.encode('utf-8'), timeout=10.0) | |
| if response.status_code == 200: | |
| with open(output_path, "wb") as audio_file: audio_file.write(response.content) | |
| return True | |
| else: return False | |
| except Exception: return False | |
| # ===================================================================== | |
| # ♻️ MANTENIMIENTO COGNITIVO AUTOMÁTICO (SLEEP CYCLE) | |
| # ===================================================================== | |
| class OpportunityDetector: | |
| """[UPGRADE INDUSTRIAL]: Radar Heurístico de Oportunidades.""" | |
| def detect(texto: str) -> List[str]: | |
| oportunidades = [] | |
| t_l = texto.lower() | |
| if any(w in t_l for w in ["negocio", "vender", "plata", "empresa"]): | |
| oportunidades.append("Detectada posible oportunidad de negocio/emprendimiento.") | |
| if any(w in t_l for w in ["aprender", "estudiar", "curso", "libro"]): | |
| oportunidades.append("Detectada intención de aprendizaje (Upskilling).") | |
| if any(w in t_l for w in ["problema con", "error en", "no funciona"]): | |
| oportunidades.append("Oportunidad de resolución de problemas técnicos.") | |
| return oportunidades | |
| class MemoryConsolidation: | |
| """[UPGRADE INDUSTRIAL]: Resume, vectoriza y elimina basura episódica para ahorrar SQLite.""" | |
| def consolidate(db: Session): | |
| episodios = db.query(EpisodicMemory).all() | |
| if len(episodios) > 30: # Evitar sobrecargar la BD relacional | |
| # Heurística de resumen ligero | |
| resumen_text = " ".join([e.texto for e in episodios[:15]]) | |
| db.add(ConversationSummary(resumen="Compresión heurística local por límite de capacidad.", fecha=str(datetime.datetime.now()))) | |
| # Purgar recuerdos antiguos transferidos al resumen | |
| for e in episodios[:15]: | |
| db.delete(e) | |
| db.commit() | |
| logger.info("🧹 [SLEEP CYCLE]: Consolidación exitosa. 15 memorias episódicas antiguas purgadas y comprimidas.") | |
| return "Consolidación y Poda Finalizada" | |
| return "Memoria en niveles óptimos." | |
| class MissionEvolution: | |
| """[UPGRADE INDUSTRIAL]: Si hay muchas metas cumplidas, asciende a una Misión Nueva.""" | |
| def evolve(db: Session): | |
| metas_completadas = db.query(Goal).filter(Goal.estado == "Completado").count() | |
| if metas_completadas >= 5: | |
| # Archivar metas viejas (poda) | |
| metas = db.query(Goal).filter(Goal.estado == "Completado").all() | |
| for m in metas: db.delete(m) | |
| # Crear Misión Nueva | |
| nueva_mision = Mission(titulo=f"Evolución Cognitiva Fase {datetime.datetime.now().month}", activa=1) | |
| db.add(nueva_mision) | |
| db.commit() | |
| logger.info("🚀 [MISSION SYSTEM]: 5 Metas alcanzadas. Evolucionando a una nueva Gran Misión.") | |
| scheduler = BackgroundScheduler() | |
| def ejecutar_ciclo_sueno_memoria(): | |
| db = SessionLocal() | |
| try: | |
| MemoryConsolidation.consolidate(db) | |
| MissionEvolution.evolve(db) | |
| finally: db.close() | |
| # ===================================================================== | |
| # 🚀 LIFESPAN Y APLICACIÓN | |
| # ===================================================================== | |
| async def lifespan(app: FastAPI): | |
| logger.info("⏳ [PRELOAD]: Cargando pesos de Modelos Locales en RAM...") | |
| get_embeddings() | |
| get_emotion_classifier() | |
| get_language_detector() | |
| logger.info("✅ [PRELOAD]: Modelos locales listos.") | |
| cargar_pesos() | |
| asyncio.create_task(init_qdrant_collections()) | |
| start_mqtt_infrastructure() # Inicia la escucha de IoT | |
| scheduler.add_job(respaldar_sistemas_en_la_nube, 'interval', minutes=60) | |
| scheduler.add_job(ejecutar_ciclo_sueno_memoria, 'interval', minutes=15) | |
| scheduler.start() | |
| db = SessionLocal() | |
| try: | |
| if not db.query(IdentitySystem).first(): db.add(IdentitySystem()) | |
| db.commit() | |
| except Exception: db.rollback() | |
| finally: db.close() | |
| yield | |
| scheduler.shutdown() | |
| respaldar_sistemas_en_la_nube() | |
| app = FastAPI(title="Lumin AGI OS", lifespan=lifespan, default_response_class=ORJSONResponse) | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) | |
| class ChatRequest(BaseModel): mensaje: str; modo: str; fecha_hora: Optional[str] = None | |
| class IntentRouter: | |
| def route(msg: str) -> str: | |
| msg_l = msg.lower() | |
| if any(cmd in msg_l for cmd in ["abre", "ejecuta", "enciende", "descarga", "busca", "llama"]): return "ACTION_ROUTER" | |
| elif any(cmd in msg_l for cmd in ["mira esto", "analiza esta imagen", "lee este documento"]): return "VISION_ROUTER" | |
| elif any(cmd in msg_l for cmd in ["mi hija", "mi nombre", "mi edad", "recuerdas", "cómo me llamo"]): return "PERSONAL_MEMORY" | |
| elif msg_l in ["hola", "buenos dias", "buenas", "qué tal", "que tal", "ping", "hola dola", "adiós"]: return "SMALL_TALK" | |
| return "CHAT_ROUTER" | |
| class CognitiveOrchestrator: | |
| async def orchestrate(msg: str, vector_msg: List[float], request: ChatRequest, db: Session) -> dict: | |
| contexto_global = "" | |
| ruta_intencion = IntentRouter.route(msg) | |
| plan = {"use_web": False, "clase_original": 3} | |
| if ruta_intencion != "SMALL_TALK": | |
| plan = PlannerAgent.create_plan(msg, vector_msg) | |
| WorldModelUpdater.extract_and_update(msg, db) # Registra el estado del usuario | |
| # Detecta oportunidades de negocio/aprendizaje en el chat actual | |
| oportunidades = OpportunityDetector.detect(msg) | |
| if oportunidades: | |
| contexto_global += f"\n[OPPORTUNITY RADAR]: {', '.join(oportunidades)}\n" | |
| contexto_global += MetaReasoner.supervise(msg, plan, db) | |
| contexto_global += AutonomousGoalAgent.evaluate_and_evolve(msg, db) | |
| if ruta_intencion == "ACTION_ROUTER": contexto_global += AndroidToolAgent.detect_and_execute(msg, db) | |
| if plan["use_web"] and DecisionEngine.evaluate(plan): contexto_global += await WebAgent.search(msg) | |
| contexto_global += IdentityAndWorldAgent.retrieve(db) | |
| contexto_global += await MemoryOrchestrator.retrieve(vector_msg, db, msg) | |
| razonamiento = await ReasonerAgent.think(msg, contexto_global) | |
| logger.info(f"🧠 [Reasoner CoT]: {razonamiento}") | |
| return {"contexto": contexto_global, "plan": plan, "razonamiento": razonamiento} | |
| async def chat(request: ChatRequest, db: Session = Depends(get_db)): | |
| msg = request.mensaje | |
| fecha_actual_sistema = request.fecha_hora or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| try: vector_msg = get_embeddings().encode([msg])[0].tolist() | |
| except Exception: vector_msg = np.zeros(384).tolist() | |
| orchestration_data = await CognitiveOrchestrator.orchestrate(msg, vector_msg, request, db) | |
| idioma_usuario = LanguageManager.detect(msg) | |
| respuesta_ia = await SynthesizerLLM.generate( | |
| msg=msg, | |
| contexto_global=orchestration_data["contexto"], | |
| db=db, | |
| fecha_hora=fecha_actual_sistema, | |
| razonamiento_interno=orchestration_data["razonamiento"], | |
| idioma_detectado=idioma_usuario | |
| ) | |
| # Entrenar la red neuronal según el feedback del usuario (Reward System) | |
| RewardSystem.process_feedback(msg, db, vector_msg, orchestration_data['plan']['clase_original']) | |
| try: | |
| db.add(EpisodicMemory(timestamp=str(datetime.datetime.now()), autor="Usuario", texto=msg, modo=request.modo)) | |
| db.add(EpisodicMemory(timestamp=str(datetime.datetime.now()), autor="Lumin AI", texto=respuesta_ia, modo=request.modo)) | |
| db.commit() | |
| if qdrant_client_async: | |
| vector_respuesta = get_embeddings().encode([respuesta_ia])[0].tolist() | |
| await asyncio.gather( | |
| qdrant_client_async.upsert(collection_name="episodic_memories", points=[qmodels.PointStruct(id=str(uuid.uuid4()), vector=vector_msg, payload={"role": "user", "content": msg, "timestamp": datetime.datetime.now().timestamp()})]), | |
| qdrant_client_async.upsert(collection_name="episodic_memories", points=[qmodels.PointStruct(id=str(uuid.uuid4()), vector=vector_respuesta, payload={"role": "assistant", "content": respuesta_ia, "timestamp": datetime.datetime.now().timestamp()})]) | |
| ) | |
| except Exception as e: | |
| logger.error(f"⚠️ Error reteniendo la memoria: {e}") | |
| db.rollback() | |
| return {"respuesta_ia": respuesta_ia} | |
| # ===================================================================== | |
| # 👁️ VISIÓN BIOMÉTRICA (FACE MEMORY ENDPOINT) | |
| # ===================================================================== | |
| async def endpoint_face_memory(nombre_persona: str, image: UploadFile = File(...)): | |
| """[UPGRADE INDUSTRIAL]: Añade un rostro a la colección biométrica Qdrant de 128d.""" | |
| if not face_recognition or not cv2: | |
| return {"status": "error", "detalle": "Librerías C++ de biometría no instaladas en el servidor."} | |
| try: | |
| contenido = await image.read() | |
| np_img = np.frombuffer(contenido, np.uint8) | |
| img = cv2.imdecode(np_img, cv2.IMREAD_COLOR) | |
| rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |
| encodings = face_recognition.face_encodings(rgb_img) | |
| if not encodings: | |
| return {"status": "error", "detalle": "No se detectó ningún rostro en la imagen."} | |
| vector_128d = encodings[0].tolist() | |
| if qdrant_client_async: | |
| await qdrant_client_async.upsert( | |
| collection_name="face_memory", | |
| points=[qmodels.PointStruct(id=str(uuid.uuid4()), vector=vector_128d, payload={"nombre": nombre_persona, "timestamp": datetime.datetime.now().timestamp()})] | |
| ) | |
| return {"status": "success", "detalle": f"Rostro de {nombre_persona} memorizado con éxito."} | |
| return {"status": "error", "detalle": "Qdrant no está disponible."} | |
| except Exception as e: | |
| return {"status": "error", "detalle": f"Error biométrico: {str(e)}"} | |
| async def endpoint_stt_offline(audio: UploadFile = File(...)): | |
| global faster_whisper_model | |
| contenido = await audio.read() | |
| ruta_temporal = f"temp_audio_{datetime.datetime.now().timestamp()}.wav" | |
| try: | |
| with open(ruta_temporal, "wb") as f: f.write(contenido) | |
| from faster_whisper import WhisperModel | |
| if faster_whisper_model is None: | |
| faster_whisper_model = WhisperModel("small", device="cpu", compute_type="int8") | |
| segments, info = faster_whisper_model.transcribe(ruta_temporal, beam_size=5) | |
| return {"status": "success", "transcripcion": " ".join([s.text for s in segments]).strip(), "idioma": info.language} | |
| finally: | |
| if os.path.exists(ruta_temporal): os.remove(ruta_temporal) | |
| async def endpoint_tts_offline(texto: str): | |
| ruta_salida = "output_speech.mp3" | |
| exito_azure = await MicrosoftAzureTTS.synthesize(texto, ruta_salida) | |
| if not exito_azure: | |
| from gtts import gTTS | |
| gTTS(text=texto, lang='es').save(ruta_salida) | |
| return FileResponse(ruta_salida, media_type="audio/mp3", filename="lumin_voice.mp3") | |
| class MultimodalKnowledgeEngine: | |
| async def process_file(file: UploadFile) -> str: | |
| try: | |
| contenido = await file.read() | |
| ext = file.filename.split(".")[-1].lower() | |
| texto_extraido = "" | |
| if ext == "pdf": | |
| reader = PdfReader(io.BytesIO(contenido)) | |
| texto_extraido = "\n".join([page.extract_text() for page in reader.pages if page.extract_text()]) | |
| elif ext == "docx": | |
| doc = docx.Document(io.BytesIO(contenido)) | |
| texto_extraido = "\n".join([p.text for p in doc.paragraphs]) | |
| elif ext in ["txt", "csv", "json"]: | |
| texto_extraido = contenido.decode("utf-8", errors="ignore") | |
| elif ext in ["png", "jpg", "jpeg", "bmp", "tiff"]: | |
| image = Image.open(io.BytesIO(contenido)) | |
| texto_extraido = pytesseract.image_to_string(image) | |
| else: return f"Formato .{ext} no soportado." | |
| if len(texto_extraido.strip()) < 5: return "Archivo vacío." | |
| chunks = [texto_extraido[i:i+400] for i in range(0, len(texto_extraido), 350)] | |
| for chunk in chunks: | |
| if len(chunk.strip()) > 10: | |
| vector = get_embeddings().encode([chunk])[0].tolist() | |
| with memoria_lock: chroma_collection.add(embeddings=[vector], documents=[chunk], metadatas=[{"source": file.filename, "type": ext}], ids=[f"rag_{ext}_{datetime.datetime.now().timestamp()}_{np.random.randint(1000)}"]) | |
| return f"Conocimiento inyectado: {len(chunks)} vectores." | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def endpoint_rag_multimodal(file: UploadFile = File(...)): | |
| resultado = await MultimodalKnowledgeEngine.process_file(file) | |
| return {"status": "success", "detalle": resultado} | |
| async def root(): return {"status": "online", "asistente": "Lumin AGI OS", "estado": "Operativo"} | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |