Compare commits
15 Commits
develop
...
cambios_llm
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ad80defcf | |||
| aef8791151 | |||
| 3d5deef0fb | |||
| 3438102dc0 | |||
| 9ee8daa295 | |||
| 6d6fab5634 | |||
| 9c638fc3e5 | |||
| 43f6fb03fe | |||
| ac83907e7c | |||
| 3cd267ee6e | |||
| e1b756ac99 | |||
| 628cddc3ae | |||
| cf6a768f6b | |||
| a62778a030 | |||
| 6b491a9a41 |
@@ -0,0 +1,6 @@
|
||||
# Inicia el frontend en una nueva ventana de PowerShell
|
||||
Start-Process powershell -ArgumentList "-NoExit", "-Command", 'cd ./frontend; npm run dev; pause'
|
||||
|
||||
# Inicia el backend en una nueva ventana de PowerShell
|
||||
Start-Process powershell -ArgumentList "-NoExit", "-Command", 'cd ./backend; uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000; pause'
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# backend/api/router.py
|
||||
|
||||
from fastapi import APIRouter
|
||||
from backend.api.v1.endpoints import ping, text_manager_endpoint
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ping.router, prefix="/api/v1/ping")
|
||||
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
|
||||
@@ -0,0 +1,57 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/bar")
|
||||
def get_bar_chart():
|
||||
return {
|
||||
"xAxis": {"type": "category", "data": ["Ene", "Feb", "Mar", "Abr"]},
|
||||
"yAxis": {"type": "value"},
|
||||
"series": [{"data": [5, 20, 36, 10], "type": "bar"}]
|
||||
}
|
||||
|
||||
@router.get("/line")
|
||||
def get_line_chart():
|
||||
return {
|
||||
"xAxis": {"type": "category", "data": ["Semana 1", "Semana 2", "Semana 3", "Semana 4"]},
|
||||
"yAxis": {"type": "value"},
|
||||
"series": [{"data": [15, 25, 18, 30], "type": "line"}]
|
||||
}
|
||||
|
||||
@router.get("/pie")
|
||||
def get_pie_chart():
|
||||
return {
|
||||
"tooltip": {"trigger": "item"},
|
||||
"legend": {"top": "5%", "left": "center"},
|
||||
"series": [{
|
||||
"name": "Accesos",
|
||||
"type": "pie",
|
||||
"radius": ["40%", "70%"],
|
||||
"avoidLabelOverlap": False,
|
||||
"itemStyle": {"borderRadius": 10, "borderColor": "#fff", "borderWidth": 2},
|
||||
"label": {"show": False, "position": "center"},
|
||||
"emphasis": {
|
||||
"label": {"show": True, "fontSize": 16, "fontWeight": "bold"}
|
||||
},
|
||||
"labelLine": {"show": False},
|
||||
"data": [
|
||||
{"value": 1048, "name": "Search"},
|
||||
{"value": 735, "name": "Direct"},
|
||||
{"value": 580, "name": "Email"},
|
||||
{"value": 484, "name": "Union Ads"},
|
||||
{"value": 300, "name": "Video Ads"}
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
@router.get("/scatter")
|
||||
def get_scatter_chart():
|
||||
return {
|
||||
"xAxis": {},
|
||||
"yAxis": {},
|
||||
"series": [{
|
||||
"symbolSize": 20,
|
||||
"data": [[10, 8], [20, 20], [30, 10], [40, 30], [50, 15]],
|
||||
"type": "scatter"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# backend/domains/llm/agent_endpoints.py
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from backend.backend_domains.llms.llm_chat_srvc import construir_agente_llm, responder, responder_stream
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
from entrypoint.init_db import db_credencial
|
||||
|
||||
LoggerDB(db_credencial, "logger_llm", created_by="sistema")
|
||||
|
||||
router = APIRouter()
|
||||
agente = construir_agente_llm() # inicializa el agente una vez
|
||||
|
||||
# 📥 Schema para entrada de prompt
|
||||
class ChatInput(BaseModel):
|
||||
prompt: str
|
||||
|
||||
# ✅ Endpoint de respuesta simple
|
||||
@router.post("/chat", summary="Enviar prompt y obtener respuesta completa del agente")
|
||||
async def chat_endpoint(data: ChatInput):
|
||||
try:
|
||||
return await responder(data.prompt, agente)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.exception("[ERROR] Fallo durante respuesta del agente:")
|
||||
raise HTTPException(status_code=500, detail="Error interno al procesar la solicitud.")
|
||||
|
||||
# 🔁 Endpoint de streaming
|
||||
@router.post("/chat-stream", summary="Enviar prompt y recibir respuesta del agente en streaming")
|
||||
async def chat_stream_endpoint(data: ChatInput):
|
||||
try:
|
||||
return StreamingResponse(
|
||||
responder_stream(data.prompt, agente),
|
||||
media_type="text/plain"
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.exception("[ERROR] Fallo durante respuesta en streaming:")
|
||||
raise HTTPException(status_code=500, detail="Error interno en el agente.")
|
||||
@@ -0,0 +1,84 @@
|
||||
# src/services/agent_service.py
|
||||
|
||||
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from domains.Llms.Modelos.Openai_model import ModeloOpenAI
|
||||
from domains.Llms.Agente import AgenteAI
|
||||
from domains.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres
|
||||
from domains.Llms.MCPs.McpClient import MCPClient
|
||||
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
|
||||
from entrypoint.init_db import db_credencial
|
||||
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_llm", created_by="sistema")
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
# 🔧 Inicialización única del agente
|
||||
def construir_agente_llm() -> AgenteAI:
|
||||
logger.info("[INICIO] Inicializando agente LLM...")
|
||||
|
||||
conexion = PostgresConexion(db_credencial)
|
||||
|
||||
# Paso 1: Obtener credencial
|
||||
repo = OpenAICredencialRepo(conexion)
|
||||
credencial = repo.get_by_id("OPAK20250513-61b29978b7604031014")
|
||||
if not credencial:
|
||||
raise ValueError("No se encontró la credencial OpenAI")
|
||||
|
||||
logger.debug(f"[OK] Credencial OpenAI cargada: {credencial.titulo}")
|
||||
|
||||
# Paso 2: Crear cliente
|
||||
cliente = OpenAICliente(credencial)
|
||||
|
||||
# Paso 3: Instanciar modelo
|
||||
modelo = ModeloOpenAI(
|
||||
cliente=cliente,
|
||||
model="gpt-4o",
|
||||
temperature=1
|
||||
)
|
||||
|
||||
# Paso 4: Memoria en PostgreSQL
|
||||
memoria = MemoryConvPostgres(
|
||||
credencial=db_credencial,
|
||||
nombre_tabla="memoria_conversacion_pruebas",
|
||||
k=10
|
||||
)
|
||||
|
||||
# Paso 5: Herramientas MCP (ej. archivos)
|
||||
archivos = MCPClient.from_http(
|
||||
name="files",
|
||||
url="http://127.0.0.1:4201/fs"
|
||||
)
|
||||
registry = ClientRegistry()
|
||||
registry.add("files", archivos)
|
||||
|
||||
# Paso 6: Agente
|
||||
agente = AgenteAI(
|
||||
modelo=modelo,
|
||||
nombre="Asistente Inteligente",
|
||||
descripcion="",
|
||||
system_prompt="",
|
||||
rol="asistente",
|
||||
objetivos=[],
|
||||
max_iterations=0,
|
||||
memoria=memoria,
|
||||
mcp=registry
|
||||
)
|
||||
|
||||
logger.success("[OK] Agente LLM listo.")
|
||||
return agente
|
||||
|
||||
# ⚡ Función simple
|
||||
async def responder(prompt: str, agente: AgenteAI) -> str:
|
||||
logger.info(f"[Petición] Prompt recibido: {prompt[:50]}...")
|
||||
respuesta = await agente.interactuar_en_bucle(prompt=prompt, stream=False)
|
||||
logger.debug(f"[Respuesta] {respuesta[:100]}...")
|
||||
return respuesta
|
||||
|
||||
# 🔁 Función en streaming
|
||||
async def responder_stream(prompt: str, agente: AgenteAI) -> AsyncGenerator[str, None]:
|
||||
logger.info(f"[Streaming] Prompt recibido: {prompt[:50]}...")
|
||||
async for token in agente.interactuar_en_bucle(prompt=prompt, stream=True):
|
||||
yield token
|
||||
@@ -0,0 +1,35 @@
|
||||
from fastapi import WebSocket, APIRouter, WebSocketDisconnect
|
||||
from backend.backend_domains.llms.llm_chat_srvc import construir_agente_llm
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
from entrypoint.init_db import db_credencial
|
||||
import json
|
||||
|
||||
LoggerDB(db_credencial, "logger_llm_ws", created_by="sistema")
|
||||
|
||||
router = APIRouter()
|
||||
agente = construir_agente_llm()
|
||||
|
||||
@router.websocket("/ws/chat")
|
||||
async def chat_ws(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
try:
|
||||
data = await websocket.receive_text()
|
||||
parsed = json.loads(data)
|
||||
prompt = parsed.get("prompt")
|
||||
if not prompt:
|
||||
await websocket.send_text("⚠️ Prompt vacío.")
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# ✅ Solución: hacer await antes de iterar
|
||||
respuesta_gen = await agente.interactuar_en_bucle(prompt=prompt, stream=True)
|
||||
async for token in respuesta_gen:
|
||||
await websocket.send_text(token)
|
||||
|
||||
await websocket.close()
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info("🔌 WebSocket desconectado por el cliente.")
|
||||
except Exception as e:
|
||||
logger.exception("❌ Error en WebSocket:")
|
||||
await websocket.close()
|
||||
+7
-3
@@ -1,12 +1,16 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import Path
|
||||
|
||||
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput
|
||||
from backend.backend_domains.text_manager.text_manager_schema import BibliotecaInput, NotaInput
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from backend.db.conexion import get_conexion
|
||||
from backend.services.text_manager_srvc import *
|
||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from backend.backend_domains.text_manager.text_manager_srvc import *
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
|
||||
from entrypoint.init_db import db_credencial
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
+29
-32
@@ -1,53 +1,55 @@
|
||||
from src.TextManager.biblioteca import Biblioteca
|
||||
from src.TextManager.biblioteca_mmr import BibliotecaRepo
|
||||
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
|
||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from src.TextManager.nota import Nota
|
||||
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
|
||||
from domains.TextManager.biblioteca import Biblioteca
|
||||
from domains.TextManager.biblioteca_mmr import BibliotecaRepo
|
||||
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from domains.TextManager.nota import Nota
|
||||
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
|
||||
from sqlalchemy import MetaData
|
||||
from backend.schemas.text_manager_schema import NotaInput
|
||||
|
||||
from backend.backend_domains.text_manager.text_manager_schema import NotaInput
|
||||
|
||||
from entrypoint.init_db import db_credencial
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
|
||||
|
||||
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
|
||||
print("[INICIO] Creando biblioteca...")
|
||||
logger.info("[INICIO] Creando biblioteca...")
|
||||
|
||||
try:
|
||||
print("[Paso 1] Obteniendo credencial...")
|
||||
logger.info("[Paso 1] Obteniendo credencial...")
|
||||
cred_repo = OpenAICredencialRepo(conexion)
|
||||
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
|
||||
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
|
||||
logger.debug(f"[OK] Credencial obtenida: {credencial.titulo if credencial else '❌ None'}")
|
||||
|
||||
print("[Paso 2] Instanciando embedder...")
|
||||
logger.info("[Paso 2] Instanciando embedder...")
|
||||
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
|
||||
print("[OK] Embedder instanciado")
|
||||
logger.debug("[OK] Embedder instanciado")
|
||||
|
||||
print("[Paso 3] Instanciando biblioteca...")
|
||||
logger.info("[Paso 3] Instanciando biblioteca...")
|
||||
biblioteca = Biblioteca(
|
||||
nombre=nombre_biblioteca,
|
||||
embedder=embedder,
|
||||
descripcion=descripcion
|
||||
)
|
||||
print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
|
||||
logger.debug(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
|
||||
|
||||
print("[Paso 4] Guardando en base de datos...")
|
||||
logger.info("[Paso 4] Guardando en base de datos...")
|
||||
repo = BibliotecaRepo(conexion)
|
||||
repo.add(biblioteca=biblioteca)
|
||||
print("[OK] Biblioteca guardada")
|
||||
logger.success("[OK] Biblioteca guardada")
|
||||
|
||||
print("[Paso 5] Generando modelo de notas...")
|
||||
logger.info("[Paso 5] Generando modelo de notas...")
|
||||
biblioteca.generar_modelo_notas(conexion)
|
||||
print("[OK] Modelo de notas generado")
|
||||
logger.success("[OK] Modelo de notas generado")
|
||||
|
||||
print("[FIN] Biblioteca creada correctamente")
|
||||
logger.success("[FIN] Biblioteca creada correctamente")
|
||||
return {
|
||||
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
|
||||
"id": biblioteca.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print("[ERROR] Ocurrió una excepción:", str(e))
|
||||
logger.exception("[ERROR] Ocurrió una excepción:")
|
||||
raise
|
||||
|
||||
|
||||
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
|
||||
for b in bibliotecas
|
||||
]
|
||||
|
||||
|
||||
def agregar_nota_a_biblioteca(
|
||||
conexion: PostgresConexion,
|
||||
biblioteca_id: str,
|
||||
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
|
||||
conexiones: list[str] = None,
|
||||
resumen: str = ""
|
||||
):
|
||||
|
||||
# Obtener la biblioteca
|
||||
repo_biblioteca = BibliotecaRepo(conexion)
|
||||
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
|
||||
if biblioteca is None:
|
||||
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
|
||||
|
||||
# Crear objeto Nota
|
||||
nota = Nota(
|
||||
titulo=titulo,
|
||||
texto=texto,
|
||||
tags=tags or [],
|
||||
conexiones=conexiones or [],
|
||||
resumen=resumen or "",
|
||||
# vector=biblioteca.embedder.embed_text(texto),
|
||||
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
|
||||
)
|
||||
# Mostrar atributos seguros
|
||||
print(
|
||||
logger.debug(
|
||||
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
|
||||
f"texto_len={len(nota.texto)}, "
|
||||
f"tags={len(nota.tags)}, "
|
||||
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
|
||||
f"resumen_len={len(nota.resumen)}"
|
||||
)
|
||||
|
||||
# Preparar tabla y modelo de nota
|
||||
metadata = MetaData()
|
||||
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
|
||||
biblioteca.nombre,
|
||||
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
|
||||
)
|
||||
metadata.create_all(conexion.get_engine())
|
||||
|
||||
# Guardar la nota
|
||||
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
|
||||
nota_id = repo_nota.add(nota)
|
||||
|
||||
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
|
||||
"nota_id": nota_id
|
||||
}
|
||||
|
||||
print(f"[SUCCESS] {resultado['mensaje']}")
|
||||
logger.success(f"[SUCCESS] {resultado['mensaje']}")
|
||||
return resultado
|
||||
|
||||
|
||||
@@ -160,6 +155,7 @@ def eliminar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str)
|
||||
fue_eliminada = repo_nota.delete_by_id(nota_id)
|
||||
|
||||
if fue_eliminada:
|
||||
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
|
||||
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
|
||||
else:
|
||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
||||
@@ -186,6 +182,7 @@ def actualizar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str
|
||||
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
|
||||
|
||||
if fue_actualizada:
|
||||
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
|
||||
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
|
||||
else:
|
||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
||||
@@ -0,0 +1,35 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from domains.Usuario.usuario_mmr import UsuarioRepo, Usuario, UsuarioModel
|
||||
from backend.db.conexion import get_conexion
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/usuarios/", response_model=dict)
|
||||
def crear_usuario(nombre: str, email: str, db: Session = Depends(get_conexion)):
|
||||
repo = UsuarioRepo(db)
|
||||
usuario = Usuario(id=None, nombre=nombre, email=email)
|
||||
usuario_id = repo.add(usuario)
|
||||
return {"id": usuario_id}
|
||||
|
||||
@router.get("/usuarios/{usuario_id}", response_model=dict)
|
||||
def obtener_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
|
||||
repo = UsuarioRepo(db)
|
||||
usuario = repo.get_by_id(usuario_id)
|
||||
if not usuario:
|
||||
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||
return {"id": usuario.id, "nombre": usuario.nombre, "email": usuario.email, "activo": usuario.activo}
|
||||
|
||||
@router.get("/usuarios/", response_model=list)
|
||||
def listar_usuarios(db: Session = Depends(get_conexion)):
|
||||
repo = UsuarioRepo(db)
|
||||
usuarios = repo.get_all()
|
||||
return [{"id": u.id, "nombre": u.nombre, "email": u.email, "activo": u.activo} for u in usuarios]
|
||||
|
||||
@router.delete("/usuarios/{usuario_id}", response_model=dict)
|
||||
def eliminar_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
|
||||
repo = UsuarioRepo(db)
|
||||
exito = repo.delete_by_id(usuario_id)
|
||||
if not exito:
|
||||
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||
return {"ok": True}
|
||||
@@ -1,6 +1,6 @@
|
||||
# backend/db/conexion.py
|
||||
from entrypoint.init_db import db_credencial
|
||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
|
||||
def get_conexion():
|
||||
conexion = PostgresConexion(db_credencial)
|
||||
|
||||
+5
-2
@@ -2,7 +2,9 @@
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from backend.api.v1.router import router
|
||||
from backend.router_v1 import router
|
||||
from backend.backend_domains.llms import llm_chat_ws_endpoint_v1
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Fitz Backend",
|
||||
@@ -21,4 +23,5 @@ app.add_middleware(
|
||||
|
||||
|
||||
# Incluye las rutas de tu API
|
||||
app.include_router(router)
|
||||
app.include_router(router, prefix="/api/v1", tags=["v1"])
|
||||
app.include_router(llm_chat_ws_endpoint_v1.router)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# backend/api/router_v1.py
|
||||
|
||||
from fastapi import APIRouter
|
||||
from backend.backend_domains.experiments import charts_examples_endpoint_v1 as charts
|
||||
from backend.backend_domains.experiments import ping_endpoint_v1
|
||||
from backend.backend_domains.text_manager import text_manager_endpoint_v1
|
||||
from backend.backend_domains.llms import llm_chat_endpoint_v1
|
||||
from backend.backend_domains.usuarios_endpoint_v1 import router as usuarios_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ping_endpoint_v1.router, prefix="/ping")
|
||||
router.include_router(text_manager_endpoint_v1.router, prefix="/text_manager")
|
||||
router.include_router(charts.router, prefix="/charts")
|
||||
router.include_router(llm_chat_endpoint_v1.router, prefix="/llm", tags=["Agente LLM"])
|
||||
router.include_router(usuarios_router, prefix="/usuarios", tags=["Usuarios"])
|
||||
+26
-1016
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
|
||||
class OpenAICredencial:
|
||||
def __init__(self, titulo: str, api_key: str, organizacion: str = None, id: str = None):
|
||||
@@ -3,17 +3,17 @@ import base64
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.base import Base
|
||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from src.Security.Encriptar import Encriptar_fernet
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from domains.Security.Encriptar import Encriptar_fernet
|
||||
from entrypoint import ENV_PATH
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
|
||||
# ----------------------
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
|
||||
|
||||
TModelo = TypeVar("TModelo")
|
||||
TDominio = TypeVar("TDominio")
|
||||
@@ -0,0 +1,62 @@
|
||||
import requests
|
||||
from domains.Credenciales.ollama_credencial import OllamaCredencial
|
||||
|
||||
class OllamaCliente:
|
||||
def __init__(self, credencial: OllamaCredencial):
|
||||
"""
|
||||
Inicializa el cliente de Ollama con una instancia de OllamaCredencial.
|
||||
"""
|
||||
self.credencial = credencial
|
||||
self.base_url = self.credencial.base_url
|
||||
|
||||
# --- Chat Completions ---
|
||||
def chat_completion(self, model: str, messages: list, stream: bool = False, **kwargs):
|
||||
url = f"{self.base_url}/api/chat"
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
**kwargs
|
||||
}
|
||||
response = requests.post(url, json=payload, stream=stream)
|
||||
response.raise_for_status()
|
||||
|
||||
return self._handle_stream(response) if stream else response.json()
|
||||
|
||||
def _handle_stream(self, response):
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
try:
|
||||
parsed = line.decode("utf-8")
|
||||
# Extraer contenido si está en JSON como {"message":{"content":"..."},...}
|
||||
if parsed.startswith("{"):
|
||||
import json
|
||||
data = json.loads(parsed)
|
||||
if "message" in data and "content" in data["message"]:
|
||||
yield data["message"]["content"]
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# --- Text Completion (legacy) ---
|
||||
def completion(self, model: str, prompt: str, **kwargs):
|
||||
url = f"{self.base_url}/api/generate"
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
**kwargs
|
||||
}
|
||||
response = requests.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# --- Embeddings ---
|
||||
def embedding(self, model: str, input: str | list[str], **kwargs):
|
||||
url = f"{self.base_url}/api/embeddings"
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": input,
|
||||
**kwargs
|
||||
}
|
||||
response = requests.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -1,5 +1,5 @@
|
||||
from openai import OpenAI
|
||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
|
||||
class OpenAICliente:
|
||||
def __init__(self, credencial: OpenAICredencial):
|
||||
@@ -4,8 +4,8 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||
|
||||
class PostgresConexion(ConexionBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -0,0 +1,20 @@
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
|
||||
class OllamaCredencial:
|
||||
def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None):
|
||||
"""
|
||||
:param titulo: Nombre descriptivo para esta credencial de Ollama.
|
||||
:param base_url: URL base donde está corriendo el servidor de Ollama (por defecto: localhost).
|
||||
"""
|
||||
self.id = id if id is not None else GeneradorIDUnico("OLLA").generar()
|
||||
self.titulo = titulo
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
def get_headers(self) -> dict:
|
||||
"""
|
||||
Retorna encabezados para autenticación si se requiere en el futuro.
|
||||
Por defecto, Ollama local no usa headers especiales.
|
||||
"""
|
||||
return {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
|
||||
class PostgresCredencial:
|
||||
def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str, id: str = None):
|
||||
+7
-7
@@ -6,14 +6,14 @@ from sqlalchemy import DateTime, Text, func
|
||||
import base64
|
||||
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.base import Base
|
||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from src.Security.Encriptar import Encriptar_fernet
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from domains.Security.Encriptar import Encriptar_fernet
|
||||
|
||||
# ----------------------
|
||||
# Cargar clave maestra
|
||||
@@ -0,0 +1,443 @@
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Union, AsyncGenerator
|
||||
import re
|
||||
import json
|
||||
|
||||
from entrypoint.init_db import db_credencial
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
|
||||
|
||||
|
||||
|
||||
|
||||
class AgenteAI:
|
||||
def __init__(
|
||||
self,
|
||||
modelo: ModeloABC,
|
||||
nombre: str,
|
||||
descripcion: str,
|
||||
system_prompt: str,
|
||||
rol: str,
|
||||
objetivos: List[str],
|
||||
max_iterations: int = 1,
|
||||
memoria: Optional[MemoryConvABC] = None,
|
||||
version: str = "1.0.0",
|
||||
mcp: Optional[ClientRegistry] = None,
|
||||
output_schema: Optional[dict] = None,
|
||||
):
|
||||
self.modelo = modelo
|
||||
self.memoria = memoria
|
||||
self.output_schema = output_schema
|
||||
|
||||
self.nombre = nombre
|
||||
self.descripcion = descripcion
|
||||
self.system_prompt = system_prompt
|
||||
self.max_iterations = max_iterations
|
||||
self.rol = rol
|
||||
self.objetivos = objetivos
|
||||
self.version = version
|
||||
|
||||
self.created_at = datetime.now()
|
||||
self.updated_at = self.created_at
|
||||
self.numero_interacciones = 0
|
||||
self.mcp = mcp # <-- Aquí guardamos el registry
|
||||
|
||||
|
||||
|
||||
def actualizar_configuracion(self, **kwargs):
|
||||
for clave, valor in kwargs.items():
|
||||
if hasattr(self, clave):
|
||||
setattr(self, clave, valor)
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
|
||||
|
||||
async def generar_system_prompt(self) -> str:
|
||||
|
||||
info = f"""Eres un agente de texto y te llamas {self.nombre}
|
||||
|
||||
### Descripción:
|
||||
{self.descripcion}
|
||||
|
||||
### Rol:
|
||||
{self.rol}
|
||||
|
||||
### Objetivos:
|
||||
{chr(10).join(f"- {o}" for o in self.objetivos)}
|
||||
|
||||
### System Prompt:
|
||||
{self.system_prompt}
|
||||
|
||||
Siempre estructura tus respuestas con claridad, y termina con <END> cuando hayas completado la tarea principal del usuario.
|
||||
""".strip()
|
||||
|
||||
return info
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def construir_prompt_usuario(self, prompt_usuario: str) -> str:
|
||||
bloques = []
|
||||
|
||||
if self.mcp:
|
||||
tools_str = await self._obtener_herramientas_disponibles_str()
|
||||
bloques.append(f"### Herramientas disponibles (MCP):\n{tools_str}")
|
||||
bloques.append("""### Instrucciones para actuar con herramientas MCP:
|
||||
Eres un agente conversacional con acceso a herramientas MCP. Cuando el usuario te haga una solicitud, sigue este proceso paso a paso:
|
||||
---
|
||||
🧠 **Piensa**:
|
||||
Reflexiona en voz alta. Explica claramente qué crees que se necesita hacer y por qué.
|
||||
🎯 **Decide**:
|
||||
Elige si puedes resolverlo tú solo, si necesitas más información del usuario, o si una herramienta MCP sería útil.
|
||||
⚙️ **Actúa**:
|
||||
Si decides usar una herramienta, **escribe el bloque MCP justo después**, sin ningún texto extra después del bloque.
|
||||
---
|
||||
### Formato MCP:
|
||||
|
||||
```mcp
|
||||
{
|
||||
"server": "tools",
|
||||
"tool": "get_current_user",
|
||||
"input": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❗ REGLAS IMPORTANTES:
|
||||
|
||||
- **Puedes pensar y decidir con texto normal**, pero:
|
||||
- El **bloque MCP debe ser lo último** que aparece en tu mensaje.
|
||||
- **NO escribas nada después del bloque MCP.**
|
||||
- Solo usa `<END>` cuando:
|
||||
- hayas terminado completamente la tarea del usuario,
|
||||
- e interpretado la salida de las herramientas que usaste.
|
||||
- Puedes hacer múltiples pasos si es necesario: usar una herramienta, esperar su salida, analizarla, usar otra, etc.
|
||||
- Si decides no usar herramientas, simplemente responde como lo harías normalmente.
|
||||
- Si no estás seguro de algo, **pide aclaraciones al usuario** antes de actuar.
|
||||
|
||||
---
|
||||
|
||||
✅ Correcto:
|
||||
```mcp
|
||||
{
|
||||
"server": "tools",
|
||||
"tool": "generate_uuid",
|
||||
"input": {}
|
||||
}
|
||||
````
|
||||
🔵 Siempre usa ` ```mcp ` (con triple backtick y la palabra `mcp`) antes del JSON. No escribas nada después del bloque.
|
||||
````
|
||||
---
|
||||
|
||||
### ✅ Ejemplo correcto:
|
||||
|
||||
Necesito generar un identificador único para el usuario.
|
||||
Para eso usaré la herramienta `generate_uuid` disponible.
|
||||
|
||||
```mcp
|
||||
{
|
||||
"server": "tools",
|
||||
"tool": "generate_uuid",
|
||||
"input": {}
|
||||
}
|
||||
|
||||
""")
|
||||
|
||||
if self.memoria:
|
||||
historial = self.memoria.cargar_historial_chat()
|
||||
if historial:
|
||||
memoria_str = "\n".join(
|
||||
[f"{msg['role']}: {msg['content']}" for msg in historial]
|
||||
)
|
||||
bloques.append(f"### Memoria del chat:\n{memoria_str}")
|
||||
|
||||
if self.output_schema:
|
||||
schema_str = str(self.output_schema)
|
||||
bloques.append(f"### Salida esperada:\n{schema_str}")
|
||||
|
||||
bloques.append(f"### Prompt del usuario:\n{prompt_usuario}")
|
||||
|
||||
return "\n\n".join(bloques)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Conseguir las herramientas disponibles
|
||||
|
||||
async def _obtener_herramientas_disponibles_str(self) -> str:
|
||||
logger.info("Inicio de obtención de herramientas disponibles")
|
||||
|
||||
if not self.mcp:
|
||||
logger.warning("No se ha definido el cliente MCP.")
|
||||
return "No se han definido herramientas disponibles."
|
||||
|
||||
try:
|
||||
resultado = await self.mcp.listar_tools_por_cliente()
|
||||
tools_por_cliente = resultado.get("tools", {})
|
||||
errores = resultado.get("errores", {})
|
||||
|
||||
logger.debug(f"Tools obtenidas: {list(tools_por_cliente.keys())}")
|
||||
logger.debug(f"Errores detectados: {list(errores.keys())}")
|
||||
|
||||
herramientas = []
|
||||
|
||||
for name, tools in tools_por_cliente.items():
|
||||
if not tools:
|
||||
logger.info(f"Servidor {name} no tiene herramientas disponibles.")
|
||||
continue
|
||||
|
||||
herramientas.append(f"\n🔌 Server: {name}")
|
||||
for tool in tools:
|
||||
props = tool.inputSchema.get("properties", {})
|
||||
parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items())
|
||||
herramientas.append(f"""Nombre: {tool.name}
|
||||
Descripción: {tool.description}
|
||||
Parámetros:
|
||||
{parametros}
|
||||
""")
|
||||
logger.debug(f"Herramienta agregada: {tool.name} del servidor {name}")
|
||||
|
||||
if errores:
|
||||
herramientas.append("\n⚠️ Los siguientes servidores no están disponibles:")
|
||||
for name, error in errores.items():
|
||||
herramientas.append(f"- {name}: {error}")
|
||||
logger.warning(f"Servidor con error: {name} -> {error}")
|
||||
|
||||
logger.info("Finalización de obtención de herramientas exitosamente.")
|
||||
return "\n".join(herramientas) or "No hay herramientas disponibles actualmente."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado al obtener herramientas: {str(e)}", exc_info=True)
|
||||
return "Se produjo un error al obtener las herramientas disponibles."
|
||||
|
||||
|
||||
|
||||
|
||||
### Formatear prompt para agentes
|
||||
|
||||
def _formatear_prompt(self, mensajes: List[dict]) -> str:
|
||||
return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes])
|
||||
|
||||
|
||||
|
||||
### Ejecutar codigo MCP
|
||||
|
||||
async def ejecutar_bloque_mcp(self, respuesta: str) -> Optional[str]:
|
||||
logger.info("Iniciando ejecución de bloque MCP.")
|
||||
|
||||
patron = r"```mcp\s*(\{.*?\})\s*```"
|
||||
match = re.search(patron, respuesta, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
patron_incorrecto = r"```[\s]*\{.*?\}[\s]*```"
|
||||
if re.search(patron_incorrecto, respuesta, re.DOTALL):
|
||||
logger.warning("Bloque detectado sin especificador `mcp`.")
|
||||
return "Advertencia: Usaste un bloque de herramienta MCP pero olvidaste indicar el lenguaje `mcp`. Corrige el bloque a: ```mcp { ... } ```"
|
||||
logger.info("No se encontró ningún bloque MCP en la respuesta.")
|
||||
return None
|
||||
|
||||
try:
|
||||
bloque_json_str = match.group(1)
|
||||
logger.debug(f"Bloque MCP detectado: {bloque_json_str}")
|
||||
|
||||
bloque = json.loads(bloque_json_str)
|
||||
|
||||
server_name = bloque["server"]
|
||||
tool_name = bloque["tool"]
|
||||
input_args = bloque.get("input", {})
|
||||
|
||||
logger.info(f"Bloque MCP válido. Servidor: {server_name}, Herramienta: {tool_name}")
|
||||
logger.debug(f"Parámetros de entrada: {input_args}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al interpretar el bloque MCP: {e}", exc_info=True)
|
||||
return f"Error al interpretar el bloque MCP: {e}"
|
||||
|
||||
try:
|
||||
cliente_mcp = self.mcp.get(server_name)
|
||||
except KeyError:
|
||||
logger.warning(f"No se encontró el cliente MCP para el servidor '{server_name}'.")
|
||||
return f"No se encontró el cliente MCP para el servidor '{server_name}'"
|
||||
|
||||
try:
|
||||
logger.info(f"Ejecutando herramienta '{tool_name}' en servidor '{server_name}' con argumentos: {json.dumps(input_args, ensure_ascii=False)}")
|
||||
|
||||
async with cliente_mcp:
|
||||
resultado = await cliente_mcp.call_tool(tool_name, input_args)
|
||||
logger.info(f"Ejecución completada exitosamente. Resultado: {resultado}")
|
||||
return str(resultado)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}", exc_info=True)
|
||||
return f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}"
|
||||
|
||||
|
||||
|
||||
### Ejecutar VARIOS bloques MCP
|
||||
|
||||
async def ejecutar_multiples_bloques_mcp(self, respuesta: str) -> Optional[List[str]]:
|
||||
logger.info("Buscando múltiples bloques MCP en la respuesta.")
|
||||
|
||||
patron = r"```mcp\s*(\{.*?\})\s*```"
|
||||
matches = re.finditer(patron, respuesta, re.DOTALL)
|
||||
|
||||
resultados = []
|
||||
hubo_bloques = False
|
||||
|
||||
for match in matches:
|
||||
hubo_bloques = True
|
||||
bloque_json_str = match.group(1)
|
||||
try:
|
||||
bloque = json.loads(bloque_json_str)
|
||||
server_name = bloque["server"]
|
||||
tool_name = bloque["tool"]
|
||||
input_args = bloque.get("input", {})
|
||||
|
||||
logger.info(f"Ejecutando bloque MCP: servidor={server_name}, herramienta={tool_name}")
|
||||
|
||||
try:
|
||||
cliente_mcp = self.mcp.get(server_name)
|
||||
except KeyError:
|
||||
msg = f"No se encontró el cliente MCP para el servidor '{server_name}'"
|
||||
logger.warning(msg)
|
||||
resultados.append(msg)
|
||||
continue
|
||||
|
||||
async with cliente_mcp:
|
||||
resultado = await cliente_mcp.call_tool(tool_name, input_args)
|
||||
resultado_str = f"[{server_name}.{tool_name}] → {resultado}"
|
||||
resultados.append(resultado_str)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error al procesar bloque MCP: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
resultados.append(error_msg)
|
||||
|
||||
if not hubo_bloques:
|
||||
logger.info("No se encontró ningún bloque MCP en la respuesta.")
|
||||
return None
|
||||
|
||||
return resultados
|
||||
|
||||
|
||||
|
||||
|
||||
###----------- Funcion para interactuar
|
||||
|
||||
async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]:
|
||||
mensaje_usuario = await self.construir_prompt_usuario(prompt)
|
||||
contexto = [{"role": "user", "content": mensaje_usuario}]
|
||||
prompt_final = self._formatear_prompt(contexto)
|
||||
|
||||
respuesta = await self.modelo.responder(
|
||||
prompt=prompt_final,
|
||||
system_prompt=await self.generar_system_prompt(),
|
||||
stream=stream
|
||||
)
|
||||
|
||||
return respuesta
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###----------- Funcion para interactuar en bucle
|
||||
|
||||
async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]:
|
||||
respuestas = [] if not stream else None
|
||||
respuesta_anterior = ""
|
||||
resultado_mcp_anterior = None # <-- Guarda último resultado del MCP
|
||||
iteration = 0
|
||||
prompt_original = prompt.strip()
|
||||
|
||||
async def generador():
|
||||
nonlocal iteration, respuesta_anterior, resultado_mcp_anterior
|
||||
|
||||
while self.max_iterations == 0 or iteration < self.max_iterations:
|
||||
instruccion_fin = (
|
||||
"\n\nIMPORTANTE: Cuando hayas respondido completamente a la pregunta original del usuario y no requieras más pasos, "
|
||||
"escribe <END> para indicar que has terminado."
|
||||
)
|
||||
|
||||
if iteration == 0:
|
||||
prompt_actual = prompt_original + instruccion_fin
|
||||
else:
|
||||
prompt_actual = (
|
||||
f"Esta es la pregunta original:\n{prompt_original}\n\n"
|
||||
f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n\n"
|
||||
f"{instruccion_fin}"
|
||||
)
|
||||
|
||||
if resultado_mcp_anterior:
|
||||
prompt_actual += (
|
||||
"\n\nEsta fue la salida de la herramienta que usaste:\n"
|
||||
f"{resultado_mcp_anterior}\n\n"
|
||||
"Úsala para seguir resolviendo el problema o tomar una nueva decisión."
|
||||
)
|
||||
|
||||
mensaje_usuario = await self.construir_prompt_usuario(prompt_actual)
|
||||
contexto = [{"role": "user", "content": mensaje_usuario}]
|
||||
prompt_final = self._formatear_prompt(contexto)
|
||||
|
||||
respuesta = await self.modelo.responder(
|
||||
prompt=prompt_final,
|
||||
system_prompt=await self.generar_system_prompt(),
|
||||
stream=stream
|
||||
)
|
||||
|
||||
if stream:
|
||||
buffer_respuesta = ""
|
||||
async for token in respuesta:
|
||||
buffer_respuesta += token
|
||||
yield token
|
||||
respuesta_anterior = buffer_respuesta
|
||||
else:
|
||||
respuestas.append(respuesta)
|
||||
respuesta_anterior = respuesta
|
||||
|
||||
# Revisar y ejecutar bloque MCP si existe
|
||||
resultado_mcp_anterior = None
|
||||
if "```mcp" in respuesta_anterior:
|
||||
resultados_mcp = await self.ejecutar_multiples_bloques_mcp(respuesta_anterior)
|
||||
if resultados_mcp:
|
||||
resultado_mcp_anterior = "\n".join(resultados_mcp)
|
||||
|
||||
if stream:
|
||||
yield "\n" + resultado_mcp_anterior
|
||||
else:
|
||||
respuestas.append(resultado_mcp_anterior)
|
||||
|
||||
# Guardar historial si hay memoria
|
||||
if self.memoria:
|
||||
self.memoria.guardar_turno("user", prompt_actual)
|
||||
self.memoria.guardar_turno("assistant", respuesta_anterior)
|
||||
|
||||
self.numero_interacciones += 1
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
if "<end>" in respuesta_anterior.lower() and "```mcp" not in respuesta_anterior.lower():
|
||||
break
|
||||
|
||||
iteration += 1
|
||||
|
||||
return generador() if stream else await generador_to_list(generador())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Helper para consumir generador asincrónico si no es stream
|
||||
async def generador_to_list(gen: AsyncGenerator[str, None]) -> List[str]:
|
||||
buffer = ""
|
||||
async for chunk in gen:
|
||||
buffer += chunk
|
||||
return [buffer]
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import List
|
||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
|
||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from src.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
|
||||
class OpenAIEmbedder(EmbedderABC):
|
||||
def __init__(self, credencial: OpenAICredencial,
|
||||
+8
-8
@@ -3,15 +3,15 @@ from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.base import Base
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
|
||||
# ----------------------
|
||||
# Cargar configuración desde .env si se requiere
|
||||
@@ -9,6 +9,7 @@ from fastmcp.client.transports import (
|
||||
)
|
||||
from mcp.types import *
|
||||
from fastmcp.exceptions import ClientError
|
||||
import asyncio
|
||||
|
||||
|
||||
class MCPClient:
|
||||
@@ -52,10 +53,13 @@ class MCPClient:
|
||||
|
||||
# Delegación MCP
|
||||
|
||||
async def call_tool(
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
||||
return await self.client.call_tool(name, arguments)
|
||||
async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[TextContent | ImageContent | EmbeddedResource]:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self.client.call_tool(name, arguments), timeout=10
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise RuntimeError(f"Timeout al ejecutar herramienta '{name}'")
|
||||
|
||||
async def get_prompt(
|
||||
self, name: str, arguments: dict[str, str] | None = None
|
||||
@@ -1,4 +1,4 @@
|
||||
from src.Llms.MCPs.McpClient import MCPClient
|
||||
from domains.Llms.MCPs.McpClient import MCPClient
|
||||
from typing import Any
|
||||
|
||||
class ClientRegistry:
|
||||
@@ -22,35 +22,35 @@ class ClientRegistry:
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self._clients
|
||||
|
||||
async def listar_tools_por_cliente(self) -> dict[str, list[Any]]:
|
||||
resultado = {}
|
||||
async def listar_tools_por_cliente(self) -> dict[str, Any]:
|
||||
resultado = {"tools": {}, "errores": {}}
|
||||
for name, wrapper in self._clients.items():
|
||||
try:
|
||||
async with wrapper:
|
||||
resultado[name] = await wrapper.list_tools()
|
||||
resultado["tools"][name] = await wrapper.list_tools()
|
||||
except Exception as e:
|
||||
print(f"[TOOLS] ❌ Error en '{name}': {e}")
|
||||
resultado[name] = []
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["tools"][name] = []
|
||||
return resultado
|
||||
|
||||
async def listar_prompts_por_cliente(self) -> dict[str, list[Any]]:
|
||||
resultado = {}
|
||||
async def listar_prompts_por_cliente(self) -> dict[str, Any]:
|
||||
resultado = {"prompts": {}, "errores": {}}
|
||||
for name, wrapper in self._clients.items():
|
||||
try:
|
||||
async with wrapper:
|
||||
resultado[name] = await wrapper.list_prompts()
|
||||
resultado["prompts"][name] = await wrapper.list_prompts()
|
||||
except Exception as e:
|
||||
print(f"[PROMPTS] ❌ Error en '{name}': {e}")
|
||||
resultado[name] = []
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["prompts"][name] = []
|
||||
return resultado
|
||||
|
||||
async def listar_resources_por_cliente(self) -> dict[str, list[Any]]:
|
||||
resultado = {}
|
||||
async def listar_resources_por_cliente(self) -> dict[str, Any]:
|
||||
resultado = {"resources": {}, "errores": {}}
|
||||
for name, wrapper in self._clients.items():
|
||||
try:
|
||||
async with wrapper:
|
||||
resultado[name] = await wrapper.list_resources()
|
||||
resultado["resources"][name] = await wrapper.list_resources()
|
||||
except Exception as e:
|
||||
print(f"[RESOURCES] ❌ Error en '{name}': {e}")
|
||||
resultado[name] = []
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["resources"][name] = []
|
||||
return resultado
|
||||
@@ -0,0 +1,48 @@
|
||||
# server_runner.py
|
||||
import subprocess
|
||||
import asyncio
|
||||
import socket
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
async def wait_for_port(host: str, port: int, timeout: float = 10.0):
|
||||
for _ in range(int(timeout * 10)):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=0.5):
|
||||
return True
|
||||
except (OSError, ConnectionRefusedError):
|
||||
await asyncio.sleep(0.1)
|
||||
raise TimeoutError(f"No se pudo conectar al servidor en {host}:{port}")
|
||||
|
||||
class MCPServerRunner:
|
||||
def __init__(self, server_script_path: str, python_path: str = "python"):
|
||||
self.server_script_path = server_script_path
|
||||
self.python_path = python_path
|
||||
self.port: int = self._extraer_puerto()
|
||||
self.process: subprocess.Popen | None = None
|
||||
|
||||
def _extraer_puerto(self) -> int:
|
||||
contenido = Path(self.server_script_path).read_text(encoding="utf-8")
|
||||
coincidencias = re.findall(r"port\s*=\s*(\d+)", contenido)
|
||||
if not coincidencias:
|
||||
raise ValueError(f"No se pudo detectar el puerto en {self.server_script_path}")
|
||||
return int(coincidencias[0])
|
||||
|
||||
async def start(self):
|
||||
if self.process is None or self.process.poll() is not None:
|
||||
self.process = subprocess.Popen(
|
||||
[self.python_path, self.server_script_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
await wait_for_port("127.0.0.1", self.port)
|
||||
print(f"🟢 Servidor MCP iniciado en puerto {self.port}")
|
||||
|
||||
async def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
print("🔴 Servidor MCP detenido")
|
||||
@@ -0,0 +1,133 @@
|
||||
from fastmcp import FastMCP
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Directorio base seguro
|
||||
SANDBOX_DIR = Path("./sandbox").resolve()
|
||||
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def safe_path(requested_path: str) -> Path:
|
||||
"""Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
|
||||
# Normaliza la ruta quitando el primer '/'
|
||||
normalized = requested_path.strip().lstrip("/")
|
||||
full_path = (SANDBOX_DIR / normalized).resolve()
|
||||
|
||||
if not full_path.is_relative_to(SANDBOX_DIR):
|
||||
raise ValueError("Ruta fuera del directorio permitido.")
|
||||
return full_path
|
||||
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
|
||||
def read_file(path: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
if not file_path.is_file():
|
||||
raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
return f"⚠️ Error al leer archivo '{path}': {str(e)}"
|
||||
|
||||
|
||||
@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
|
||||
def write_file(path: str, content: str) -> str:
|
||||
file_path = safe_path(path)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
return "Archivo guardado correctamente."
|
||||
|
||||
@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
|
||||
def delete_file(path: str) -> str:
|
||||
file_path = safe_path(path)
|
||||
if not file_path.is_file():
|
||||
raise FileNotFoundError("Archivo no encontrado.")
|
||||
file_path.unlink()
|
||||
return "Archivo eliminado."
|
||||
|
||||
@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
|
||||
def create_folder(path: str) -> str:
|
||||
folder_path = safe_path(path)
|
||||
folder_path.mkdir(parents=True, exist_ok=True)
|
||||
return "Carpeta creada."
|
||||
|
||||
|
||||
|
||||
@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
|
||||
def list_directory(path: str = ".") -> list[str]:
|
||||
folder = safe_path(path)
|
||||
if not folder.is_dir():
|
||||
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||
return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())
|
||||
|
||||
@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
|
||||
def tree(path: str = ".", depth: int = 3) -> str:
|
||||
base = safe_path(path)
|
||||
if not base.is_dir():
|
||||
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||
|
||||
tree_output = []
|
||||
|
||||
def walk(dir_path: Path, prefix: str = "", level: int = 0):
|
||||
if level > depth:
|
||||
return
|
||||
entries = sorted(dir_path.iterdir())
|
||||
for i, entry in enumerate(entries):
|
||||
connector = "└── " if i == len(entries) - 1 else "├── "
|
||||
tree_output.append(f"{prefix}{connector}{entry.name}")
|
||||
if entry.is_dir():
|
||||
extension = " " if i == len(entries) - 1 else "│ "
|
||||
walk(entry, prefix + extension, level + 1)
|
||||
|
||||
tree_output.append(f"{base.name}/")
|
||||
walk(base)
|
||||
return "\n".join(tree_output)
|
||||
|
||||
@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
|
||||
def file_info(path: str) -> dict:
|
||||
fpath = safe_path(path)
|
||||
if not fpath.exists():
|
||||
raise FileNotFoundError("Archivo no encontrado.")
|
||||
return {
|
||||
"nombre": fpath.name,
|
||||
"tipo": "carpeta" if fpath.is_dir() else "archivo",
|
||||
"tamaño_bytes": fpath.stat().st_size,
|
||||
"última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
|
||||
"relativo": str(fpath.relative_to(SANDBOX_DIR))
|
||||
}
|
||||
|
||||
@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
|
||||
def copy_file(src: str, dest: str) -> str:
|
||||
src_path = safe_path(src)
|
||||
dest_path = safe_path(dest)
|
||||
if src_path.is_dir():
|
||||
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
|
||||
else:
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src_path, dest_path)
|
||||
return "Copia completada."
|
||||
|
||||
@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
|
||||
def move_file(src: str, dest: str) -> str:
|
||||
src_path = safe_path(src)
|
||||
dest_path = safe_path(dest)
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
src_path.rename(dest_path)
|
||||
return "Movimiento completado."
|
||||
|
||||
@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
|
||||
def clear_folder(path: str) -> str:
|
||||
folder_path = safe_path(path)
|
||||
if not folder_path.is_dir():
|
||||
raise NotADirectoryError("La ruta no es una carpeta.")
|
||||
for item in folder_path.iterdir():
|
||||
if item.is_file() or item.is_symlink():
|
||||
item.unlink()
|
||||
elif item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
return "Carpeta vaciada."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")
|
||||
|
||||
+2
-2
@@ -87,6 +87,6 @@ def is_prime(n: int) -> bool:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
|
||||
|
||||
mcp.run(transport="stdio")
|
||||
# mcp.run(transport="stdio")
|
||||
+1
@@ -66,3 +66,4 @@ def current_timestamp() -> float:
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select, delete
|
||||
from typing import Literal
|
||||
|
||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from src.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
||||
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
||||
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
|
||||
|
||||
class MemoryConvPostgres(MemoryConvABC):
|
||||
@@ -0,0 +1,67 @@
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from typing import AsyncGenerator, Union
|
||||
from domains.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
|
||||
import asyncio
|
||||
|
||||
class ModeloOllama(ModeloABC):
|
||||
def __init__(
|
||||
self,
|
||||
cliente: OllamaCliente,
|
||||
model: str = "llama3",
|
||||
id: str = None,
|
||||
temperature: float = 0.7,
|
||||
top_p: float = 1.0,
|
||||
top_k: int = None,
|
||||
frecuencia_penalizacion: float = 0.0,
|
||||
num_tokens_maximos: int = 512
|
||||
):
|
||||
if not isinstance(cliente, OllamaCliente):
|
||||
raise TypeError("El parámetro 'cliente' debe ser una instancia de OllamaCliente")
|
||||
|
||||
|
||||
self.id = id if id else GeneradorIDUnico("MOOL").generar()
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
frecuencia_penalizacion=frecuencia_penalizacion,
|
||||
num_tokens_maximos=num_tokens_maximos
|
||||
)
|
||||
self.cliente = cliente
|
||||
|
||||
async def responder(
|
||||
self,
|
||||
prompt: str,
|
||||
system_prompt: str = "",
|
||||
stream: bool = False,
|
||||
**kwargs
|
||||
) -> Union[str, AsyncGenerator[str, None]]:
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
def sync_call():
|
||||
return self.cliente.chat_completion(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=self.temperature,
|
||||
top_p=self.top_p,
|
||||
max_tokens=self.num_tokens_maximos,
|
||||
frequency_penalty=self.frecuencia_penalizacion,
|
||||
stream=stream,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
resultado = await loop.run_in_executor(None, sync_call)
|
||||
|
||||
if stream:
|
||||
async def generador():
|
||||
for token in resultado:
|
||||
yield token
|
||||
return generador()
|
||||
else:
|
||||
return resultado.choices[0].message.content
|
||||
@@ -1,6 +1,6 @@
|
||||
from src.Llms.Modelos.Base_model import ModeloABC
|
||||
from src.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Union
|
||||
|
||||
@@ -2,15 +2,15 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.base import Base
|
||||
from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
|
||||
|
||||
# ----------------------
|
||||
# Cargar clave maestra
|
||||
@@ -0,0 +1,62 @@
|
||||
from loguru import logger
|
||||
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||
|
||||
class LoggerDB:
|
||||
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
|
||||
|
||||
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
|
||||
|
||||
# 🔥 Elimina todos los sinks activos, incluso los automáticos
|
||||
logger.remove()
|
||||
|
||||
self.conexion = PostgresConexion(credencial)
|
||||
self.engine = self.conexion.get_engine()
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
self.nombre_tabla = nombre_tabla
|
||||
self.created_by = created_by
|
||||
|
||||
self.modelo_logger = self._generar_modelo_logger()
|
||||
self._crear_tabla_si_no_existe()
|
||||
logger.add(self._sink, level="DEBUG")
|
||||
|
||||
def _generar_modelo_logger(self):
|
||||
class LoggerTable(Model_base):
|
||||
__tablename__ = self.nombre_tabla
|
||||
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
nivel = Column(String, nullable=False)
|
||||
mensaje = Column(Text, nullable=False)
|
||||
fecha = Column(TIMESTAMP(timezone=True), nullable=False)
|
||||
modulo = Column(String, nullable=True)
|
||||
funcion = Column(String, nullable=True)
|
||||
linea = Column(Integer, nullable=True)
|
||||
return LoggerTable
|
||||
|
||||
def _crear_tabla_si_no_existe(self):
|
||||
self.modelo_logger.__table__.create(self.engine, checkfirst=True)
|
||||
|
||||
def _sink(self, message):
|
||||
record = message.record
|
||||
try:
|
||||
session = self.Session()
|
||||
log_entry = self.modelo_logger(
|
||||
nivel=record["level"].name,
|
||||
mensaje=record["message"],
|
||||
fecha=record["time"],
|
||||
modulo=record["module"],
|
||||
funcion=record["function"],
|
||||
linea=record["line"],
|
||||
sys_created_by=self.created_by
|
||||
)
|
||||
session.add(log_entry)
|
||||
session.commit()
|
||||
session.close()
|
||||
except SQLAlchemyError as e:
|
||||
print(f"[LoggerDB] Error guardando log en BD: {e}")
|
||||
@@ -0,0 +1,190 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import random
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Tab import Tab
|
||||
|
||||
class ElementoWeb:
|
||||
def __init__(self, tab: "Tab", object_id: Optional[str]):
|
||||
self.tab = tab
|
||||
self.object_id = object_id
|
||||
self._node_id = None # Lazy resolved
|
||||
|
||||
@classmethod
|
||||
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
|
||||
inst = cls(tab, object_id=None)
|
||||
inst._node_id = node_id
|
||||
return inst
|
||||
|
||||
async def _asegurar_object_id(self):
|
||||
if not self.object_id and self._node_id:
|
||||
try:
|
||||
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
|
||||
self.object_id = resolved["object"]["objectId"]
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo resolver objectId desde nodeId: {e}")
|
||||
|
||||
async def scroll_into_view(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print("📜 Elemento desplazado a la vista.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
|
||||
|
||||
async def click(self):
|
||||
try:
|
||||
await self.scroll_into_view()
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
|
||||
|
||||
# Intenta obtener coordenadas del nodo
|
||||
node_result = await self.tab._enviar("DOM.describeNode", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
node_id = node_result["node"]["nodeId"]
|
||||
|
||||
try:
|
||||
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
|
||||
content = box_model["model"]["content"]
|
||||
x = (content[0] + content[2]) / 2
|
||||
y = (content[1] + content[5]) / 2
|
||||
except:
|
||||
quads_result = await self.tab._enviar("DOM.getContentQuads", {"nodeId": node_id})
|
||||
quad = quads_result["quads"][0]
|
||||
x = (quad[0] + quad[4]) / 2
|
||||
y = (quad[1] + quad[5]) / 2
|
||||
|
||||
# 🧠 Enfocar el elemento antes de clickear
|
||||
await self.tab._enviar("DOM.focus", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
|
||||
# 🎯 Movimiento humanoide opcional
|
||||
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
|
||||
steps = random.randint(5, 12)
|
||||
for i in range(1, steps + 1):
|
||||
curr_x = start_x + (x - start_x) * i / steps + random.uniform(-1, 1)
|
||||
curr_y = start_y + (y - start_y) * i / steps + random.uniform(-1, 1)
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseMoved",
|
||||
"x": curr_x,
|
||||
"y": curr_y,
|
||||
})
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# 👆 Mouse Down
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mousePressed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
await asyncio.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# 👇 Mouse Up
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseReleased",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# 🖱️ Click manual adicional
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseClicked",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
if self.tab.verbose:
|
||||
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer click físico: {e}")
|
||||
print("🧪 Intentando fallback con JavaScript click()...")
|
||||
await self.click_js()
|
||||
|
||||
async def click_js(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
print("⚠️ No se puede hacer click JS: objectId no disponible.")
|
||||
return
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.click(); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print("🖱️ Click simulado por JavaScript (element.click())")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar click en JS: {e}")
|
||||
|
||||
async def obtener_texto(self) -> Optional[str]:
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
result = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { return this.textContent; }",
|
||||
"returnByValue": True
|
||||
})
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener texto del elemento: {e}")
|
||||
return None
|
||||
|
||||
async def escribir_texto(self, texto: str):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": f"function() {{ this.value = {json.dumps(texto)}; this.dispatchEvent(new Event('input')); }}",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print(f"⌨️ Texto escrito en elemento: '{texto}'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al escribir texto: {e}")
|
||||
|
||||
|
||||
async def encontrar_hijo_clickeable(self) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
resultado = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": """
|
||||
function() {
|
||||
const candidatos = this.querySelectorAll("span, div, a, button");
|
||||
for (const el of candidatos) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const visible = style.display !== "none" && style.visibility !== "hidden";
|
||||
const interactivo = style.pointerEvents !== "none";
|
||||
if (visible && interactivo) return el;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
""",
|
||||
"returnByValue": False
|
||||
})
|
||||
if "result" in resultado and "objectId" in resultado["result"]:
|
||||
return ElementoWeb(self.tab, resultado["result"]["objectId"])
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo encontrar hijo clickeable: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,193 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Optional
|
||||
import aiohttp
|
||||
|
||||
|
||||
class Navegador:
|
||||
def __init__(self,
|
||||
chrome_path: str,
|
||||
user_data_dir: str,
|
||||
id: Optional[int] = None,
|
||||
download_dir: Optional[str] = None,
|
||||
debugging_port: int = 9222,
|
||||
headless: bool = False,
|
||||
user_agent: Optional[str] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"):
|
||||
self.chrome_path = chrome_path
|
||||
self.user_data_dir = user_data_dir
|
||||
self.id = id
|
||||
self.download_dir = download_dir or os.path.join(self.user_data_dir, "downloads")
|
||||
self.debugging_port = debugging_port
|
||||
self.headless = headless
|
||||
self.user_agent = user_agent
|
||||
self.chrome_process: Optional[subprocess.Popen] = None
|
||||
|
||||
async def _esperar_debugger(self, timeout=10):
|
||||
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||
for _ in range(timeout * 10): # 10 intentos por segundo
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
print("✅ Chrome listo para debugging.")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.1)
|
||||
raise RuntimeError("❌ Chrome no respondió en el puerto de debugging.")
|
||||
|
||||
def _preconfigurar_preferencias(self):
|
||||
prefs_path = os.path.join(self.user_data_dir, "Default", "Preferences")
|
||||
os.makedirs(os.path.dirname(prefs_path), exist_ok=True)
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
|
||||
prefs = {
|
||||
"profile": {
|
||||
"exit_type": "Normal",
|
||||
"exited_cleanly": True
|
||||
},
|
||||
"browser": {
|
||||
"has_seen_welcome_page": True
|
||||
},
|
||||
"distribution": {
|
||||
"skip_first_run_ui": True
|
||||
},
|
||||
"download": {
|
||||
"default_directory": self.download_dir,
|
||||
"prompt_for_download": False,
|
||||
"directory_upgrade": True,
|
||||
"extensions_to_open": ""
|
||||
},
|
||||
"savefile": {
|
||||
"default_directory": self.download_dir
|
||||
}
|
||||
}
|
||||
|
||||
if os.path.exists(prefs_path):
|
||||
try:
|
||||
with open(prefs_path, "r", encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
existing.update(prefs)
|
||||
prefs = existing
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with open(prefs_path, "w", encoding="utf-8") as f:
|
||||
json.dump(prefs, f, indent=2)
|
||||
|
||||
def _build_args(self):
|
||||
os.makedirs(self.user_data_dir, exist_ok=True)
|
||||
self._preconfigurar_preferencias()
|
||||
|
||||
args = [
|
||||
f"--remote-debugging-port={self.debugging_port}",
|
||||
f"--user-data-dir={self.user_data_dir}",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
# "--disable-web-security",
|
||||
# "--disable-extensions",
|
||||
# "--disable-dev-shm-usage",
|
||||
"--disable-infobars",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-default-apps",
|
||||
"--mute-audio",
|
||||
"--window-size=1024,1024",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-features=DefaultBrowserPrompt",
|
||||
"--disable-component-update",
|
||||
"--disable-background-networking",
|
||||
"--disable-sync",
|
||||
"--disable-translate",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--metrics-recording-only",
|
||||
"--safebrowsing-disable-auto-update",
|
||||
|
||||
|
||||
]
|
||||
|
||||
if self.headless:
|
||||
args.append("--headless=new")
|
||||
|
||||
if self.user_agent:
|
||||
args.append(f"--user-agent={self.user_agent}")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
|
||||
async def inyectar_spoof_chrome(self):
|
||||
script = """
|
||||
window.chrome = {
|
||||
app: {
|
||||
isInstalled: false,
|
||||
InstallState: {
|
||||
DISABLED: 'disabled',
|
||||
INSTALLED: 'installed',
|
||||
NOT_INSTALLED: 'not_installed'
|
||||
},
|
||||
RunningState: {
|
||||
CANNOT_RUN: 'cannot_run',
|
||||
READY_TO_RUN: 'ready_to_run',
|
||||
RUNNING: 'running'
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
|
||||
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
|
||||
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
|
||||
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' }
|
||||
}
|
||||
};
|
||||
"""
|
||||
|
||||
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
targets = await resp.json()
|
||||
|
||||
for target in targets:
|
||||
if "webSocketDebuggerUrl" not in target:
|
||||
continue
|
||||
|
||||
target_id = target["id"]
|
||||
async with session.post(
|
||||
f"http://127.0.0.1:{self.debugging_port}/json/protocol",
|
||||
json={"targetId": target_id}
|
||||
):
|
||||
pass # CDP protocol fetch optional
|
||||
|
||||
async with session.post(
|
||||
f"http://127.0.0.1:{self.debugging_port}/json/send",
|
||||
json={
|
||||
"id": 1,
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {"source": script}
|
||||
}
|
||||
) as inject_resp:
|
||||
if inject_resp.status == 200:
|
||||
print("✅ chrome.* spoof inyectado.")
|
||||
|
||||
|
||||
async def iniciar(self):
|
||||
args = self._build_args()
|
||||
self.chrome_process = subprocess.Popen([self.chrome_path] + args)
|
||||
print(f"Chrome iniciado (headless={self.headless}). Esperando disponibilidad del debugger...")
|
||||
await self._esperar_debugger()
|
||||
await self.inyectar_spoof_chrome()
|
||||
|
||||
async def cerrar(self):
|
||||
if self.chrome_process and self.chrome_process.poll() is None:
|
||||
self.chrome_process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.to_thread(self.chrome_process.wait), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
self.chrome_process.kill()
|
||||
print("🛑 Chrome cerrado correctamente.")
|
||||
@@ -0,0 +1,138 @@
|
||||
import aiohttp
|
||||
import websockets
|
||||
import json
|
||||
import asyncio
|
||||
from .Tab import Tab
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
||||
class Scrapper:
|
||||
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
|
||||
self.debugging_url = debugging_url
|
||||
self.tabs: list[Tab] = []
|
||||
|
||||
async def _crear_tab_websocket_url(self, target_url: str = "about:blank") -> str:
|
||||
"""
|
||||
Crea una nueva pestaña usando el método oficial Target.createTarget
|
||||
y devuelve su WebSocketDebuggerUrl.
|
||||
"""
|
||||
# 1. Obtener el WebSocket general del browser (root)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json/version") as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError("No se pudo obtener información del navegador")
|
||||
data = await resp.json()
|
||||
browser_ws_url = data["webSocketDebuggerUrl"]
|
||||
|
||||
# 2. Conectarse al WebSocket del browser
|
||||
async with websockets.connect(browser_ws_url) as websocket:
|
||||
# 3. Enviar comando para crear target
|
||||
msg_id = 1
|
||||
await websocket.send(json.dumps({
|
||||
"id": msg_id,
|
||||
"method": "Target.createTarget",
|
||||
"params": {
|
||||
"url": target_url,
|
||||
"newWindow": False
|
||||
}
|
||||
}))
|
||||
|
||||
# 4. Esperar respuesta con el targetId
|
||||
while True:
|
||||
respuesta = await websocket.recv()
|
||||
data = json.loads(respuesta)
|
||||
if data.get("id") == msg_id:
|
||||
target_id = data["result"]["targetId"]
|
||||
break
|
||||
|
||||
# 5. Esperar a que el target aparezca en /json
|
||||
for _ in range(30): # máximo ~3 segundos
|
||||
await asyncio.sleep(0.1)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json") as resp:
|
||||
if resp.status == 200:
|
||||
tabs = await resp.json()
|
||||
for tab in tabs:
|
||||
if tab.get("id") == target_id:
|
||||
return tab["webSocketDebuggerUrl"]
|
||||
|
||||
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
|
||||
|
||||
async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
|
||||
websocket_url = await self._crear_tab_websocket_url()
|
||||
tab = await Tab.crear_desde_websocket(websocket_url)
|
||||
self.tabs.append(tab)
|
||||
|
||||
if url:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await tab.navegar(url, wait_time)
|
||||
else:
|
||||
print("⚠️ No se especificó URL. La pestaña se creó pero no se navegó a ninguna página.")
|
||||
|
||||
return tab
|
||||
|
||||
async def cerrar_todos(self):
|
||||
for tab in list(self.tabs):
|
||||
await tab.cerrar()
|
||||
self.tabs.clear()
|
||||
|
||||
def get_tab(self, identifier: str) -> Optional[Tab]:
|
||||
"""
|
||||
Devuelve una instancia de Tab según su WebSocket URL o su ID final (extraído del WebSocket URL).
|
||||
Acepta:
|
||||
- ws_url completo: ws://127.0.0.1:9222/devtools/page/XYZ
|
||||
- id directo: XYZ
|
||||
"""
|
||||
for tab in self.tabs:
|
||||
# Comparar directamente contra ws_url
|
||||
if tab.ws_url == identifier:
|
||||
return tab
|
||||
|
||||
# Comparar contra el ID extraído
|
||||
ws_id = tab.ws_url.rsplit("/", 1)[-1]
|
||||
if ws_id == identifier:
|
||||
return tab
|
||||
|
||||
return None
|
||||
|
||||
async def obtener_tabs_existentes(self) -> list[Tab]:
|
||||
"""
|
||||
Recupera todas las pestañas de tipo 'page' que no están ya en self.tabs,
|
||||
las conecta y devuelve como lista. Muestra resumen limpio por consola.
|
||||
"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json") as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError("No se pudo obtener la lista de pestañas")
|
||||
|
||||
tabs_info = await resp.json()
|
||||
|
||||
print("\n🧾 Pestañas activas (filtradas: solo 'type': 'page'):\n")
|
||||
nuevas_tabs = []
|
||||
for idx, tab_info in enumerate(tabs_info, start=1):
|
||||
tipo = tab_info.get("type")
|
||||
if tipo != "page":
|
||||
continue # Filtrar todo lo que no sea página visible
|
||||
|
||||
ws_url = tab_info.get("webSocketDebuggerUrl")
|
||||
tab_id = tab_info.get("id")
|
||||
title = tab_info.get("title", "<Sin título>")
|
||||
url = tab_info.get("url", "<Sin URL>")
|
||||
|
||||
# Verifica si ya la tienes cargada
|
||||
if any(t.ws_url == ws_url for t in self.tabs):
|
||||
continue
|
||||
|
||||
# Conectar
|
||||
try:
|
||||
tab = await Tab.crear_desde_websocket(ws_url)
|
||||
self.tabs.append(tab)
|
||||
nuevas_tabs.append(tab)
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo conectar a pestaña {tab_id}: {e}")
|
||||
|
||||
if not nuevas_tabs:
|
||||
print("⚠️ No se encontraron nuevas pestañas para agregar.\n")
|
||||
|
||||
return nuevas_tabs
|
||||
@@ -0,0 +1,206 @@
|
||||
import asyncio
|
||||
import json
|
||||
import base64
|
||||
import websockets
|
||||
from typing import Optional, List
|
||||
from .ElementoWeb import ElementoWeb
|
||||
import os
|
||||
|
||||
|
||||
class Tab:
|
||||
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
|
||||
self.websocket = websocket
|
||||
self.ws_url = ws_url
|
||||
self._message_id = 0
|
||||
self._pending = {}
|
||||
self._load_event = asyncio.Event()
|
||||
self.verbose = verbose
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.cerrar()
|
||||
|
||||
@classmethod
|
||||
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
||||
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
|
||||
tab = cls(websocket, ws_url)
|
||||
asyncio.create_task(tab._recibir_eventos())
|
||||
await tab._enviar("Page.enable")
|
||||
await tab._enviar("Network.enable")
|
||||
return tab
|
||||
|
||||
async def _recibir_eventos(self):
|
||||
async for mensaje in self.websocket:
|
||||
data = json.loads(mensaje)
|
||||
if "id" in data and data["id"] in self._pending:
|
||||
future = self._pending.pop(data["id"])
|
||||
if "result" in data:
|
||||
future.set_result(data["result"])
|
||||
elif "error" in data:
|
||||
future.set_exception(Exception(data["error"]))
|
||||
elif data.get("method") == "Page.loadEventFired":
|
||||
self._load_event.set()
|
||||
|
||||
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
|
||||
self._message_id += 1
|
||||
msg_id = self._message_id
|
||||
mensaje = {
|
||||
"id": msg_id,
|
||||
"method": metodo,
|
||||
"params": parametros or {}
|
||||
}
|
||||
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
self._pending[msg_id] = future
|
||||
await self.websocket.send(json.dumps(mensaje))
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
|
||||
async def navegar(self, url: str, wait_time: float = 5.0):
|
||||
self._load_event.clear()
|
||||
if self.verbose:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await self._enviar("Page.navigate", {"url": url})
|
||||
try:
|
||||
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
||||
if self.verbose:
|
||||
print("✅ Página cargada correctamente.")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
||||
|
||||
async def evaluar_js(self, js_code: str) -> Optional[str]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": js_code,
|
||||
"returnByValue": True
|
||||
})
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar JS: {e}")
|
||||
return None
|
||||
|
||||
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
|
||||
if not os.path.exists(ruta_archivo):
|
||||
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
|
||||
return None
|
||||
|
||||
with open(ruta_archivo, "r", encoding="utf-8") as f:
|
||||
js_code = f.read()
|
||||
|
||||
if reemplazos:
|
||||
for key, value in reemplazos.items():
|
||||
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
# 🔧 Eliminamos el `return` externo
|
||||
js_code_final = f"(async () => {{\n{js_code}\n}})();"
|
||||
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": js_code_final,
|
||||
"returnByValue": True
|
||||
})
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
|
||||
return None
|
||||
|
||||
async def obtener_user_agent(self) -> Optional[str]:
|
||||
return await self.evaluar_js("navigator.userAgent")
|
||||
|
||||
async def capturar_screenshot(self, output_path: str = "screenshot.png"):
|
||||
try:
|
||||
result = await self._enviar("Page.captureScreenshot")
|
||||
data = result["data"]
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(base64.b64decode(data))
|
||||
if self.verbose:
|
||||
print(f"📸 Screenshot guardado como {output_path}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al capturar screenshot: {e}")
|
||||
|
||||
async def cerrar(self):
|
||||
try:
|
||||
if not self.websocket.closed:
|
||||
await self.websocket.close()
|
||||
if self.verbose:
|
||||
print("🛑 WebSocket cerrado.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al cerrar pestaña: {e}")
|
||||
|
||||
async def obtener_html_completo(self) -> Optional[str]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": "document.documentElement.outerHTML",
|
||||
"returnByValue": True
|
||||
})
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener HTML: {e}")
|
||||
return None
|
||||
|
||||
async def obtener_dominio(self) -> Optional[str]:
|
||||
try:
|
||||
dominio = await self.evaluar_js("window.location.hostname")
|
||||
if self.verbose and dominio:
|
||||
print(f"🌐 Dominio actual: {dominio}")
|
||||
return dominio
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener dominio: {e}")
|
||||
return None
|
||||
|
||||
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
doc = await self._enviar("DOM.getDocument")
|
||||
root_node_id = doc["root"]["nodeId"]
|
||||
|
||||
result = await self._enviar("DOM.querySelector", {
|
||||
"nodeId": root_node_id,
|
||||
"selector": selector
|
||||
})
|
||||
node_id = result.get("nodeId")
|
||||
|
||||
if not node_id:
|
||||
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
||||
return None
|
||||
|
||||
return ElementoWeb.from_node(self, node_id=node_id)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
||||
return None
|
||||
|
||||
async def get_elements_by_css_selector(self, selector: str) -> List["ElementoWeb"]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": f'Array.from(document.querySelectorAll("{selector}"))',
|
||||
"objectGroup": "grupo_elementos_css",
|
||||
"includeCommandLineAPI": True,
|
||||
"returnByValue": False
|
||||
})
|
||||
array_id = result["result"]["objectId"]
|
||||
props = await self._enviar("Runtime.getProperties", {
|
||||
"objectId": array_id,
|
||||
"ownProperties": True
|
||||
})
|
||||
elementos = []
|
||||
for prop in props["result"]:
|
||||
if "value" in prop and "objectId" in prop["value"]:
|
||||
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
||||
if self.verbose:
|
||||
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
||||
return elementos
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
|
||||
return []
|
||||
|
||||
async def enfocar(self):
|
||||
try:
|
||||
await self._enviar("Page.bringToFront")
|
||||
if self.verbose:
|
||||
print("🪟 Pestaña enfocada (bringToFront).")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al enfocar pestaña: {e}")
|
||||
@@ -1,11 +1,11 @@
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
|
||||
from typing import List, Optional
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from sqlalchemy import MetaData # Asegúrate de importar esto
|
||||
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
|
||||
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
|
||||
from sqlalchemy import inspect
|
||||
from src.base import Base
|
||||
from domains.base import Base
|
||||
|
||||
|
||||
class Biblioteca:
|
||||
@@ -3,16 +3,16 @@ import base64
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, String, Integer
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from src.base import Base
|
||||
from src.Security.Encriptar import Encriptar_fernet
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC
|
||||
from src.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Security.Encriptar import Encriptar_fernet
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Embedders.Base_Embedder import EmbedderABC
|
||||
from domains.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
|
||||
|
||||
# ----------------------
|
||||
# Cargar clave maestra
|
||||
@@ -1,4 +1,4 @@
|
||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from typing import List
|
||||
|
||||
class Nota:
|
||||
@@ -3,18 +3,42 @@ from dotenv import load_dotenv
|
||||
from sqlalchemy import Table, Column, String, Text, MetaData
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy.orm import registry, Session
|
||||
from src.TextManager.nota import Nota
|
||||
from src.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.TextManager.nota import Nota
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from typing import Tuple
|
||||
import re
|
||||
|
||||
|
||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_base
|
||||
from src.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
|
||||
|
||||
|
||||
from src.base import Base # Este es tu declarative_base()
|
||||
titulo = os.getenv('DB_TITLE')
|
||||
usuario = os.getenv('DB_USER')
|
||||
passwrd = os.getenv('DB_PASSWORD')
|
||||
host = os.getenv('DB_HOST')
|
||||
port = os.getenv('DB_PORT')
|
||||
db_name = os.getenv('DB_NAME')
|
||||
|
||||
|
||||
db_credencial = PostgresCredencial(
|
||||
titulo=titulo,
|
||||
user=usuario,
|
||||
password=passwrd,
|
||||
host=host,
|
||||
port=port,
|
||||
dbname=db_name
|
||||
)
|
||||
|
||||
# from entrypoint.init_db import db_credencial
|
||||
from domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
|
||||
|
||||
|
||||
from domains.base import Base # Este es tu declarative_base()
|
||||
|
||||
# ----------------------
|
||||
# Cargar .env
|
||||
@@ -37,11 +61,11 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
|
||||
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
|
||||
"""
|
||||
try:
|
||||
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
|
||||
logger.info(f"Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
|
||||
|
||||
# Nombre SQL-safe
|
||||
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
|
||||
print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'")
|
||||
logger.debug(f"Nombre de tabla SQL-safe: '{nombre_tabla}'")
|
||||
|
||||
# Modelo ORM dinámico
|
||||
class NotaModel(Base, Model_base):
|
||||
@@ -57,15 +81,15 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
|
||||
vector = Column(Vector(vector_dim), nullable=True)
|
||||
vector_resumen = Column(Vector(vector_dim), nullable=True)
|
||||
|
||||
print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
|
||||
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
|
||||
print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
|
||||
print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
|
||||
logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
|
||||
logger.debug(f"Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
|
||||
logger.debug(f"Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
|
||||
logger.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
|
||||
|
||||
return NotaModel.__table__, NotaModel
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
|
||||
logger.error(f"Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
|
||||
raise
|
||||
|
||||
# ----------------------
|
||||
@@ -0,0 +1,15 @@
|
||||
class Usuario:
|
||||
def __init__(self, id: int, nombre: str, email: str, activo: bool = True):
|
||||
self.id = id
|
||||
self.nombre = nombre
|
||||
self.email = email
|
||||
self.activo = activo
|
||||
|
||||
def activar(self):
|
||||
self.activo = True
|
||||
|
||||
def desactivar(self):
|
||||
self.activo = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"Usuario(id={self.id}, nombre='{self.nombre}', email='{self.email}', activo={self.activo})"
|
||||
@@ -0,0 +1,82 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
from domains.Usuario.usuario import Usuario
|
||||
|
||||
# ----------------------
|
||||
# MODELO (SQLAlchemy)
|
||||
# ----------------------
|
||||
|
||||
class UsuarioModel(Model_base):
|
||||
__tablename__ = 'usuarios'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nombre = Column(String, nullable=False)
|
||||
email = Column(String, unique=True, nullable=False)
|
||||
activo = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# ----------------------
|
||||
# MAPPER
|
||||
# ----------------------
|
||||
|
||||
class UsuarioMapper(Mapper_base[Usuario, UsuarioModel]):
|
||||
@staticmethod
|
||||
def to_model(obj: Usuario) -> UsuarioModel:
|
||||
return UsuarioModel(
|
||||
id=obj.id,
|
||||
nombre=obj.nombre,
|
||||
email=obj.email,
|
||||
activo=obj.activo
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_model(model: UsuarioModel) -> Usuario:
|
||||
return Usuario(
|
||||
id=model.id,
|
||||
nombre=model.nombre,
|
||||
email=model.email,
|
||||
activo=model.activo
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_dict(obj: Usuario) -> dict:
|
||||
return {
|
||||
'id': obj.id,
|
||||
'nombre': obj.nombre,
|
||||
'email': obj.email,
|
||||
'activo': obj.activo
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> Usuario:
|
||||
return Usuario(
|
||||
id=data['id'],
|
||||
nombre=data['nombre'],
|
||||
email=data['email'],
|
||||
activo=data.get('activo', True)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_model_list(models: list[UsuarioModel]) -> list[Usuario]:
|
||||
return [UsuarioMapper.from_model(m) for m in models]
|
||||
|
||||
# ----------------------
|
||||
# REPO
|
||||
# ----------------------
|
||||
|
||||
class UsuarioRepo(Repo_base[UsuarioModel, Usuario]):
|
||||
def __init__(self, session):
|
||||
super().__init__(
|
||||
session=session,
|
||||
modelo=UsuarioModel,
|
||||
mapper=UsuarioMapper
|
||||
)
|
||||
|
||||
def get_by_email(self, email: str) -> Usuario | None:
|
||||
model = (
|
||||
self.session.query(self.Modelo)
|
||||
.filter_by(email=email, sys_deleted_at=None)
|
||||
.first()
|
||||
)
|
||||
return self.Mapper.from_model(model) if model else None
|
||||
@@ -1,14 +1,14 @@
|
||||
# entrypoint/init_db.py
|
||||
|
||||
from src.base import Base
|
||||
from src.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
|
||||
from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
|
||||
from domains.base import Base
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
|
||||
|
||||
from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
|
||||
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
|
||||
from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
|
||||
from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
|
||||
from src.TextManager.biblioteca_mmr import BibliotecaModel
|
||||
from domains.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
|
||||
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
|
||||
from domains.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
|
||||
from domains.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
|
||||
from domains.TextManager.biblioteca_mmr import BibliotecaModel
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
Generated
+2984
-36
File diff suppressed because it is too large
Load Diff
+14
-4
@@ -20,16 +20,25 @@
|
||||
"storybook:build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "8.0.0",
|
||||
"@mantine/hooks": "8.0.0",
|
||||
"@cycjimmy/jsmpeg-player": "^6.1.2",
|
||||
"@mantine/core": "^8.0.1",
|
||||
"@mantine/hooks": "^8.0.1",
|
||||
"@mantine/tiptap": "^8.0.1",
|
||||
"@react-three/fiber": "^9.1.2",
|
||||
"@tabler/icons": "^3.31.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tiptap/react": "^2.12.0",
|
||||
"@tiptap/starter-kit": "^2.12.0",
|
||||
"@uiw/react-markdown-preview": "^5.1.4",
|
||||
"@uiw/react-md-editor": "^4.0.7",
|
||||
"axios": "^1.9.0",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"marked": "^15.0.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router-dom": "^7.4.0"
|
||||
"react-router-dom": "^7.4.0",
|
||||
"turndown": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
@@ -44,6 +53,7 @@
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/three": "^0.176.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-mantine": "^4.0.3",
|
||||
|
||||
+67
-11
@@ -1,29 +1,85 @@
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { HomePage } from './pages/Home.page';
|
||||
import { Consulta_API } from './pages/Consulta_api';
|
||||
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
|
||||
import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
|
||||
import { Biblioteca } from './pages/Biblioteca';
|
||||
import { HomePage } from './frontend_domains/Home/Home.page';
|
||||
import { Consulta_API } from './frontend_domains/Experiments/Consulta_api';
|
||||
import { Error_404 } from './frontend_domains/FitzStudio/404/404'; // Ajusta si está en otra carpeta
|
||||
import { Grid_Dashboard } from './frontend_domains/Experiments/Grid_dashboard'; // Ajusta si está en otra carpeta
|
||||
import { Biblioteca } from './frontend_domains/TextEditor/Biblioteca';
|
||||
import { VisualizacionesRandom } from './frontend_domains/Experiments/Visualizaciones_Random';
|
||||
import { Camara_noir } from './frontend_domains/CamaraNoir/Camaras_noir';
|
||||
import EditorTest from "./frontend_domains/TextEditor/Editor_Test";
|
||||
import { ChatPage } from './frontend_domains/Llms/Chat/ChatPage';
|
||||
import { LoginPage } from './frontend_domains/Usuarios/Login.page';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
|
||||
// Home Principal
|
||||
|
||||
{
|
||||
path: '/',
|
||||
element: <HomePage />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
// Biblioteca
|
||||
|
||||
{
|
||||
path: '/Consulta_API',
|
||||
element: <Consulta_API />,
|
||||
path: '/bibliot/Biblioteca',
|
||||
element: <Biblioteca />,
|
||||
},
|
||||
{
|
||||
path: '/Grid_Dashboard',
|
||||
path: '/bibliot/editortest',
|
||||
element: <EditorTest />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Chat LLM
|
||||
|
||||
{
|
||||
path: '/llms/chat',
|
||||
element: <ChatPage />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// CamaraNoir
|
||||
|
||||
{
|
||||
path: '/camara/principal',
|
||||
element: <Camara_noir />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Experimentos
|
||||
|
||||
{
|
||||
path: '/experiments/Consulta_API',
|
||||
element: <Consulta_API />,
|
||||
},
|
||||
|
||||
{
|
||||
path: '/experiments/Grid_Dashboard',
|
||||
element: <Grid_Dashboard />,
|
||||
},
|
||||
{
|
||||
path: '/Biblioteca',
|
||||
element: <Biblioteca />,
|
||||
path: '/experiments/Visualizaciones_Random',
|
||||
element: <VisualizacionesRandom />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Login
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginPage />,
|
||||
},
|
||||
|
||||
|
||||
// FitzStudio Pages -------------------------------------------------------
|
||||
// Error 404
|
||||
|
||||
{
|
||||
path: '*',
|
||||
element: <Error_404 />,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// https://tabler.io/icons
|
||||
|
||||
// OUTLINED
|
||||
export { default as IconArrowLeft } from './outlined/arrow-left.svg?react';
|
||||
export { default as IconHomeOutline } from './outlined/home.svg?react';
|
||||
@@ -11,7 +13,10 @@ export { default as IconSettings } from './outlined/settings.svg?react';
|
||||
export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react';
|
||||
export { default as IconArrowBarRight } from './outlined/arrow-bar-right.svg?react';
|
||||
export { default as IconCheck } from './outlined/check.svg?react';
|
||||
|
||||
export { default as CameraPlus } from './outlined/camera-plus.svg?react';
|
||||
export { default as Flask } from './outlined/flask.svg?react';
|
||||
export { default as Users } from './outlined/users.svg?react';
|
||||
export { default as IconNotebook } from './outlined/notebook.svg?react';
|
||||
|
||||
// FILLED
|
||||
export { default as IconHomeFilled } from './filled/home.svg?react';
|
||||
|
||||
@@ -4,17 +4,20 @@ import {
|
||||
IconDeviceDesktopAnalytics,
|
||||
IconFingerprint,
|
||||
IconGauge,
|
||||
IconNotebook,
|
||||
IconHome2,
|
||||
IconSettings,
|
||||
IconUserOutline as IconUser,
|
||||
CameraPlus,
|
||||
Flask,
|
||||
Users
|
||||
} from '../assets/icons'; // ajusta según tu estructura de proyecto
|
||||
|
||||
export const mainLinksdata = [
|
||||
{ icon: IconHome2, label: 'Home' },
|
||||
{ icon: IconGauge, label: 'Dashboard' },
|
||||
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' },
|
||||
{ icon: IconCalendarStats, label: 'Releases' },
|
||||
{ icon: IconUser, label: 'Account' },
|
||||
{ icon: IconFingerprint, label: 'Security' },
|
||||
{ icon: IconNotebook, label: 'Biblioteca' },
|
||||
{ icon: Users, label: 'AgentesLLMs' },
|
||||
{ icon: CameraPlus, label: 'CameraNoir' },
|
||||
{ icon: Flask, label: 'Experimentos' },
|
||||
{ icon: IconSettings, label: 'Settings' },
|
||||
];
|
||||
@@ -1,35 +1,49 @@
|
||||
// src/data/submenuLinks.ts
|
||||
|
||||
import { Biblioteca } from "@/frontend_domains/TextEditor/Biblioteca";
|
||||
|
||||
export const submenuLinks = {
|
||||
|
||||
// Home Principal
|
||||
|
||||
Home: [
|
||||
{ label: 'Inicio', to: '/' },
|
||||
{ label: 'Consulta Api', to: '/Consulta_API' },
|
||||
{ label: 'Biblioteca', to: '/Biblioteca' },
|
||||
|
||||
],
|
||||
Dashboard: [
|
||||
{ label: 'Resumen', to: '/dashboard/resumen' },
|
||||
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
|
||||
{ label: 'Estadísticas', to: '/dashboard/estadisticas' },
|
||||
{ label: 'Usuarios', to: '/dashboard/usuarios' },
|
||||
|
||||
// Biblioteca
|
||||
|
||||
Biblioteca: [
|
||||
{ label: 'Biblioteca', to: '/bibliot/Biblioteca' },
|
||||
{ label: 'test', to: '/bibliot/editortest' },
|
||||
|
||||
],
|
||||
Analytics: [
|
||||
{ label: 'Conversiones', to: '/analytics/conversiones' },
|
||||
{ label: 'Tráfico', to: '/analytics/trafico' },
|
||||
{ label: 'Tendencias', to: '/analytics/tendencias' },
|
||||
|
||||
|
||||
|
||||
// Experimentos
|
||||
Experimentos: [
|
||||
{ label: 'Consulta Api', to: '/experiments/Consulta_API' },
|
||||
{ label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
|
||||
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
|
||||
],
|
||||
Releases: [
|
||||
{ label: 'Notas de versión', to: '/releases/notas-de-version' },
|
||||
{ label: 'Historial', to: '/releases/historial' },
|
||||
|
||||
// Camara
|
||||
CameraNoir: [
|
||||
{ label: 'Camara_principal', to: '/camara/principal' },
|
||||
],
|
||||
Account: [
|
||||
{ label: 'Perfil', to: '/account/perfil' },
|
||||
{ label: 'Suscripciones', to: '/account/suscripciones' },
|
||||
],
|
||||
Security: [
|
||||
{ label: 'Contraseña', to: '/security/contraseña' },
|
||||
{ label: '2FA', to: '/security/2fa' },
|
||||
|
||||
// LLms
|
||||
|
||||
AgentesLLMs: [
|
||||
{ label: 'LLMs', to: '/llms' },
|
||||
{ label: 'Chat', to: '/llms/chat' },
|
||||
{ label: 'Documentos', to: '/llms/documentos' },
|
||||
|
||||
|
||||
],
|
||||
|
||||
// Settings
|
||||
Settings: [
|
||||
{ label: 'Preferencias', to: '/settings/preferencias' },
|
||||
{ label: 'Notificaciones', to: '/settings/notificaciones' },
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { Card, Text, Container } from '@mantine/core';
|
||||
|
||||
export function Camara_noir() {
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<Container
|
||||
size="lg"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Card shadow="sm" padding="xl" radius="md" withBorder>
|
||||
<Text size="lg" mb="md">
|
||||
Cámara Noir en Vivo
|
||||
</Text>
|
||||
<img
|
||||
src="http://10.8.0.9:8000/video"
|
||||
alt="Stream MJPEG en vivo desde Raspberry Pi"
|
||||
style={{
|
||||
width: '640px',
|
||||
height: '480px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<Text size="sm" color="dimmed" mt="sm">
|
||||
Transmisión MJPEG en vivo vía FastAPI / libcamera-vid
|
||||
</Text>
|
||||
</Card>
|
||||
</Container>
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
|
||||
import { LlamadorAPI } from '../components/LlamadorAPI';
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
import { LlamadorAPI } from './LlamadorAPI';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
|
||||
|
||||
export function Consulta_API() {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Grid } from '@mantine/core';
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
import { GridDashboard } from '../components/Grid_dashboard';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { GridDashboard } from './Grid_dashboard_component';
|
||||
|
||||
export function Grid_Dashboard() {
|
||||
return (
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Select, Group } from '@mantine/core';
|
||||
import { IconCheck } from '../assets/icons';
|
||||
import { IconCheck } from '../../assets/icons';
|
||||
|
||||
interface MetodoSelectProps {
|
||||
metodo: string;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { Card, Grid, Title, Loader } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
type ChartOption = any;
|
||||
|
||||
function useChartOption(endpoint: string) {
|
||||
const [option, setOption] = useState<ChartOption | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/v1/charts/${endpoint}`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => setOption(json))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, [endpoint]);
|
||||
|
||||
return { option, loading };
|
||||
}
|
||||
|
||||
export function VisualizacionesRandom() {
|
||||
const charts = [
|
||||
{ title: 'Gráfico de barras', endpoint: 'bar' },
|
||||
{ title: 'Gráfico de líneas', endpoint: 'line' },
|
||||
{ title: 'Gráfico de pastel', endpoint: 'pie' },
|
||||
{ title: 'Scatter plot', endpoint: 'scatter' },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<Grid>
|
||||
{charts.map(({ title, endpoint }, idx) => {
|
||||
const { option, loading } = useChartOption(endpoint);
|
||||
|
||||
return (
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 6, lg: 3 }} key={idx}>
|
||||
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
||||
<Title order={4}>{title}</Title>
|
||||
{loading || !option ? (
|
||||
<Loader mt="md" />
|
||||
) : (
|
||||
<ReactECharts option={option} style={{ height: 250, marginTop: 16 }} />
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box, Title, Text, Button, Group, Stack, Image, Center } from '@mantine/core';
|
||||
import { useMantineTheme } from '@mantine/core';
|
||||
import { IconArrowLeft } from '../assets/icons';
|
||||
import { IconArrowLeft } from '../../../assets/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MantineCardWithShader } from '../components/HoloShader'; // Ajusta ruta si es necesario
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
import { MantineCardWithShader } from './HoloShader_404'; // Ajusta ruta si es necesario
|
||||
import { AppShellWithMenu } from '../Appshell/Appshell';
|
||||
|
||||
|
||||
|
||||
export function Error_404() {
|
||||
+3
-3
@@ -11,9 +11,9 @@ import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { default as LogoIcon } from '../../assets/icons/favicon';
|
||||
import { mainLinksdata } from '../../data/navigationsLinks_1';
|
||||
import { submenuLinks } from '../../data/submenuLinks_1';
|
||||
import { default as LogoIcon } from '../../../assets/icons/favicon';
|
||||
import { mainLinksdata } from '../../../data/navigationsLinks_1';
|
||||
import { submenuLinks } from '../../../data/submenuLinks_1';
|
||||
|
||||
import classes from './Appshell.module.css';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
import { AppShellWithMenu } from './Appshell/Appshell';
|
||||
|
||||
|
||||
export function Plantilla() {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
import { Welcome } from '@/components/Welcome/Welcome';
|
||||
import { ColorSchemeToggle } from '@/components/ColorSchemeToggle/ColorSchemeToggle';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { Welcome } from '@/frontend_domains/FitzStudio/Welcome/Welcome';
|
||||
import { ColorSchemeToggle } from '@/frontend_domains/FitzStudio/ColorSchemeToggle/ColorSchemeToggle';
|
||||
|
||||
|
||||
export function HomePage() {
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useState } from "react";
|
||||
import { Textarea, Button, Group } from "@mantine/core";
|
||||
|
||||
export function ChatInput({ onSend }: { onSend: (text: string) => void }) {
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const handleSend = () => {
|
||||
if (!text.trim()) return;
|
||||
onSend(text.trim());
|
||||
setText("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.currentTarget.value)}
|
||||
autosize
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
placeholder="Escribe tu mensaje..."
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button onClick={handleSend}>Enviar</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { Container, Stack, Paper, ScrollArea, Title } from "@mantine/core";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { MessageList } from "./MessageList";
|
||||
import { AppShellWithMenu } from "../../FitzStudio/Appshell/Appshell";
|
||||
|
||||
export function ChatPage() {
|
||||
const [messages, setMessages] = useState([
|
||||
{ sender: "bot", content: "Hola, ¿en qué puedo ayudarte hoy?" },
|
||||
]);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const handleSend = async (content: string) => {
|
||||
const newMessages = [...messages, { sender: "user", content }];
|
||||
setMessages(newMessages);
|
||||
|
||||
let currentResponse = "";
|
||||
setMessages((prev) => [...prev, { sender: "bot", content: "" }]);
|
||||
|
||||
wsRef.current = new WebSocket("ws://localhost:8000/ws/chat");
|
||||
|
||||
wsRef.current.onopen = () => {
|
||||
wsRef.current?.send(JSON.stringify({ prompt: content }));
|
||||
};
|
||||
|
||||
wsRef.current.onmessage = (event) => {
|
||||
const token = event.data;
|
||||
currentResponse += token;
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[updated.length - 1] = { sender: "bot", content: currentResponse };
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
wsRef.current.onerror = (err) => {
|
||||
console.error("WebSocket error:", err);
|
||||
setMessages((prev) => [
|
||||
...prev.slice(0, -1),
|
||||
{ sender: "bot", content: "⚠️ Error al comunicarse con el servidor." },
|
||||
]);
|
||||
};
|
||||
|
||||
wsRef.current.onclose = () => {
|
||||
wsRef.current = null;
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<Container size="sm" p="md">
|
||||
<Stack>
|
||||
<Title order={2}>Chat LLM</Title>
|
||||
<Paper shadow="xs" p="md" withBorder>
|
||||
<ScrollArea h={400}>
|
||||
<MessageList messages={messages} />
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
<ChatInput onSend={handleSend} />
|
||||
</Stack>
|
||||
</Container>
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Paper, Text, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||
|
||||
export function MessageBubble({ sender, content }: { sender: string; content: string }) {
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isUser = sender === "user";
|
||||
|
||||
const userBg = theme.colors[theme.primaryColor][0];
|
||||
const botBg = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0];
|
||||
const userTextColor = theme.colors[theme.primaryColor][9];
|
||||
|
||||
return (
|
||||
<Paper
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{
|
||||
alignSelf: isUser ? "flex-end" : "flex-start",
|
||||
backgroundColor: isUser ? userBg : botBg,
|
||||
maxWidth: "80%",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c={isUser ? userTextColor : undefined}>
|
||||
{content}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Stack } from "@mantine/core";
|
||||
import { MessageBubble } from "./MessageBubble";
|
||||
|
||||
export function MessageList({ messages }: { messages: { sender: string; content: string }[] }) {
|
||||
return (
|
||||
<Stack>
|
||||
{messages.map((msg, i) => (
|
||||
<MessageBubble key={i} sender={msg.sender} content={msg.content} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
ScrollArea,
|
||||
Group,
|
||||
Button,
|
||||
TextInput,
|
||||
Modal,
|
||||
Box
|
||||
} from '@mantine/core';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import axios from 'axios';
|
||||
|
||||
import { RichTextEditor } from '@mantine/tiptap';
|
||||
import { useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import '@mantine/tiptap/styles.css';
|
||||
|
||||
import TurndownService from 'turndown';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import './Editor_biblioteca.css';
|
||||
|
||||
type Nota = {
|
||||
id: string;
|
||||
titulo: string;
|
||||
texto: string; // Markdown
|
||||
};
|
||||
|
||||
type Biblioteca = {
|
||||
id: string;
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
notas: Nota[];
|
||||
};
|
||||
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
bulletListMarker: '-',
|
||||
codeBlockStyle: 'fenced',
|
||||
emDelimiter: '*',
|
||||
strongDelimiter: '**',
|
||||
});
|
||||
|
||||
export function Biblioteca() {
|
||||
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
|
||||
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
|
||||
const [notaSeleccionada, setNotaSeleccionada] = useState<Nota | null>(null);
|
||||
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
|
||||
const [nombreBiblio, setNombreBiblio] = useState('');
|
||||
const [descripcionBiblio, setDescripcionBiblio] = useState('');
|
||||
const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false);
|
||||
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: '',
|
||||
onUpdate: ({ editor }) => {
|
||||
const html = editor.getHTML();
|
||||
const markdown = turndownService.turndown(html);
|
||||
setNotaSeleccionada((prev) =>
|
||||
prev ? { ...prev, texto: markdown } : null
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🟡 editor:', editor);
|
||||
console.log('🟠 isDestroyed:', editor?.isDestroyed);
|
||||
console.log('🟢 isEditable:', editor?.isEditable);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBibliotecas();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !notaSeleccionada || editor.isDestroyed) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const markdown = notaSeleccionada.texto;
|
||||
const html = await marked.parse(markdown);
|
||||
editor.commands.setContent(html);
|
||||
setTimeout(() => editor.commands.focus(), 100);
|
||||
} catch (err) {
|
||||
console.error('❌ Error al hacer setContent:', err);
|
||||
}
|
||||
})();
|
||||
}, [notaSeleccionada?.id, editor]);
|
||||
|
||||
const fetchBibliotecas = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/v1/text_manager/list');
|
||||
if (!Array.isArray(res.data)) return;
|
||||
|
||||
const bibliotecasConNotas = await Promise.all(
|
||||
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
|
||||
const notas = await axios.get(`/api/v1/text_manager/nota/list/${biblio.id}`);
|
||||
return { ...biblio, notas: notas.data as Nota[] };
|
||||
})
|
||||
);
|
||||
setBibliotecas(bibliotecasConNotas);
|
||||
setBibliotecaSeleccionada(bibliotecasConNotas[0] || null);
|
||||
} catch (error) {
|
||||
console.error('Error al cargar bibliotecas:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const crearBiblioteca = async () => {
|
||||
setLoadingNuevaBiblio(true);
|
||||
try {
|
||||
await axios.post('/api/v1/text_manager/biblioteca', {
|
||||
nombre_biblioteca: nombreBiblio,
|
||||
descripcion: descripcionBiblio,
|
||||
});
|
||||
setNombreBiblio('');
|
||||
setDescripcionBiblio('');
|
||||
setModalNuevaBiblio(false);
|
||||
await fetchBibliotecas();
|
||||
} catch (error) {
|
||||
console.error('❌ Error al crear biblioteca:', error);
|
||||
} finally {
|
||||
setLoadingNuevaBiblio(false);
|
||||
}
|
||||
};
|
||||
|
||||
const guardarEdicionNota = async () => {
|
||||
if (!notaSeleccionada || !bibliotecaSeleccionada) return;
|
||||
try {
|
||||
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaSeleccionada.id}`, {
|
||||
titulo: notaSeleccionada.titulo,
|
||||
texto: notaSeleccionada.texto,
|
||||
tags: [],
|
||||
conexiones: [],
|
||||
resumen: ""
|
||||
});
|
||||
await fetchBibliotecas();
|
||||
} catch (error) {
|
||||
console.error("Error al actualizar nota:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const eliminarNota = async (notaId: string) => {
|
||||
if (!bibliotecaSeleccionada) return;
|
||||
try {
|
||||
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
|
||||
await fetchBibliotecas();
|
||||
setNotaSeleccionada(null);
|
||||
} catch (error) {
|
||||
console.error("Error al eliminar nota:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<Box display="flex" h="100%" style={{ overflow: 'hidden' }}>
|
||||
<Box w={240} p="md">
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="md">
|
||||
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
|
||||
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}>➕ Nueva biblioteca</Button>
|
||||
{bibliotecas.map((biblio) => (
|
||||
<Button
|
||||
key={biblio.id}
|
||||
size="xs"
|
||||
fullWidth
|
||||
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
setBibliotecaSeleccionada(biblio);
|
||||
setNotaSeleccionada(null);
|
||||
}}
|
||||
>
|
||||
{biblio.nombre}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box w={240} p="md">
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="md">
|
||||
<Title order={4}>Notas</Title>
|
||||
<Button
|
||||
color="green"
|
||||
variant="outline"
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
if (!bibliotecaSeleccionada) return;
|
||||
try {
|
||||
const res = await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
|
||||
titulo: 'Nueva nota',
|
||||
texto: '',
|
||||
tags: [],
|
||||
conexiones: [],
|
||||
resumen: ''
|
||||
});
|
||||
const nuevaNota: Nota = res.data;
|
||||
await fetchBibliotecas();
|
||||
setNotaSeleccionada(nuevaNota);
|
||||
} catch (error) {
|
||||
console.error('Error al crear nota:', error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
➕ Nueva nota
|
||||
</Button>
|
||||
{bibliotecaSeleccionada?.notas.map((nota) => (
|
||||
<Button
|
||||
key={nota.id}
|
||||
fullWidth
|
||||
variant={notaSeleccionada?.id === nota.id ? 'filled' : 'light'}
|
||||
color="gray"
|
||||
onClick={() => setNotaSeleccionada(nota)}
|
||||
>
|
||||
{nota.titulo}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box p="md" style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||
{notaSeleccionada ? (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Título"
|
||||
size="lg"
|
||||
styles={{ input: { fontSize: 20, fontWeight: 600 } }}
|
||||
value={notaSeleccionada.titulo}
|
||||
onChange={(e) =>
|
||||
setNotaSeleccionada((prev) => prev ? { ...prev, titulo: e.currentTarget.value } : null)
|
||||
}
|
||||
/>
|
||||
{editor && !editor.isDestroyed && (
|
||||
<RichTextEditor
|
||||
editor={editor}
|
||||
miw={0}
|
||||
style={{ fontSize: 14, minHeight: 200 }}
|
||||
classNames={{ content: 'tiptap' }}
|
||||
>
|
||||
<RichTextEditor.Toolbar sticky stickyOffset={0}>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Bold />
|
||||
<RichTextEditor.Italic />
|
||||
<RichTextEditor.Strikethrough />
|
||||
<RichTextEditor.ClearFormatting />
|
||||
<RichTextEditor.H1 />
|
||||
<RichTextEditor.H2 />
|
||||
<RichTextEditor.Blockquote />
|
||||
<RichTextEditor.CodeBlock />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
</RichTextEditor.Toolbar>
|
||||
{/* tabIndex removido */}
|
||||
<RichTextEditor.Content className="tiptap" />
|
||||
</RichTextEditor>
|
||||
)}
|
||||
<Group mt="sm">
|
||||
<Button color="blue" onClick={guardarEdicionNota}>💾 Guardar</Button>
|
||||
<Button color="red" onClick={() => eliminarNota(notaSeleccionada.id)}>🗑️ Eliminar</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text>Selecciona una nota para editar</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Modal opened={modalNuevaBiblio} onClose={() => setModalNuevaBiblio(false)} title="Crear nueva biblioteca">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nombre"
|
||||
value={nombreBiblio}
|
||||
onChange={(e) => setNombreBiblio(e.currentTarget.value)}
|
||||
disabled={loadingNuevaBiblio}
|
||||
/>
|
||||
<TextInput
|
||||
label="Descripción"
|
||||
value={descripcionBiblio}
|
||||
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
|
||||
disabled={loadingNuevaBiblio}
|
||||
/>
|
||||
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>Crear</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { RichTextEditor } from '@mantine/tiptap';
|
||||
import { useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import '@mantine/tiptap/styles.css';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
|
||||
export default function EditorTest() {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: '<p>Prueba aquí. Presiona ENTER o ESPACIO.</p>',
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
<div style={{ padding: 40 }}>
|
||||
{editor && ( <AppShellWithMenu>
|
||||
<RichTextEditor editor={editor}>
|
||||
<RichTextEditor.Toolbar sticky stickyOffset={0}>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Bold />
|
||||
<RichTextEditor.Italic />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
</RichTextEditor.Toolbar>
|
||||
<RichTextEditor.Content />
|
||||
</RichTextEditor>
|
||||
</AppShellWithMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Editor_biblioteca.css */
|
||||
/* En Editor_biblioteca.css */
|
||||
.tiptap {
|
||||
min-height: 200px;
|
||||
padding: 8px;
|
||||
/* white-space: pre-wrap; */
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tiptap p {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.tiptap h1, .tiptap h2, .tiptap h3 {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
.tiptap blockquote {
|
||||
margin: 0.6em 0;
|
||||
/* padding-left: 1em; */
|
||||
border-left: 3px solid #888;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.mantine-RichTextEditor-toolbar {
|
||||
background-color: #1e1e1e; /* o el color de tu layout */
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.mantine-RichTextEditor-controlIcon {
|
||||
color: white !important;
|
||||
stroke: white !important;
|
||||
}
|
||||
|
||||
.mantine-RichTextEditor-control {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user