Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daebea9e9c | |||
| 58238a5918 | |||
| fa33321ff1 | |||
| a2873e4c85 | |||
| 0d97aa2d61 | |||
| 23f25034ad | |||
| 8ad80defcf | |||
| aef8791151 | |||
| 3d5deef0fb | |||
| 3438102dc0 | |||
| 9ee8daa295 | |||
| 6d6fab5634 | |||
| 9c638fc3e5 | |||
| 43f6fb03fe | |||
| ac83907e7c | |||
| 3cd267ee6e | |||
| e1b756ac99 | |||
| 628cddc3ae |
@@ -17,6 +17,7 @@ pruebas_conceptos/postgres_extensions/pgdata/*
|
|||||||
*.env
|
*.env
|
||||||
config/.env
|
config/.env
|
||||||
|
|
||||||
|
.continue
|
||||||
|
|
||||||
#Icon files
|
#Icon files
|
||||||
frontend/src/assets/icons/filled/** */
|
frontend/src/assets/icons/filled/** */
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from backend.api.v1.router import router
|
from backend.backend_domains.router_v1 import router
|
||||||
|
from backend.backend_domains.llms import llm_chat_ws_endpoint_v1
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Fitz Backend",
|
title="Fitz Backend",
|
||||||
@@ -21,4 +23,5 @@ app.add_middleware(
|
|||||||
|
|
||||||
|
|
||||||
# Incluye las rutas de tu API
|
# 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"])
|
||||||
+7
-3
@@ -1,12 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi import Path
|
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 fastapi.concurrency import run_in_threadpool
|
||||||
from backend.db.conexion import get_conexion
|
from backend.db.conexion import get_conexion
|
||||||
from backend.services.text_manager_srvc import *
|
from backend.backend_domains.text_manager.text_manager_srvc import *
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
+29
-32
@@ -1,53 +1,55 @@
|
|||||||
from src.TextManager.biblioteca import Biblioteca
|
from domains.TextManager.biblioteca import Biblioteca
|
||||||
from src.TextManager.biblioteca_mmr import BibliotecaRepo
|
from domains.TextManager.biblioteca_mmr import BibliotecaRepo
|
||||||
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||||
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
|
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||||
from src.TextManager.nota import Nota
|
from domains.TextManager.nota import Nota
|
||||||
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
|
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
|
||||||
from sqlalchemy import MetaData
|
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):
|
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
|
||||||
print("[INICIO] Creando biblioteca...")
|
logger.info("[INICIO] Creando biblioteca...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("[Paso 1] Obteniendo credencial...")
|
logger.info("[Paso 1] Obteniendo credencial...")
|
||||||
cred_repo = OpenAICredencialRepo(conexion)
|
cred_repo = OpenAICredencialRepo(conexion)
|
||||||
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
|
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")
|
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(
|
biblioteca = Biblioteca(
|
||||||
nombre=nombre_biblioteca,
|
nombre=nombre_biblioteca,
|
||||||
embedder=embedder,
|
embedder=embedder,
|
||||||
descripcion=descripcion
|
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 = BibliotecaRepo(conexion)
|
||||||
repo.add(biblioteca=biblioteca)
|
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)
|
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 {
|
return {
|
||||||
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
|
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
|
||||||
"id": biblioteca.id
|
"id": biblioteca.id
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[ERROR] Ocurrió una excepción:", str(e))
|
logger.exception("[ERROR] Ocurrió una excepción:")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
|
|||||||
for b in bibliotecas
|
for b in bibliotecas
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def agregar_nota_a_biblioteca(
|
def agregar_nota_a_biblioteca(
|
||||||
conexion: PostgresConexion,
|
conexion: PostgresConexion,
|
||||||
biblioteca_id: str,
|
biblioteca_id: str,
|
||||||
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
|
|||||||
conexiones: list[str] = None,
|
conexiones: list[str] = None,
|
||||||
resumen: str = ""
|
resumen: str = ""
|
||||||
):
|
):
|
||||||
|
|
||||||
# Obtener la biblioteca
|
|
||||||
repo_biblioteca = BibliotecaRepo(conexion)
|
repo_biblioteca = BibliotecaRepo(conexion)
|
||||||
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
|
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
|
||||||
if biblioteca is None:
|
if biblioteca is None:
|
||||||
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
|
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
|
||||||
|
|
||||||
# Crear objeto Nota
|
|
||||||
nota = Nota(
|
nota = Nota(
|
||||||
titulo=titulo,
|
titulo=titulo,
|
||||||
texto=texto,
|
texto=texto,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
conexiones=conexiones or [],
|
conexiones=conexiones or [],
|
||||||
resumen=resumen or "",
|
resumen=resumen or "",
|
||||||
# vector=biblioteca.embedder.embed_text(texto),
|
|
||||||
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
|
|
||||||
)
|
)
|
||||||
# Mostrar atributos seguros
|
logger.debug(
|
||||||
print(
|
|
||||||
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
|
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
|
||||||
f"texto_len={len(nota.texto)}, "
|
f"texto_len={len(nota.texto)}, "
|
||||||
f"tags={len(nota.tags)}, "
|
f"tags={len(nota.tags)}, "
|
||||||
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
|
|||||||
f"resumen_len={len(nota.resumen)}"
|
f"resumen_len={len(nota.resumen)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Preparar tabla y modelo de nota
|
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
|
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
|
||||||
biblioteca.nombre,
|
biblioteca.nombre,
|
||||||
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
|
|||||||
)
|
)
|
||||||
metadata.create_all(conexion.get_engine())
|
metadata.create_all(conexion.get_engine())
|
||||||
|
|
||||||
# Guardar la nota
|
|
||||||
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
|
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
|
||||||
nota_id = repo_nota.add(nota)
|
nota_id = repo_nota.add(nota)
|
||||||
|
|
||||||
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
|
|||||||
"nota_id": nota_id
|
"nota_id": nota_id
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"[SUCCESS] {resultado['mensaje']}")
|
logger.success(f"[SUCCESS] {resultado['mensaje']}")
|
||||||
return resultado
|
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)
|
fue_eliminada = repo_nota.delete_by_id(nota_id)
|
||||||
|
|
||||||
if fue_eliminada:
|
if fue_eliminada:
|
||||||
|
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
|
||||||
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
|
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
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)
|
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
|
||||||
|
|
||||||
if fue_actualizada:
|
if fue_actualizada:
|
||||||
|
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
|
||||||
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
|
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
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
|
# backend/db/conexion.py
|
||||||
from entrypoint.init_db import db_credencial
|
from entrypoint.init_db import db_credencial
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||||
|
|
||||||
def get_conexion():
|
def get_conexion():
|
||||||
conexion = PostgresConexion(db_credencial)
|
conexion = PostgresConexion(db_credencial)
|
||||||
|
|||||||
+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:
|
class OpenAICredencial:
|
||||||
def __init__(self, titulo: str, api_key: str, organizacion: str = None, id: str = None):
|
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 dotenv import load_dotenv
|
||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||||
from src.Security.Encriptar import Encriptar_fernet
|
from domains.Security.Encriptar import Encriptar_fernet
|
||||||
from entrypoint import ENV_PATH
|
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 sqlalchemy import Column, String
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from domains.ArquitectureLayer.Repo import Repo_base
|
||||||
|
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from datetime import datetime
|
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")
|
TModelo = TypeVar("TModelo")
|
||||||
TDominio = TypeVar("TDominio")
|
TDominio = TypeVar("TDominio")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
from src.Credenciales.ollama_credencial import OllamaCredencial
|
from domains.Credenciales.ollama_credencial import OllamaCredencial
|
||||||
|
|
||||||
class OllamaCliente:
|
class OllamaCliente:
|
||||||
def __init__(self, credencial: OllamaCredencial):
|
def __init__(self, credencial: OllamaCredencial):
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||||
|
|
||||||
class OpenAICliente:
|
class OpenAICliente:
|
||||||
def __init__(self, credencial: OpenAICredencial):
|
def __init__(self, credencial: OpenAICredencial):
|
||||||
@@ -4,8 +4,8 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||||
|
|
||||||
class PostgresConexion(ConexionBase):
|
class PostgresConexion(ConexionBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
|
|
||||||
class OllamaCredencial:
|
class OllamaCredencial:
|
||||||
def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None):
|
def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None):
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
|
|
||||||
class PostgresCredencial:
|
class PostgresCredencial:
|
||||||
def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str, id: str = None):
|
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
|
import base64
|
||||||
|
|
||||||
|
|
||||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from domains.ArquitectureLayer.Repo import Repo_base
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||||
from src.Security.Encriptar import Encriptar_fernet
|
from domains.Security.Encriptar import Encriptar_fernet
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
# Cargar clave maestra
|
# Cargar clave maestra
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
from src.Llms.Modelos.Base_model import ModeloABC
|
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||||
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||||
from src.Llms.MCPs.McpClient_Registry import ClientRegistry
|
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List, Union, AsyncGenerator
|
from typing import Optional, List, Union, AsyncGenerator
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from entrypoint.init_db import db_credencial
|
from entrypoint.init_db import db_credencial
|
||||||
from src.Logger.logger_db import LoggerDB, logger
|
from domains.Logger.logger_db import LoggerDB, logger
|
||||||
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
|
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
|
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
|
||||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||||
from src.ConexionApis.OpenAi_conexion import OpenAICliente
|
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
|
|
||||||
class OpenAIEmbedder(EmbedderABC):
|
class OpenAIEmbedder(EmbedderABC):
|
||||||
def __init__(self, credencial: OpenAICredencial,
|
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
|
||||||
from sqlalchemy import Column, String, ForeignKey
|
from sqlalchemy import Column, String, ForeignKey
|
||||||
|
|
||||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from domains.ArquitectureLayer.Repo import Repo_base
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||||
from src.ApiKeys.openai_apikey import OpenAICredencial
|
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
# Cargar configuración desde .env si se requiere
|
# Cargar configuración desde .env si se requiere
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from src.Llms.MCPs.McpClient import MCPClient
|
from domains.Llms.MCPs.McpClient import MCPClient
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
class ClientRegistry:
|
class ClientRegistry:
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Script para generar un servidor MCP a partir de código fuente recibido (por ejemplo, desde un LLM).
|
||||||
|
Crea un archivo Python en la carpeta de servidores MCP con el código proporcionado y un nombre único.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
SERVERS_DIR = Path(__file__).parent
|
||||||
|
mcp = FastMCP()
|
||||||
|
|
||||||
|
def generate_server(code: str, name: str = None) -> str:
|
||||||
|
if not name:
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
name = f"server_llm_{timestamp}.py"
|
||||||
|
else:
|
||||||
|
if not name.endswith('.py'):
|
||||||
|
name += '.py'
|
||||||
|
server_path = SERVERS_DIR / name
|
||||||
|
with open(server_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(code)
|
||||||
|
return str(server_path)
|
||||||
|
|
||||||
|
@mcp.tool(description="Genera un archivo de servidor MCP a partir de código fuente y un nombre opcional.")
|
||||||
|
def mcp_generate_server(code: str, name: str = None) -> str:
|
||||||
|
"""
|
||||||
|
Esta herramienta guarda el código fuente en un archivo Python con nombre opcional.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Código fuente del servidor MCP como string.
|
||||||
|
name: (opcional) Nombre del archivo. Si no se especifica, se generará uno con timestamp.
|
||||||
|
|
||||||
|
Requiere que el código incluya explícitamente: path="/"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ruta absoluta del archivo creado.
|
||||||
|
|
||||||
|
Ejemplo de uso mínimo:
|
||||||
|
|
||||||
|
mcp_generate_server(
|
||||||
|
code=\"\"\"from fastmcp import FastMCP
|
||||||
|
mcp = FastMCP()
|
||||||
|
|
||||||
|
@mcp.tool(description="Saluda al mundo.")
|
||||||
|
def hello():
|
||||||
|
return "Hola mundo"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run(transport="streamable-http", host="127.0.0.1", port=4211, path="/")
|
||||||
|
\"\"\"
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
# Validación: asegurar que el código incluya path="/"
|
||||||
|
if 'path="/"' not in code.replace(" ", "").replace("'", '"'):
|
||||||
|
raise ValueError('El código del servidor debe contener path="/".')
|
||||||
|
|
||||||
|
return generate_server(code, name)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run(transport="streamable-http", host="127.0.0.1", port=4210, path="/")
|
||||||
|
# mcp.run(transport="stdio")
|
||||||
+1
-1
@@ -129,5 +129,5 @@ def clear_folder(path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")
|
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/")
|
||||||
|
|
||||||
+1
-1
@@ -87,6 +87,6 @@ def is_prime(n: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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="/")
|
||||||
|
|
||||||
# mcp.run(transport="stdio")
|
# mcp.run(transport="stdio")
|
||||||
+1
-1
@@ -65,5 +65,5 @@ def current_timestamp() -> float:
|
|||||||
return datetime.datetime.now().timestamp()
|
return datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")
|
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/")
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select, delete
|
from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select, delete
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
||||||
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||||
|
|
||||||
|
|
||||||
class MemoryConvPostgres(MemoryConvABC):
|
class MemoryConvPostgres(MemoryConvABC):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from src.Llms.Modelos.Base_model import ModeloABC
|
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
from typing import AsyncGenerator, Union
|
from typing import AsyncGenerator, Union
|
||||||
from src.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
|
from domains.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
class ModeloOllama(ModeloABC):
|
class ModeloOllama(ModeloABC):
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from src.Llms.Modelos.Base_model import ModeloABC
|
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||||
from src.ConexionApis.OpenAi_conexion import OpenAICliente
|
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator, Union
|
from typing import AsyncGenerator, Union
|
||||||
|
|
||||||
@@ -2,15 +2,15 @@ import os
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from sqlalchemy import Column, Integer, String, Float, Boolean
|
from sqlalchemy import Column, Integer, String, Float, Boolean
|
||||||
|
|
||||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from domains.ArquitectureLayer.Repo import Repo_base
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
|
from domains.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
# Cargar clave maestra
|
# Cargar clave maestra
|
||||||
@@ -3,17 +3,17 @@ from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
from domains.ConexionSql.Postgres_conexion import PostgresConexion
|
||||||
from src.Credenciales.postgres_credencial import PostgresCredencial
|
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||||
|
|
||||||
class LoggerDB:
|
class LoggerDB:
|
||||||
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
|
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
|
||||||
|
|
||||||
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
|
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
|
||||||
if not LoggerDB._sink_removido:
|
|
||||||
logger.remove() # 🧹 elimina impresión en terminal
|
# 🔥 Elimina todos los sinks activos, incluso los automáticos
|
||||||
LoggerDB._sink_removido = True
|
logger.remove()
|
||||||
|
|
||||||
self.conexion = PostgresConexion(credencial)
|
self.conexion = PostgresConexion(credencial)
|
||||||
self.engine = self.conexion.get_engine()
|
self.engine = self.conexion.get_engine()
|
||||||
@@ -28,6 +28,8 @@ class LoggerDB:
|
|||||||
def _generar_modelo_logger(self):
|
def _generar_modelo_logger(self):
|
||||||
class LoggerTable(Model_base):
|
class LoggerTable(Model_base):
|
||||||
__tablename__ = self.nombre_tabla
|
__tablename__ = self.nombre_tabla
|
||||||
|
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
nivel = Column(String, nullable=False)
|
nivel = Column(String, nullable=False)
|
||||||
mensaje = Column(Text, nullable=False)
|
mensaje = Column(Text, nullable=False)
|
||||||
@@ -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
|
||||||
@@ -87,9 +87,9 @@ class Navegador:
|
|||||||
f"--user-data-dir={self.user_data_dir}",
|
f"--user-data-dir={self.user_data_dir}",
|
||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-web-security",
|
# "--disable-web-security",
|
||||||
# "--disable-extensions",
|
# "--disable-extensions",
|
||||||
"--disable-dev-shm-usage",
|
# "--disable-dev-shm-usage",
|
||||||
"--disable-infobars",
|
"--disable-infobars",
|
||||||
"--disable-popup-blocking",
|
"--disable-popup-blocking",
|
||||||
"--disable-default-apps",
|
"--disable-default-apps",
|
||||||
@@ -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
|
||||||
@@ -2,21 +2,29 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import websockets
|
import websockets
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from typing import List
|
from .ElementoWeb import ElementoWeb
|
||||||
from src.ScrappingWeb.ElementoWeb import ElementoWeb
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Tab:
|
class Tab:
|
||||||
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str):
|
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
self.ws_url = ws_url
|
self.ws_url = ws_url
|
||||||
self._message_id = 0
|
self._message_id = 0
|
||||||
self._pending = {}
|
self._pending = {}
|
||||||
self._load_event = asyncio.Event()
|
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
|
@classmethod
|
||||||
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
||||||
websocket = await websockets.connect(ws_url)
|
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
|
||||||
tab = cls(websocket, ws_url)
|
tab = cls(websocket, ws_url)
|
||||||
asyncio.create_task(tab._recibir_eventos())
|
asyncio.create_task(tab._recibir_eventos())
|
||||||
await tab._enviar("Page.enable")
|
await tab._enviar("Page.enable")
|
||||||
@@ -28,11 +36,14 @@ class Tab:
|
|||||||
data = json.loads(mensaje)
|
data = json.loads(mensaje)
|
||||||
if "id" in data and data["id"] in self._pending:
|
if "id" in data and data["id"] in self._pending:
|
||||||
future = self._pending.pop(data["id"])
|
future = self._pending.pop(data["id"])
|
||||||
future.set_result(data.get("result"))
|
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":
|
elif data.get("method") == "Page.loadEventFired":
|
||||||
self._load_event.set()
|
self._load_event.set()
|
||||||
|
|
||||||
async def _enviar(self, metodo: str, parametros: Optional[dict] = None) -> dict:
|
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
|
||||||
self._message_id += 1
|
self._message_id += 1
|
||||||
msg_id = self._message_id
|
msg_id = self._message_id
|
||||||
mensaje = {
|
mensaje = {
|
||||||
@@ -44,15 +55,17 @@ class Tab:
|
|||||||
future = asyncio.get_event_loop().create_future()
|
future = asyncio.get_event_loop().create_future()
|
||||||
self._pending[msg_id] = future
|
self._pending[msg_id] = future
|
||||||
await self.websocket.send(json.dumps(mensaje))
|
await self.websocket.send(json.dumps(mensaje))
|
||||||
return await future
|
return await asyncio.wait_for(future, timeout=timeout)
|
||||||
|
|
||||||
async def navegar(self, url: str, wait_time: float = 5.0):
|
async def navegar(self, url: str, wait_time: float = 5.0):
|
||||||
self._load_event.clear()
|
self._load_event.clear()
|
||||||
print(f"🌍 Navegando a: {url}")
|
if self.verbose:
|
||||||
|
print(f"🌍 Navegando a: {url}")
|
||||||
await self._enviar("Page.navigate", {"url": url})
|
await self._enviar("Page.navigate", {"url": url})
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
||||||
print("✅ Página cargada correctamente.")
|
if self.verbose:
|
||||||
|
print("✅ Página cargada correctamente.")
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
||||||
|
|
||||||
@@ -62,11 +75,40 @@ class Tab:
|
|||||||
"expression": js_code,
|
"expression": js_code,
|
||||||
"returnByValue": True
|
"returnByValue": True
|
||||||
})
|
})
|
||||||
return result["result"]["value"]
|
if "exceptionDetails" in result:
|
||||||
|
raise Exception(result["exceptionDetails"])
|
||||||
|
return result.get("result", {}).get("value")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al ejecutar JS: {e}")
|
print(f"⚠️ Error al ejecutar JS: {e}")
|
||||||
return None
|
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]:
|
async def obtener_user_agent(self) -> Optional[str]:
|
||||||
return await self.evaluar_js("navigator.userAgent")
|
return await self.evaluar_js("navigator.userAgent")
|
||||||
|
|
||||||
@@ -76,66 +118,57 @@ class Tab:
|
|||||||
data = result["data"]
|
data = result["data"]
|
||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write(base64.b64decode(data))
|
f.write(base64.b64decode(data))
|
||||||
print(f"📸 Screenshot guardado como {output_path}")
|
if self.verbose:
|
||||||
|
print(f"📸 Screenshot guardado como {output_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al capturar screenshot: {e}")
|
print(f"⚠️ Error al capturar screenshot: {e}")
|
||||||
|
|
||||||
async def cerrar(self):
|
async def cerrar(self):
|
||||||
try:
|
try:
|
||||||
await self.websocket.close()
|
if not self.websocket.closed:
|
||||||
print("🛑 WebSocket cerrado.")
|
await self.websocket.close()
|
||||||
|
if self.verbose:
|
||||||
|
print("🛑 WebSocket cerrado.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al cerrar pestaña: {e}")
|
print(f"⚠️ Error al cerrar pestaña: {e}")
|
||||||
|
|
||||||
async def obtener_html_completo(self) -> Optional[str]:
|
async def obtener_html_completo(self) -> Optional[str]:
|
||||||
"""
|
|
||||||
Devuelve el HTML completo de la página actual.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
result = await self._enviar("Runtime.evaluate", {
|
result = await self._enviar("Runtime.evaluate", {
|
||||||
"expression": "document.documentElement.outerHTML",
|
"expression": "document.documentElement.outerHTML",
|
||||||
"returnByValue": True
|
"returnByValue": True
|
||||||
})
|
})
|
||||||
html = result["result"]["value"]
|
return result.get("result", {}).get("value")
|
||||||
print("📄 HTML completo obtenido.")
|
|
||||||
return html
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al obtener HTML: {e}")
|
print(f"⚠️ Error al obtener HTML: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def obtener_dominio(self) -> Optional[str]:
|
async def obtener_dominio(self) -> Optional[str]:
|
||||||
"""
|
|
||||||
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
dominio = await self.evaluar_js("window.location.hostname")
|
dominio = await self.evaluar_js("window.location.hostname")
|
||||||
print(f"🌐 Dominio actual: {dominio}")
|
if self.verbose and dominio:
|
||||||
|
print(f"🌐 Dominio actual: {dominio}")
|
||||||
return dominio
|
return dominio
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al obtener dominio: {e}")
|
print(f"⚠️ Error al obtener dominio: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
||||||
try:
|
try:
|
||||||
# Obtener nodo raíz del documento
|
|
||||||
doc = await self._enviar("DOM.getDocument")
|
doc = await self._enviar("DOM.getDocument")
|
||||||
root_node_id = doc["root"]["nodeId"]
|
root_node_id = doc["root"]["nodeId"]
|
||||||
|
|
||||||
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
|
|
||||||
result = await self._enviar("DOM.querySelector", {
|
result = await self._enviar("DOM.querySelector", {
|
||||||
"nodeId": root_node_id,
|
"nodeId": root_node_id,
|
||||||
"selector": selector
|
"selector": selector
|
||||||
})
|
})
|
||||||
node_id = result["nodeId"]
|
node_id = result.get("nodeId")
|
||||||
|
|
||||||
if not node_id:
|
if not node_id:
|
||||||
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return ElementoWeb.from_node(self, node_id=node_id)
|
return ElementoWeb.from_node(self, node_id=node_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -157,8 +190,17 @@ class Tab:
|
|||||||
for prop in props["result"]:
|
for prop in props["result"]:
|
||||||
if "value" in prop and "objectId" in prop["value"]:
|
if "value" in prop and "objectId" in prop["value"]:
|
||||||
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
||||||
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
if self.verbose:
|
||||||
|
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
||||||
return elementos
|
return elementos
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
|
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
|
||||||
return []
|
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 domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
|
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
|
||||||
from typing import List, Optional
|
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 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 sqlalchemy import inspect
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
|
|
||||||
|
|
||||||
class Biblioteca:
|
class Biblioteca:
|
||||||
@@ -3,16 +3,16 @@ import base64
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from sqlalchemy import Column, String, Integer
|
from sqlalchemy import Column, String, Integer
|
||||||
|
|
||||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from domains.ArquitectureLayer.Repo import Repo_base
|
||||||
|
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.Security.Encriptar import Encriptar_fernet
|
from domains.Security.Encriptar import Encriptar_fernet
|
||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
from src.Llms.Embedders.Base_Embedder import EmbedderABC
|
from domains.Llms.Embedders.Base_Embedder import EmbedderABC
|
||||||
from src.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
|
from domains.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
# Cargar clave maestra
|
# Cargar clave maestra
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from src.Security.GenerarIDs import GeneradorIDUnico
|
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
class Nota:
|
class Nota:
|
||||||
@@ -3,18 +3,42 @@ from dotenv import load_dotenv
|
|||||||
from sqlalchemy import Table, Column, String, Text, MetaData
|
from sqlalchemy import Table, Column, String, Text, MetaData
|
||||||
from pgvector.sqlalchemy import Vector
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy.orm import registry, Session
|
from sqlalchemy.orm import registry, Session
|
||||||
from src.TextManager.nota import Nota
|
from domains.TextManager.nota import Nota
|
||||||
from src.ConexionSql.Base_conexion import ConexionBase
|
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
from src.ArquitectureLayer.Mapper import Mapper_base
|
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||||
from src.ArquitectureLayer.Model import Model_base
|
from domains.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_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
|
# 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.
|
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
|
||||||
"""
|
"""
|
||||||
try:
|
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 SQL-safe
|
||||||
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
|
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
|
# Modelo ORM dinámico
|
||||||
class NotaModel(Base, Model_base):
|
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 = Column(Vector(vector_dim), nullable=True)
|
||||||
vector_resumen = 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}'")
|
logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
|
||||||
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
|
logger.debug(f"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]}")
|
logger.debug(f"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.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
|
||||||
|
|
||||||
return NotaModel.__table__, NotaModel
|
return NotaModel.__table__, NotaModel
|
||||||
|
|
||||||
except Exception as e:
|
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
|
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
|
# entrypoint/init_db.py
|
||||||
|
|
||||||
from src.base import Base
|
from domains.base import Base
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
|
from domains.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.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
|
||||||
|
|
||||||
from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
|
from domains.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
|
||||||
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
|
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
|
||||||
from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
|
from domains.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
|
||||||
from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
|
from domains.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
|
||||||
from src.TextManager.biblioteca_mmr import BibliotecaModel
|
from domains.TextManager.biblioteca_mmr import BibliotecaModel
|
||||||
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|||||||
Generated
+2931
-35
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -20,18 +20,25 @@
|
|||||||
"storybook:build": "storybook build"
|
"storybook:build": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "8.0.0",
|
"@cycjimmy/jsmpeg-player": "^6.1.2",
|
||||||
"@mantine/hooks": "8.0.0",
|
"@mantine/core": "^8.0.1",
|
||||||
|
"@mantine/hooks": "^8.0.1",
|
||||||
|
"@mantine/tiptap": "^8.0.1",
|
||||||
"@react-three/fiber": "^9.1.2",
|
"@react-three/fiber": "^9.1.2",
|
||||||
"@tabler/icons": "^3.31.0",
|
"@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",
|
"axios": "^1.9.0",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
|
"marked": "^15.0.12",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.2",
|
||||||
"react-router-dom": "^7.4.0"
|
"react-router-dom": "^7.4.0",
|
||||||
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.23.0",
|
||||||
@@ -46,6 +53,7 @@
|
|||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-config-mantine": "^4.0.3",
|
"eslint-config-mantine": "^4.0.3",
|
||||||
|
|||||||
+65
-19
@@ -1,39 +1,85 @@
|
|||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
import { HomePage } from './pages/Home.page';
|
import { HomePage } from './frontend_domains/Home/Home.page';
|
||||||
import { Consulta_API } from './pages/Consulta_api';
|
import { Consulta_API } from './frontend_domains/Experiments/Consulta_api';
|
||||||
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
|
import { Error_404 } from './frontend_domains/FitzStudio/404/404'; // Ajusta si está en otra carpeta
|
||||||
import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
|
import { Grid_Dashboard } from './frontend_domains/Experiments/Grid_dashboard'; // Ajusta si está en otra carpeta
|
||||||
import { Biblioteca } from './pages/Biblioteca';
|
import { Biblioteca } from './frontend_domains/TextEditor/Biblioteca';
|
||||||
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
|
import { VisualizacionesRandom } from './frontend_domains/Experiments/Visualizaciones_Random';
|
||||||
import { Camara_noir } from './pages/Camaras_noir';
|
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([
|
const router = createBrowserRouter([
|
||||||
|
|
||||||
|
// Home Principal
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <HomePage />,
|
element: <HomePage />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Biblioteca
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/Consulta_API',
|
path: '/bibliot/Biblioteca',
|
||||||
element: <Consulta_API />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/Grid_Dashboard',
|
|
||||||
element: <Grid_Dashboard />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/Biblioteca',
|
|
||||||
element: <Biblioteca />,
|
element: <Biblioteca />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/analytics/Visualizaciones_Random',
|
path: '/bibliot/editortest',
|
||||||
element: <VisualizacionesRandom />,
|
element: <EditorTest />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Chat LLM
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/analytics/Camaras',
|
path: '/llms/chat',
|
||||||
|
element: <ChatPage />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// CamaraNoir
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/camara/principal',
|
||||||
element: <Camara_noir />,
|
element: <Camara_noir />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Experimentos
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/experiments/Consulta_API',
|
||||||
|
element: <Consulta_API />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/experiments/Grid_Dashboard',
|
||||||
|
element: <Grid_Dashboard />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/experiments/Visualizaciones_Random',
|
||||||
|
element: <VisualizacionesRandom />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Login
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <LoginPage />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// FitzStudio Pages -------------------------------------------------------
|
||||||
|
// Error 404
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
element: <Error_404 />,
|
element: <Error_404 />,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// https://tabler.io/icons
|
||||||
|
|
||||||
// OUTLINED
|
// OUTLINED
|
||||||
export { default as IconArrowLeft } from './outlined/arrow-left.svg?react';
|
export { default as IconArrowLeft } from './outlined/arrow-left.svg?react';
|
||||||
export { default as IconHomeOutline } from './outlined/home.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 IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react';
|
||||||
export { default as IconArrowBarRight } from './outlined/arrow-bar-right.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 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
|
// FILLED
|
||||||
export { default as IconHomeFilled } from './filled/home.svg?react';
|
export { default as IconHomeFilled } from './filled/home.svg?react';
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
// Archivo: components/CanvasDisplay.tsx
|
|
||||||
import { Box } from '@mantine/core';
|
|
||||||
import { RefObject } from 'react';
|
|
||||||
|
|
||||||
interface CanvasDisplayProps {
|
|
||||||
canvasRef: RefObject<HTMLCanvasElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CanvasDisplay({ canvasRef }: CanvasDisplayProps) {
|
|
||||||
return (
|
|
||||||
<Box style={{ position: 'relative', width: '100%', height: 480 }}>
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
width={640}
|
|
||||||
height={480}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: 8,
|
|
||||||
backgroundColor: '#000',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
// Archivo: components/CaptureGrid.tsx
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { SimpleGrid } from '@mantine/core';
|
|
||||||
import { FrameCard } from './FrameCard';
|
|
||||||
|
|
||||||
interface CaptureGridProps {
|
|
||||||
totalSlots: number;
|
|
||||||
capturas: string[][];
|
|
||||||
setCapturas: (value: string[][]) => void;
|
|
||||||
frameIndices: number[];
|
|
||||||
setFrameIndices: (value: number[]) => void;
|
|
||||||
fijados: boolean[];
|
|
||||||
setFijados: (value: boolean[]) => void;
|
|
||||||
frameTemporal: number | null;
|
|
||||||
onScroll: (e: React.WheelEvent, index: number) => void;
|
|
||||||
onSliderChange: (val: number) => void;
|
|
||||||
onSliderEnd: (index: number, val: number) => void;
|
|
||||||
numColumnas: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CaptureGrid({
|
|
||||||
totalSlots,
|
|
||||||
capturas,
|
|
||||||
setCapturas,
|
|
||||||
frameIndices,
|
|
||||||
setFrameIndices,
|
|
||||||
fijados,
|
|
||||||
setFijados,
|
|
||||||
frameTemporal,
|
|
||||||
onScroll,
|
|
||||||
onSliderChange,
|
|
||||||
onSliderEnd,
|
|
||||||
numColumnas,
|
|
||||||
}: CaptureGridProps) {
|
|
||||||
const handleImageClick = useCallback((index: number, dataUrl: string) => {
|
|
||||||
const nuevasCapturas = [...capturas];
|
|
||||||
nuevasCapturas[index] = [dataUrl];
|
|
||||||
setCapturas(nuevasCapturas);
|
|
||||||
|
|
||||||
const nuevosFijados = [...fijados];
|
|
||||||
nuevosFijados[index] = true;
|
|
||||||
setFijados(nuevosFijados);
|
|
||||||
|
|
||||||
const nuevosIndices = [...frameIndices];
|
|
||||||
nuevosIndices[index] = 0;
|
|
||||||
setFrameIndices(nuevosIndices);
|
|
||||||
}, [capturas, fijados, frameIndices, setCapturas, setFijados, setFrameIndices]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SimpleGrid cols={numColumnas} style={{ flex: 1, columnGap: 12, rowGap: 4 }}>
|
|
||||||
{Array.from({ length: totalSlots }).map((_, i) => {
|
|
||||||
const frames = capturas[i] ?? [];
|
|
||||||
const currentIndex =
|
|
||||||
!fijados[i] && frameTemporal !== null && frames.length > 0
|
|
||||||
? Math.max(0, Math.min(frames.length - 1, frameTemporal))
|
|
||||||
: frameIndices[i];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FrameCard
|
|
||||||
key={i}
|
|
||||||
index={i}
|
|
||||||
frames={frames}
|
|
||||||
currentIndex={currentIndex}
|
|
||||||
fijado={fijados[i]}
|
|
||||||
onScroll={onScroll}
|
|
||||||
onSliderChange={onSliderChange}
|
|
||||||
onSliderEnd={onSliderEnd}
|
|
||||||
onImageClick={handleImageClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SimpleGrid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Archivo: components/ControlPanel.tsx
|
|
||||||
import { Button, Group } from '@mantine/core';
|
|
||||||
|
|
||||||
interface ControlPanelProps {
|
|
||||||
grabando: boolean;
|
|
||||||
onAlternar: () => void;
|
|
||||||
onLimpiar: () => void;
|
|
||||||
onDesfijar: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ControlPanel({ grabando, onAlternar, onLimpiar, onDesfijar }: ControlPanelProps) {
|
|
||||||
return (
|
|
||||||
<Group gap="sm">
|
|
||||||
<Button onClick={onAlternar} variant="light" color={grabando ? 'orange' : 'blue'}>
|
|
||||||
{grabando ? 'Grabando... (Presiona Espacio)' : 'Iniciar grabación'}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onLimpiar} variant="filled" color="red">
|
|
||||||
Eliminar imágenes
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onDesfijar} variant="default" color="gray">
|
|
||||||
Desfijar todos (D)
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Archivo: components/FrameCard.tsx
|
|
||||||
import { Box, HoverCard, Image, Slider, Stack, Text } from '@mantine/core';
|
|
||||||
|
|
||||||
interface FrameCardProps {
|
|
||||||
index: number;
|
|
||||||
frames: string[];
|
|
||||||
currentIndex: number;
|
|
||||||
fijado: boolean;
|
|
||||||
onScroll: (e: React.WheelEvent, index: number) => void;
|
|
||||||
onSliderChange: (val: number) => void;
|
|
||||||
onSliderEnd: (index: number, val: number) => void;
|
|
||||||
onImageClick?: (index: number, dataUrl: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FrameCard({
|
|
||||||
index,
|
|
||||||
frames,
|
|
||||||
currentIndex,
|
|
||||||
fijado,
|
|
||||||
onScroll,
|
|
||||||
onSliderChange,
|
|
||||||
onSliderEnd,
|
|
||||||
onImageClick,
|
|
||||||
}: FrameCardProps) {
|
|
||||||
const borderColor = fijado ? '#4caf50' : '#ccc';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HoverCard key={index} width={700} shadow="md" position="top" withArrow>
|
|
||||||
<HoverCard.Target>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
width: 160,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 8,
|
|
||||||
border: `2px solid ${borderColor}`,
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'relative',
|
|
||||||
background: '#f1f1f1',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{frames.length > 0 && currentIndex < frames.length && (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
src={frames[currentIndex]}
|
|
||||||
alt={`Captura ${index}`}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
fit="cover"
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 4,
|
|
||||||
right: 6,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: '2px 4px',
|
|
||||||
fontSize: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentIndex + 1} / {frames.length}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</HoverCard.Target>
|
|
||||||
|
|
||||||
<HoverCard.Dropdown p="xs" onWheel={(e) => onScroll(e, index)}>
|
|
||||||
{frames.length > 0 && currentIndex < frames.length && (
|
|
||||||
<Stack gap={6}>
|
|
||||||
<Image
|
|
||||||
src={frames[currentIndex]}
|
|
||||||
alt={`Vista ampliada ${index}`}
|
|
||||||
style={{ width: '100%', height: 'auto', maxWidth: '100%', cursor: 'zoom-in' }}
|
|
||||||
fit="contain"
|
|
||||||
onClick={async (e) => {
|
|
||||||
const imgElement = e.currentTarget;
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = frames[currentIndex];
|
|
||||||
await img.decode();
|
|
||||||
|
|
||||||
const rect = imgElement.getBoundingClientRect();
|
|
||||||
const clickX = e.clientX - rect.left;
|
|
||||||
const clickY = e.clientY - rect.top;
|
|
||||||
const ratioX = clickX / rect.width;
|
|
||||||
const ratioY = clickY / rect.height;
|
|
||||||
|
|
||||||
const zoomFactor = 2;
|
|
||||||
const cropWidth = img.width / zoomFactor;
|
|
||||||
const cropHeight = img.height / zoomFactor;
|
|
||||||
|
|
||||||
const centerX = img.width * ratioX;
|
|
||||||
const centerY = img.height * ratioY;
|
|
||||||
|
|
||||||
const x = Math.max(0, Math.min(img.width - cropWidth, centerX - cropWidth / 2));
|
|
||||||
const y = Math.max(0, Math.min(img.height - cropHeight, centerY - cropHeight / 2));
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = cropWidth;
|
|
||||||
canvas.height = cropHeight;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(img, x, y, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
||||||
const dataUrl = canvas.toDataURL('image/jpeg');
|
|
||||||
onImageClick?.(index, dataUrl);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
value={currentIndex}
|
|
||||||
onChange={(val) => onSliderChange(val)}
|
|
||||||
onChangeEnd={(val) => onSliderEnd(index, val)}
|
|
||||||
min={0}
|
|
||||||
max={frames.length - 1}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</HoverCard.Dropdown>
|
|
||||||
</HoverCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Archivo: components/GridConfigPanel.tsx
|
|
||||||
import { NumberInput, Stack, Text } from '@mantine/core';
|
|
||||||
|
|
||||||
interface GridConfigPanelProps {
|
|
||||||
numFilas: number;
|
|
||||||
setNumFilas: (val: number) => void;
|
|
||||||
numColumnas: number;
|
|
||||||
setNumColumnas: (val: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GridConfigPanel({ numFilas, setNumFilas, numColumnas, setNumColumnas }: GridConfigPanelProps) {
|
|
||||||
return (
|
|
||||||
<Stack align="center" gap="xs" ml="md">
|
|
||||||
<Text size="sm">Cartas</Text>
|
|
||||||
<NumberInput
|
|
||||||
value={numFilas}
|
|
||||||
onChange={(val) => setNumFilas(Number(val))}
|
|
||||||
min={1}
|
|
||||||
max={5}
|
|
||||||
step={1}
|
|
||||||
size="xs"
|
|
||||||
style={{ width: 60 }}
|
|
||||||
/>
|
|
||||||
<Text size="sm">Jugadores</Text>
|
|
||||||
<NumberInput
|
|
||||||
value={numColumnas}
|
|
||||||
onChange={(val) => setNumColumnas(Number(val))}
|
|
||||||
min={1}
|
|
||||||
max={11}
|
|
||||||
step={1}
|
|
||||||
size="xs"
|
|
||||||
style={{ width: 60 }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * from './CanvasDisplay';
|
|
||||||
export * from './ControlPanel';
|
|
||||||
export * from './GridConfigPanel';
|
|
||||||
export * from './CaptureGrid';
|
|
||||||
export * from './FrameCard';
|
|
||||||
export * from './useCamaraNoir';
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
export function useCamaraNoir() {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
||||||
const bufferPrevioRef = useRef<string[]>([]);
|
|
||||||
const bufferGrabacionRef = useRef<string[]>([]);
|
|
||||||
const bufferAcumuladoRef = useRef<string[]>([]);
|
|
||||||
const pregrabacionActivaRef = useRef(true);
|
|
||||||
const primeraGrabacionRealizadaRef = useRef(false);
|
|
||||||
|
|
||||||
const [bufferGlobal, setBufferGlobal] = useState<string[]>([]);
|
|
||||||
const [intervaloId, setIntervaloId] = useState<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
const [grabando, setGrabando] = useState(false);
|
|
||||||
|
|
||||||
const [capturas, setCapturas] = useState<string[][]>([]);
|
|
||||||
const [frameIndices, setFrameIndices] = useState<number[]>([]);
|
|
||||||
const [fijados, setFijados] = useState<boolean[]>([]);
|
|
||||||
const [frameTemporal, setFrameTemporal] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const [numFilas, setNumFilas] = useState(2);
|
|
||||||
const [numColumnas, setNumColumnas] = useState(10);
|
|
||||||
|
|
||||||
const DELAY_ENTRE_FRAMES_MS = 10;
|
|
||||||
const SEGUNDOS_PRE_GRABACION = 5;
|
|
||||||
const FPS = 1000 / DELAY_ENTRE_FRAMES_MS;
|
|
||||||
const FRAMES_PRE_GRABACION = Math.floor(FPS * SEGUNDOS_PRE_GRABACION);
|
|
||||||
const totalSlots = numFilas * numColumnas;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const id = setInterval(() => {
|
|
||||||
if (!pregrabacionActivaRef.current) return;
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
const frame = canvas.toDataURL('image/jpeg');
|
|
||||||
bufferPrevioRef.current.push(frame);
|
|
||||||
if (bufferPrevioRef.current.length > FRAMES_PRE_GRABACION) {
|
|
||||||
bufferPrevioRef.current.shift();
|
|
||||||
}
|
|
||||||
}, DELAY_ENTRE_FRAMES_MS);
|
|
||||||
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const desfijarTodos = () => {
|
|
||||||
setFijados(Array(totalSlots).fill(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const socket = new WebSocket('ws://10.8.0.9:8000/ws');
|
|
||||||
socket.binaryType = 'blob';
|
|
||||||
|
|
||||||
socket.onmessage = async (event) => {
|
|
||||||
if (event.data instanceof Blob) {
|
|
||||||
const imgBitmap = await createImageBitmap(event.data);
|
|
||||||
ctx.drawImage(imgBitmap, 0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = (e) => console.error('WebSocket error:', e);
|
|
||||||
socket.onclose = () => console.warn('WebSocket cerrado');
|
|
||||||
|
|
||||||
return () => socket.close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.code === 'Space') {
|
|
||||||
event.preventDefault();
|
|
||||||
alternarGrabacion();
|
|
||||||
} else if (event.key.toLowerCase() === 'f') {
|
|
||||||
event.preventDefault();
|
|
||||||
limpiarCapturas();
|
|
||||||
} else if (event.key.toLowerCase() === 'd') {
|
|
||||||
event.preventDefault();
|
|
||||||
desfijarTodos();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [grabando, bufferGlobal, totalSlots]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const total = numFilas * numColumnas;
|
|
||||||
const nuevoCapturas = Array(total)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, i) => capturas[i] ?? bufferGlobal);
|
|
||||||
const nuevoIndices = Array(total)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, i) => frameIndices[i] ?? (frameTemporal ?? Math.floor(bufferGlobal.length / 2)));
|
|
||||||
const nuevosFijados = Array(total)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, i) => fijados[i] ?? false);
|
|
||||||
|
|
||||||
setCapturas(nuevoCapturas);
|
|
||||||
setFrameIndices(nuevoIndices);
|
|
||||||
setFijados(nuevosFijados);
|
|
||||||
}, [numFilas, numColumnas]);
|
|
||||||
|
|
||||||
const iniciarGrabacion = () => {
|
|
||||||
if (grabando) return;
|
|
||||||
setGrabando(true);
|
|
||||||
bufferGrabacionRef.current = [];
|
|
||||||
|
|
||||||
if (!primeraGrabacionRealizadaRef.current) {
|
|
||||||
pregrabacionActivaRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = setInterval(() => {
|
|
||||||
const frame = capturarFrame();
|
|
||||||
if (frame) {
|
|
||||||
bufferGrabacionRef.current.push(frame);
|
|
||||||
}
|
|
||||||
}, DELAY_ENTRE_FRAMES_MS);
|
|
||||||
|
|
||||||
setIntervaloId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const capturarFrame = (): string | null => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return null;
|
|
||||||
return canvas.toDataURL('image/jpeg');
|
|
||||||
};
|
|
||||||
|
|
||||||
const detenerGrabacion = () => {
|
|
||||||
if (!grabando) return;
|
|
||||||
setGrabando(false);
|
|
||||||
if (intervaloId) clearInterval(intervaloId);
|
|
||||||
|
|
||||||
const nuevaSesion = primeraGrabacionRealizadaRef.current
|
|
||||||
? [...bufferGrabacionRef.current] // solo frames nuevos
|
|
||||||
: [...bufferPrevioRef.current, ...bufferGrabacionRef.current];
|
|
||||||
|
|
||||||
bufferAcumuladoRef.current.push(...nuevaSesion);
|
|
||||||
primeraGrabacionRealizadaRef.current = true;
|
|
||||||
|
|
||||||
setBufferGlobal([...bufferAcumuladoRef.current]);
|
|
||||||
setCapturas(Array(totalSlots).fill([...bufferAcumuladoRef.current]));
|
|
||||||
const frameInicial = Math.floor(bufferAcumuladoRef.current.length / 2);
|
|
||||||
setFrameIndices(Array(totalSlots).fill(frameInicial));
|
|
||||||
setFijados(Array(totalSlots).fill(false));
|
|
||||||
setFrameTemporal(frameInicial);
|
|
||||||
};
|
|
||||||
|
|
||||||
const alternarGrabacion = () => {
|
|
||||||
grabando ? detenerGrabacion() : iniciarGrabacion();
|
|
||||||
};
|
|
||||||
|
|
||||||
const limpiarCapturas = () => {
|
|
||||||
setCapturas([]);
|
|
||||||
setFrameIndices([]);
|
|
||||||
setFijados([]);
|
|
||||||
setBufferGlobal([]);
|
|
||||||
bufferGrabacionRef.current = [];
|
|
||||||
bufferPrevioRef.current = [];
|
|
||||||
bufferAcumuladoRef.current = [];
|
|
||||||
primeraGrabacionRealizadaRef.current = false;
|
|
||||||
pregrabacionActivaRef.current = true;
|
|
||||||
setFrameTemporal(null);
|
|
||||||
if (intervaloId) clearInterval(intervaloId);
|
|
||||||
setGrabando(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moverFrame = (capturaIndex: number, nuevoIndice: number) => {
|
|
||||||
setFrameIndices((prev) => {
|
|
||||||
const nuevos = [...prev];
|
|
||||||
const max = capturas[capturaIndex]?.length - 1 ?? 0;
|
|
||||||
nuevos[capturaIndex] = Math.max(0, Math.min(max, nuevoIndice));
|
|
||||||
return nuevos;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const moverFrameYFijar = (capturaIndex: number, nuevoIndice: number) => {
|
|
||||||
moverFrame(capturaIndex, nuevoIndice);
|
|
||||||
setFijados((prev) => {
|
|
||||||
const actualizados = [...prev];
|
|
||||||
actualizados[capturaIndex] = true;
|
|
||||||
return actualizados;
|
|
||||||
});
|
|
||||||
setFrameTemporal(nuevoIndice);
|
|
||||||
};
|
|
||||||
|
|
||||||
const manejarScrollEnSlider = (e: React.WheelEvent, index: number) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!fijados[index]) return;
|
|
||||||
moverFrame(index, frameIndices[index] + (e.deltaY > 0 ? 1 : -1));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvasRef,
|
|
||||||
bufferGlobal,
|
|
||||||
grabando,
|
|
||||||
capturas,
|
|
||||||
frameIndices,
|
|
||||||
fijados,
|
|
||||||
frameTemporal,
|
|
||||||
numFilas,
|
|
||||||
setNumFilas,
|
|
||||||
numColumnas,
|
|
||||||
setNumColumnas,
|
|
||||||
alternarGrabacion,
|
|
||||||
limpiarCapturas,
|
|
||||||
desfijarTodos,
|
|
||||||
manejarScrollEnSlider,
|
|
||||||
moverFrameYFijar,
|
|
||||||
setFrameTemporal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,20 @@ import {
|
|||||||
IconDeviceDesktopAnalytics,
|
IconDeviceDesktopAnalytics,
|
||||||
IconFingerprint,
|
IconFingerprint,
|
||||||
IconGauge,
|
IconGauge,
|
||||||
|
IconNotebook,
|
||||||
IconHome2,
|
IconHome2,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserOutline as IconUser,
|
IconUserOutline as IconUser,
|
||||||
|
CameraPlus,
|
||||||
|
Flask,
|
||||||
|
Users
|
||||||
} from '../assets/icons'; // ajusta según tu estructura de proyecto
|
} from '../assets/icons'; // ajusta según tu estructura de proyecto
|
||||||
|
|
||||||
export const mainLinksdata = [
|
export const mainLinksdata = [
|
||||||
{ icon: IconHome2, label: 'Home' },
|
{ icon: IconHome2, label: 'Home' },
|
||||||
{ icon: IconGauge, label: 'Dashboard' },
|
{ icon: IconNotebook, label: 'Biblioteca' },
|
||||||
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' },
|
{ icon: Users, label: 'AgentesLLMs' },
|
||||||
{ icon: IconCalendarStats, label: 'Releases' },
|
{ icon: CameraPlus, label: 'CameraNoir' },
|
||||||
{ icon: IconUser, label: 'Account' },
|
{ icon: Flask, label: 'Experimentos' },
|
||||||
{ icon: IconFingerprint, label: 'Security' },
|
|
||||||
{ icon: IconSettings, label: 'Settings' },
|
{ icon: IconSettings, label: 'Settings' },
|
||||||
];
|
];
|
||||||
@@ -1,35 +1,49 @@
|
|||||||
// src/data/submenuLinks.ts
|
// src/data/submenuLinks.ts
|
||||||
|
|
||||||
|
import { Biblioteca } from "@/frontend_domains/TextEditor/Biblioteca";
|
||||||
|
|
||||||
export const submenuLinks = {
|
export const submenuLinks = {
|
||||||
|
|
||||||
|
// Home Principal
|
||||||
|
|
||||||
Home: [
|
Home: [
|
||||||
{ label: 'Inicio', to: '/' },
|
{ label: 'Inicio', to: '/' },
|
||||||
{ label: 'Consulta Api', to: '/Consulta_API' },
|
|
||||||
{ label: 'Biblioteca', to: '/Biblioteca' },
|
|
||||||
|
|
||||||
],
|
],
|
||||||
Dashboard: [
|
|
||||||
{ label: 'Resumen', to: '/dashboard/resumen' },
|
// Biblioteca
|
||||||
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
|
|
||||||
{ label: 'Estadísticas', to: '/dashboard/estadisticas' },
|
Biblioteca: [
|
||||||
{ label: 'Usuarios', to: '/dashboard/usuarios' },
|
{ label: 'Biblioteca', to: '/bibliot/Biblioteca' },
|
||||||
|
{ label: 'test', to: '/bibliot/editortest' },
|
||||||
|
|
||||||
],
|
],
|
||||||
Analytics: [
|
|
||||||
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
|
|
||||||
{ label: 'Camaras', to: '/analytics/Camaras' },
|
|
||||||
{ 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' },
|
// Camara
|
||||||
{ label: 'Historial', to: '/releases/historial' },
|
CameraNoir: [
|
||||||
|
{ label: 'Camara_principal', to: '/camara/principal' },
|
||||||
],
|
],
|
||||||
Account: [
|
|
||||||
{ label: 'Perfil', to: '/account/perfil' },
|
// LLms
|
||||||
{ label: 'Suscripciones', to: '/account/suscripciones' },
|
|
||||||
],
|
AgentesLLMs: [
|
||||||
Security: [
|
{ label: 'LLMs', to: '/llms' },
|
||||||
{ label: 'Contraseña', to: '/security/contraseña' },
|
{ label: 'Chat', to: '/llms/chat' },
|
||||||
{ label: '2FA', to: '/security/2fa' },
|
{ label: 'Documentos', to: '/llms/documentos' },
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Settings
|
||||||
Settings: [
|
Settings: [
|
||||||
{ label: 'Preferencias', to: '/settings/preferencias' },
|
{ label: 'Preferencias', to: '/settings/preferencias' },
|
||||||
{ label: 'Notificaciones', to: '/settings/notificaciones' },
|
{ 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 { LlamadorAPI } from './LlamadorAPI';
|
||||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||||
|
|
||||||
|
|
||||||
export function Consulta_API() {
|
export function Consulta_API() {
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { Grid } from '@mantine/core';
|
import { Grid } from '@mantine/core';
|
||||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||||
import { GridDashboard } from '../components/Grid_dashboard';
|
import { GridDashboard } from './Grid_dashboard_component';
|
||||||
|
|
||||||
export function Grid_Dashboard() {
|
export function Grid_Dashboard() {
|
||||||
return (
|
return (
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Select, Group } from '@mantine/core';
|
import { Select, Group } from '@mantine/core';
|
||||||
import { IconCheck } from '../assets/icons';
|
import { IconCheck } from '../../assets/icons';
|
||||||
|
|
||||||
interface MetodoSelectProps {
|
interface MetodoSelectProps {
|
||||||
metodo: string;
|
metodo: string;
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||||
import { Card, Grid, Title, Loader } from '@mantine/core';
|
import { Card, Grid, Title, Loader } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
@@ -10,7 +10,7 @@ function useChartOption(endpoint: string) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`/api/${endpoint}`)
|
fetch(`/api/v1/charts/${endpoint}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => setOption(json))
|
.then((json) => setOption(json))
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Box, Title, Text, Button, Group, Stack, Image, Center } from '@mantine/core';
|
import { Box, Title, Text, Button, Group, Stack, Image, Center } from '@mantine/core';
|
||||||
import { useMantineTheme } 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 { Link } from 'react-router-dom';
|
||||||
import { MantineCardWithShader } from '../components/HoloShader'; // Ajusta ruta si es necesario
|
import { MantineCardWithShader } from './HoloShader_404'; // Ajusta ruta si es necesario
|
||||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
import { AppShellWithMenu } from '../Appshell/Appshell';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function Error_404() {
|
export function Error_404() {
|
||||||
+3
-3
@@ -11,9 +11,9 @@ import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { default as LogoIcon } from '../../assets/icons/favicon';
|
import { default as LogoIcon } from '../../../assets/icons/favicon';
|
||||||
import { mainLinksdata } from '../../data/navigationsLinks_1';
|
import { mainLinksdata } from '../../../data/navigationsLinks_1';
|
||||||
import { submenuLinks } from '../../data/submenuLinks_1';
|
import { submenuLinks } from '../../../data/submenuLinks_1';
|
||||||
|
|
||||||
import classes from './Appshell.module.css';
|
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() {
|
export function Plantilla() {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user