21 Commits

Author SHA1 Message Date
egutierrez daebea9e9c feat: Implement MCP server generation and process management scripts 2025-06-22 12:34:24 +02:00
egutierrez 58238a5918 feat: Update server scripts to use root path for HTTP transport and add process management scripts 2025-06-22 11:50:54 +02:00
egutierrez fa33321ff1 feat: Refactor backend structure by moving main application and router files to backend_domains 2025-06-22 10:05:04 +02:00
egutierrez a2873e4c85 feat: Add .continue to .gitignore 2025-06-21 14:51:20 +02:00
egutierrez 0d97aa2d61 feat: Remove unused Login page component 2025-06-21 02:18:55 +02:00
egutierrez 23f25034ad Merge remote-tracking branch 'origin/cambios_llm' 2025-06-21 02:17:56 +02:00
egutierrez 8ad80defcf feat: Remove @tabler/icons-react dependency and add Login page component 2025-06-21 02:13:19 +02:00
egutierrez aef8791151 feat: Implement main application shell with navigation and color scheme toggle
- Added Appshell component with responsive navbar and main content area
- Integrated ColorSchemeToggle for light/dark mode switching
- Created Welcome component with styled title and introductory text
- Developed ChatPage for LLM interaction with WebSocket support
- Implemented Biblioteca for managing notes with rich text editor
- Added LoginPage for user authentication with error handling
- Introduced MessageList and MessageBubble components for chat messages
- Styled components with CSS modules for consistent design
2025-06-21 02:01:21 +02:00
egutierrez 3d5deef0fb feat: Implement user management functionality with login and CRUD operations 2025-06-21 01:40:19 +02:00
egutierrez 3438102dc0 feat: Add initial task list for project planning and user management 2025-06-17 00:38:00 +02:00
egutierrez 9ee8daa295 feat: Implement WebSocket support for chat functionality and refactor chat service
- Added WebSocket endpoint for real-time chat interactions.
- Refactored ChatPage component to utilize WebSocket for sending and receiving messages.
- Updated chat service to handle streaming responses from the LLM agent.
- Introduced error handling for WebSocket connections and message processing.
- Modified Editor_Test to include AppShellWithMenu for better layout.
- Adjusted file path in generar_tree.py for correct directory structure.
- Created llm_chat_endpoint_v1.py and llm_chat_srvc.py for handling chat requests and responses.
- Established logging for WebSocket interactions and errors.
2025-06-17 00:19:36 +02:00
egutierrez 6d6fab5634 feat: Add Chat LLM functionality with message input and display components 2025-06-16 23:18:18 +02:00
egutierrez 9c638fc3e5 Generacion de estructura ddd para backend
feat: Refactor API structure by consolidating endpoints and removing deprecated files
2025-06-16 22:22:41 +02:00
egutierrez 43f6fb03fe feat: Refactor CamaraNoir component layout and update content for live streaming 2025-06-16 22:12:28 +02:00
egutierrez ac83907e7c feat: Update routing paths and enhance navigation links for Biblioteca and CameraNoir 2025-06-11 23:19:23 +02:00
egutierrez 3cd267ee6e Gestionado todo el frontend para domain driven design
feat: Implement LlamadorAPI component for API interaction with dynamic request handling

feat: Create MetodoSelect component for selecting HTTP methods with visual feedback

feat: Add VisualizacionesRandom component to display various charts using ECharts

feat: Develop custom 404 Error page with holographic shader effect

feat: Create HoloShader component for dynamic background effects on 404 page

style: Add CSS styles for Appshell layout and navigation

feat: Build Appshell component to manage application layout and navigation

feat: Add ColorSchemeToggle component for switching between light and dark themes

feat: Create Plantilla component as a template for future pages

style: Define styles for Welcome component

feat: Implement Welcome component with introductory text and links

feat: Develop HomePage component to serve as the main entry point of the application

feat: Create Biblioteca component for managing notes and libraries with rich text editor

feat: Add Editor_Test component for testing rich text editor functionality

style: Define styles for the rich text editor in Biblioteca
2025-06-11 22:53:32 +02:00
egutierrez e1b756ac99 feat: Implement cookie extraction script for Chrome v20 and enhance browser interaction 2025-06-01 15:31:13 +02:00
egutierrez 628cddc3ae feat: Enhance logging and add chart endpoints
- Updated LoggerDB to remove all active sinks on initialization.
- Added a new PostgresCredencial setup in notas_mmr.py for database connection.
- Replaced print statements with logger calls for better logging in notas_mmr.py.
- Introduced new FastAPI endpoints for various chart types (bar, line, pie, scatter).
- Created Editor_biblioteca.css for styling the rich text editor.
- Implemented Editor_Test.tsx to test the rich text editor functionality.
2025-06-01 00:33:48 +02:00
egutierrez cf6a768f6b Refactor and enhance MCP client and server functionality
- Removed prueba_cliente_mcp.py as it was no longer needed.
- Updated prueba_loop_agente.py to integrate MCPServerRunner for managing server instances.
- Modified prueba_mcp.py to implement a new structure for starting and stopping MCP servers.
- Enhanced AgenteAI class to support multiple MCP blocks execution.
- Improved MCPClient with timeout handling for tool calls.
- Added new sandbox files for children's stories.
- Created a simple ERP system with a main entry point.
- Added unit tests for the ERP system.
- Implemented MCPServerRunner to manage server processes.
- Developed server_files.py to handle file operations securely within a sandbox environment.
- Introduced ElementoWeb and Navegador classes for web scraping functionalities.
- Enhanced Scrapper and Tab classes for better interaction with web pages.
2025-05-25 13:49:08 +02:00
egutierrez a62778a030 feat: add ECharts and related components for data visualization
- Added `echarts` and `echarts-for-react` dependencies to the project.
- Created new pages for visualizations: `VisualizacionesRandom` and `Camara_noir`.
- Implemented `CanvasDisplay`, `ControlPanel`, `CaptureGrid`, `GridConfigPanel`, and `FrameCard` components for camera functionality.
- Integrated WebSocket for real-time image capture in `useCamaraNoir` hook.
- Developed FastAPI backend with endpoints for various chart data (bar, line, pie, scatter).
- Updated routing to include new analytics paths for visualizations.
- Modified submenu links to reflect new analytics options.
2025-05-22 01:45:57 +02:00
egutierrez 6b491a9a41 Implementación del cliente Ollama y su credencial, integración de logging en base de datos, y mejoras en la gestión de herramientas MCP. 2025-05-19 22:57:01 +02:00
132 changed files with 8555 additions and 2017 deletions
+1
View File
@@ -17,6 +17,7 @@ pruebas_conceptos/postgres_extensions/pgdata/*
*.env
config/.env
.continue
#Icon files
frontend/src/assets/icons/filled/** */
+6
View File
@@ -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'
-8
View File
@@ -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.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(
title="Fitz Backend",
@@ -21,4 +23,5 @@ app.add_middleware(
# Incluye las rutas de tu API
app.include_router(router)
app.include_router(router, prefix="/api/v1", tags=["v1"])
app.include_router(llm_chat_ws_endpoint_v1.router)
+15
View File
@@ -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"])
@@ -1,12 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import Path
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput
from backend.backend_domains.text_manager.text_manager_schema import BibliotecaInput, NotaInput
from fastapi.concurrency import run_in_threadpool
from backend.db.conexion import get_conexion
from backend.services.text_manager_srvc import *
from src.ConexionSql.Postgres_conexion import PostgresConexion
from backend.backend_domains.text_manager.text_manager_srvc import *
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
router = APIRouter()
@@ -1,53 +1,55 @@
from src.TextManager.biblioteca import Biblioteca
from src.TextManager.biblioteca_mmr import BibliotecaRepo
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from src.ConexionSql.Postgres_conexion import PostgresConexion
from src.TextManager.nota import Nota
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
from domains.TextManager.biblioteca import Biblioteca
from domains.TextManager.biblioteca_mmr import BibliotecaRepo
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from domains.TextManager.nota import Nota
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
from sqlalchemy import MetaData
from backend.schemas.text_manager_schema import NotaInput
from backend.backend_domains.text_manager.text_manager_schema import NotaInput
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
print("[INICIO] Creando biblioteca...")
logger.info("[INICIO] Creando biblioteca...")
try:
print("[Paso 1] Obteniendo credencial...")
logger.info("[Paso 1] Obteniendo credencial...")
cred_repo = OpenAICredencialRepo(conexion)
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
logger.debug(f"[OK] Credencial obtenida: {credencial.titulo if credencial else '❌ None'}")
print("[Paso 2] Instanciando embedder...")
logger.info("[Paso 2] Instanciando embedder...")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
print("[OK] Embedder instanciado")
logger.debug("[OK] Embedder instanciado")
print("[Paso 3] Instanciando biblioteca...")
logger.info("[Paso 3] Instanciando biblioteca...")
biblioteca = Biblioteca(
nombre=nombre_biblioteca,
embedder=embedder,
descripcion=descripcion
)
print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
logger.debug(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
print("[Paso 4] Guardando en base de datos...")
logger.info("[Paso 4] Guardando en base de datos...")
repo = BibliotecaRepo(conexion)
repo.add(biblioteca=biblioteca)
print("[OK] Biblioteca guardada")
logger.success("[OK] Biblioteca guardada")
print("[Paso 5] Generando modelo de notas...")
logger.info("[Paso 5] Generando modelo de notas...")
biblioteca.generar_modelo_notas(conexion)
print("[OK] Modelo de notas generado")
logger.success("[OK] Modelo de notas generado")
print("[FIN] Biblioteca creada correctamente")
logger.success("[FIN] Biblioteca creada correctamente")
return {
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
"id": biblioteca.id
}
except Exception as e:
print("[ERROR] Ocurrió una excepción:", str(e))
logger.exception("[ERROR] Ocurrió una excepción:")
raise
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
for b in bibliotecas
]
def agregar_nota_a_biblioteca(
conexion: PostgresConexion,
biblioteca_id: str,
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
conexiones: list[str] = None,
resumen: str = ""
):
# Obtener la biblioteca
repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if biblioteca is None:
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
# Crear objeto Nota
nota = Nota(
titulo=titulo,
texto=texto,
tags=tags or [],
conexiones=conexiones or [],
resumen=resumen or "",
# vector=biblioteca.embedder.embed_text(texto),
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
)
# Mostrar atributos seguros
print(
logger.debug(
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
f"texto_len={len(nota.texto)}, "
f"tags={len(nota.tags)}, "
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
f"resumen_len={len(nota.resumen)}"
)
# Preparar tabla y modelo de nota
metadata = MetaData()
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
biblioteca.nombre,
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
)
metadata.create_all(conexion.get_engine())
# Guardar la nota
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_id = repo_nota.add(nota)
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
"nota_id": nota_id
}
print(f"[SUCCESS] {resultado['mensaje']}")
logger.success(f"[SUCCESS] {resultado['mensaje']}")
return resultado
@@ -160,6 +155,7 @@ def eliminar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str)
fue_eliminada = repo_nota.delete_by_id(nota_id)
if fue_eliminada:
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
@@ -186,6 +182,7 @@ def actualizar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
if fue_actualizada:
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from domains.Usuario.usuario_mmr import UsuarioRepo, Usuario, UsuarioModel
from backend.db.conexion import get_conexion
router = APIRouter()
@router.post("/usuarios/", response_model=dict)
def crear_usuario(nombre: str, email: str, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuario = Usuario(id=None, nombre=nombre, email=email)
usuario_id = repo.add(usuario)
return {"id": usuario_id}
@router.get("/usuarios/{usuario_id}", response_model=dict)
def obtener_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuario = repo.get_by_id(usuario_id)
if not usuario:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return {"id": usuario.id, "nombre": usuario.nombre, "email": usuario.email, "activo": usuario.activo}
@router.get("/usuarios/", response_model=list)
def listar_usuarios(db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuarios = repo.get_all()
return [{"id": u.id, "nombre": u.nombre, "email": u.email, "activo": u.activo} for u in usuarios]
@router.delete("/usuarios/{usuario_id}", response_model=dict)
def eliminar_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
exito = repo.delete_by_id(usuario_id)
if not exito:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return {"ok": True}
+1 -1
View File
@@ -1,6 +1,6 @@
# backend/db/conexion.py
from entrypoint.init_db import db_credencial
from src.ConexionSql.Postgres_conexion import PostgresConexion
from domains.ConexionSql.Postgres_conexion import PostgresConexion
def get_conexion():
conexion = PostgresConexion(db_credencial)
+26 -1016
View File
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
class OpenAICredencial:
def __init__(self, titulo: str, api_key: str, organizacion: str = None, id: str = None):
@@ -3,17 +3,17 @@ import base64
from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.ApiKeys.openai_apikey import OpenAICredencial
from src.Security.Encriptar import Encriptar_fernet
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.ApiKeys.openai_apikey import OpenAICredencial
from domains.Security.Encriptar import Encriptar_fernet
from entrypoint import ENV_PATH
from src.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from sqlalchemy import Column, String
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
# ----------------------
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
from src.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
from domains.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
TModelo = TypeVar("TModelo")
TDominio = TypeVar("TDominio")
+62
View File
@@ -0,0 +1,62 @@
import requests
from domains.Credenciales.ollama_credencial import OllamaCredencial
class OllamaCliente:
def __init__(self, credencial: OllamaCredencial):
"""
Inicializa el cliente de Ollama con una instancia de OllamaCredencial.
"""
self.credencial = credencial
self.base_url = self.credencial.base_url
# --- Chat Completions ---
def chat_completion(self, model: str, messages: list, stream: bool = False, **kwargs):
url = f"{self.base_url}/api/chat"
payload = {
"model": model,
"messages": messages,
"stream": stream,
**kwargs
}
response = requests.post(url, json=payload, stream=stream)
response.raise_for_status()
return self._handle_stream(response) if stream else response.json()
def _handle_stream(self, response):
for line in response.iter_lines():
if line:
try:
parsed = line.decode("utf-8")
# Extraer contenido si está en JSON como {"message":{"content":"..."},...}
if parsed.startswith("{"):
import json
data = json.loads(parsed)
if "message" in data and "content" in data["message"]:
yield data["message"]["content"]
except Exception:
continue
# --- Text Completion (legacy) ---
def completion(self, model: str, prompt: str, **kwargs):
url = f"{self.base_url}/api/generate"
payload = {
"model": model,
"prompt": prompt,
**kwargs
}
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()
# --- Embeddings ---
def embedding(self, model: str, input: str | list[str], **kwargs):
url = f"{self.base_url}/api/embeddings"
payload = {
"model": model,
"prompt": input,
**kwargs
}
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()
@@ -1,5 +1,5 @@
from openai import OpenAI
from src.ApiKeys.openai_apikey import OpenAICredencial
from domains.ApiKeys.openai_apikey import OpenAICredencial
class OpenAICliente:
def __init__(self, credencial: OpenAICredencial):
@@ -4,8 +4,8 @@ from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.engine import Engine
from src.ConexionSql.Base_conexion import ConexionBase
from src.Credenciales.postgres_credencial import PostgresCredencial
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.Credenciales.postgres_credencial import PostgresCredencial
class PostgresConexion(ConexionBase):
def __init__(self, *args, **kwargs):
+20
View File
@@ -0,0 +1,20 @@
from domains.Security.GenerarIDs import GeneradorIDUnico
class OllamaCredencial:
def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None):
"""
:param titulo: Nombre descriptivo para esta credencial de Ollama.
:param base_url: URL base donde está corriendo el servidor de Ollama (por defecto: localhost).
"""
self.id = id if id is not None else GeneradorIDUnico("OLLA").generar()
self.titulo = titulo
self.base_url = base_url.rstrip("/")
def get_headers(self) -> dict:
"""
Retorna encabezados para autenticación si se requiere en el futuro.
Por defecto, Ollama local no usa headers especiales.
"""
return {
"Content-Type": "application/json"
}
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
class PostgresCredencial:
def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str, id: str = None):
@@ -6,14 +6,14 @@ from sqlalchemy import DateTime, Text, func
import base64
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Credenciales.postgres_credencial import PostgresCredencial
from src.Security.Encriptar import Encriptar_fernet
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Credenciales.postgres_credencial import PostgresCredencial
from domains.Security.Encriptar import Encriptar_fernet
# ----------------------
# Cargar clave maestra
+443
View File
@@ -0,0 +1,443 @@
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
from datetime import datetime
from typing import Optional, List, Union, AsyncGenerator
import re
import json
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
class AgenteAI:
def __init__(
self,
modelo: ModeloABC,
nombre: str,
descripcion: str,
system_prompt: str,
rol: str,
objetivos: List[str],
max_iterations: int = 1,
memoria: Optional[MemoryConvABC] = None,
version: str = "1.0.0",
mcp: Optional[ClientRegistry] = None,
output_schema: Optional[dict] = None,
):
self.modelo = modelo
self.memoria = memoria
self.output_schema = output_schema
self.nombre = nombre
self.descripcion = descripcion
self.system_prompt = system_prompt
self.max_iterations = max_iterations
self.rol = rol
self.objetivos = objetivos
self.version = version
self.created_at = datetime.now()
self.updated_at = self.created_at
self.numero_interacciones = 0
self.mcp = mcp # <-- Aquí guardamos el registry
def actualizar_configuracion(self, **kwargs):
for clave, valor in kwargs.items():
if hasattr(self, clave):
setattr(self, clave, valor)
self.updated_at = datetime.now()
async def generar_system_prompt(self) -> str:
info = f"""Eres un agente de texto y te llamas {self.nombre}
### Descripción:
{self.descripcion}
### Rol:
{self.rol}
### Objetivos:
{chr(10).join(f"- {o}" for o in self.objetivos)}
### System Prompt:
{self.system_prompt}
Siempre estructura tus respuestas con claridad, y termina con <END> cuando hayas completado la tarea principal del usuario.
""".strip()
return info
async def construir_prompt_usuario(self, prompt_usuario: str) -> str:
bloques = []
if self.mcp:
tools_str = await self._obtener_herramientas_disponibles_str()
bloques.append(f"### Herramientas disponibles (MCP):\n{tools_str}")
bloques.append("""### Instrucciones para actuar con herramientas MCP:
Eres un agente conversacional con acceso a herramientas MCP. Cuando el usuario te haga una solicitud, sigue este proceso paso a paso:
---
🧠 **Piensa**:
Reflexiona en voz alta. Explica claramente qué crees que se necesita hacer y por qué.
🎯 **Decide**:
Elige si puedes resolverlo tú solo, si necesitas más información del usuario, o si una herramienta MCP sería útil.
⚙️ **Actúa**:
Si decides usar una herramienta, **escribe el bloque MCP justo después**, sin ningún texto extra después del bloque.
---
### Formato MCP:
```mcp
{
"server": "tools",
"tool": "get_current_user",
"input": {}
}
```
---
### ❗ REGLAS IMPORTANTES:
- **Puedes pensar y decidir con texto normal**, pero:
- El **bloque MCP debe ser lo último** que aparece en tu mensaje.
- **NO escribas nada después del bloque MCP.**
- Solo usa `<END>` cuando:
- hayas terminado completamente la tarea del usuario,
- e interpretado la salida de las herramientas que usaste.
- Puedes hacer múltiples pasos si es necesario: usar una herramienta, esperar su salida, analizarla, usar otra, etc.
- Si decides no usar herramientas, simplemente responde como lo harías normalmente.
- Si no estás seguro de algo, **pide aclaraciones al usuario** antes de actuar.
---
✅ Correcto:
```mcp
{
"server": "tools",
"tool": "generate_uuid",
"input": {}
}
````
🔵 Siempre usa ` ```mcp ` (con triple backtick y la palabra `mcp`) antes del JSON. No escribas nada después del bloque.
````
---
### ✅ Ejemplo correcto:
Necesito generar un identificador único para el usuario.
Para eso usaré la herramienta `generate_uuid` disponible.
```mcp
{
"server": "tools",
"tool": "generate_uuid",
"input": {}
}
""")
if self.memoria:
historial = self.memoria.cargar_historial_chat()
if historial:
memoria_str = "\n".join(
[f"{msg['role']}: {msg['content']}" for msg in historial]
)
bloques.append(f"### Memoria del chat:\n{memoria_str}")
if self.output_schema:
schema_str = str(self.output_schema)
bloques.append(f"### Salida esperada:\n{schema_str}")
bloques.append(f"### Prompt del usuario:\n{prompt_usuario}")
return "\n\n".join(bloques)
### Conseguir las herramientas disponibles
async def _obtener_herramientas_disponibles_str(self) -> str:
logger.info("Inicio de obtención de herramientas disponibles")
if not self.mcp:
logger.warning("No se ha definido el cliente MCP.")
return "No se han definido herramientas disponibles."
try:
resultado = await self.mcp.listar_tools_por_cliente()
tools_por_cliente = resultado.get("tools", {})
errores = resultado.get("errores", {})
logger.debug(f"Tools obtenidas: {list(tools_por_cliente.keys())}")
logger.debug(f"Errores detectados: {list(errores.keys())}")
herramientas = []
for name, tools in tools_por_cliente.items():
if not tools:
logger.info(f"Servidor {name} no tiene herramientas disponibles.")
continue
herramientas.append(f"\n🔌 Server: {name}")
for tool in tools:
props = tool.inputSchema.get("properties", {})
parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items())
herramientas.append(f"""Nombre: {tool.name}
Descripción: {tool.description}
Parámetros:
{parametros}
""")
logger.debug(f"Herramienta agregada: {tool.name} del servidor {name}")
if errores:
herramientas.append("\n⚠️ Los siguientes servidores no están disponibles:")
for name, error in errores.items():
herramientas.append(f"- {name}: {error}")
logger.warning(f"Servidor con error: {name} -> {error}")
logger.info("Finalización de obtención de herramientas exitosamente.")
return "\n".join(herramientas) or "No hay herramientas disponibles actualmente."
except Exception as e:
logger.error(f"Error inesperado al obtener herramientas: {str(e)}", exc_info=True)
return "Se produjo un error al obtener las herramientas disponibles."
### Formatear prompt para agentes
def _formatear_prompt(self, mensajes: List[dict]) -> str:
return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes])
### Ejecutar codigo MCP
async def ejecutar_bloque_mcp(self, respuesta: str) -> Optional[str]:
logger.info("Iniciando ejecución de bloque MCP.")
patron = r"```mcp\s*(\{.*?\})\s*```"
match = re.search(patron, respuesta, re.DOTALL)
if not match:
patron_incorrecto = r"```[\s]*\{.*?\}[\s]*```"
if re.search(patron_incorrecto, respuesta, re.DOTALL):
logger.warning("Bloque detectado sin especificador `mcp`.")
return "Advertencia: Usaste un bloque de herramienta MCP pero olvidaste indicar el lenguaje `mcp`. Corrige el bloque a: ```mcp { ... } ```"
logger.info("No se encontró ningún bloque MCP en la respuesta.")
return None
try:
bloque_json_str = match.group(1)
logger.debug(f"Bloque MCP detectado: {bloque_json_str}")
bloque = json.loads(bloque_json_str)
server_name = bloque["server"]
tool_name = bloque["tool"]
input_args = bloque.get("input", {})
logger.info(f"Bloque MCP válido. Servidor: {server_name}, Herramienta: {tool_name}")
logger.debug(f"Parámetros de entrada: {input_args}")
except Exception as e:
logger.error(f"Error al interpretar el bloque MCP: {e}", exc_info=True)
return f"Error al interpretar el bloque MCP: {e}"
try:
cliente_mcp = self.mcp.get(server_name)
except KeyError:
logger.warning(f"No se encontró el cliente MCP para el servidor '{server_name}'.")
return f"No se encontró el cliente MCP para el servidor '{server_name}'"
try:
logger.info(f"Ejecutando herramienta '{tool_name}' en servidor '{server_name}' con argumentos: {json.dumps(input_args, ensure_ascii=False)}")
async with cliente_mcp:
resultado = await cliente_mcp.call_tool(tool_name, input_args)
logger.info(f"Ejecución completada exitosamente. Resultado: {resultado}")
return str(resultado)
except Exception as e:
logger.error(f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}", exc_info=True)
return f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}"
### Ejecutar VARIOS bloques MCP
async def ejecutar_multiples_bloques_mcp(self, respuesta: str) -> Optional[List[str]]:
logger.info("Buscando múltiples bloques MCP en la respuesta.")
patron = r"```mcp\s*(\{.*?\})\s*```"
matches = re.finditer(patron, respuesta, re.DOTALL)
resultados = []
hubo_bloques = False
for match in matches:
hubo_bloques = True
bloque_json_str = match.group(1)
try:
bloque = json.loads(bloque_json_str)
server_name = bloque["server"]
tool_name = bloque["tool"]
input_args = bloque.get("input", {})
logger.info(f"Ejecutando bloque MCP: servidor={server_name}, herramienta={tool_name}")
try:
cliente_mcp = self.mcp.get(server_name)
except KeyError:
msg = f"No se encontró el cliente MCP para el servidor '{server_name}'"
logger.warning(msg)
resultados.append(msg)
continue
async with cliente_mcp:
resultado = await cliente_mcp.call_tool(tool_name, input_args)
resultado_str = f"[{server_name}.{tool_name}] → {resultado}"
resultados.append(resultado_str)
except Exception as e:
error_msg = f"Error al procesar bloque MCP: {str(e)}"
logger.error(error_msg, exc_info=True)
resultados.append(error_msg)
if not hubo_bloques:
logger.info("No se encontró ningún bloque MCP en la respuesta.")
return None
return resultados
###----------- Funcion para interactuar
async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]:
mensaje_usuario = await self.construir_prompt_usuario(prompt)
contexto = [{"role": "user", "content": mensaje_usuario}]
prompt_final = self._formatear_prompt(contexto)
respuesta = await self.modelo.responder(
prompt=prompt_final,
system_prompt=await self.generar_system_prompt(),
stream=stream
)
return respuesta
###----------- Funcion para interactuar en bucle
async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]:
respuestas = [] if not stream else None
respuesta_anterior = ""
resultado_mcp_anterior = None # <-- Guarda último resultado del MCP
iteration = 0
prompt_original = prompt.strip()
async def generador():
nonlocal iteration, respuesta_anterior, resultado_mcp_anterior
while self.max_iterations == 0 or iteration < self.max_iterations:
instruccion_fin = (
"\n\nIMPORTANTE: Cuando hayas respondido completamente a la pregunta original del usuario y no requieras más pasos, "
"escribe <END> para indicar que has terminado."
)
if iteration == 0:
prompt_actual = prompt_original + instruccion_fin
else:
prompt_actual = (
f"Esta es la pregunta original:\n{prompt_original}\n\n"
f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n\n"
f"{instruccion_fin}"
)
if resultado_mcp_anterior:
prompt_actual += (
"\n\nEsta fue la salida de la herramienta que usaste:\n"
f"{resultado_mcp_anterior}\n\n"
"Úsala para seguir resolviendo el problema o tomar una nueva decisión."
)
mensaje_usuario = await self.construir_prompt_usuario(prompt_actual)
contexto = [{"role": "user", "content": mensaje_usuario}]
prompt_final = self._formatear_prompt(contexto)
respuesta = await self.modelo.responder(
prompt=prompt_final,
system_prompt=await self.generar_system_prompt(),
stream=stream
)
if stream:
buffer_respuesta = ""
async for token in respuesta:
buffer_respuesta += token
yield token
respuesta_anterior = buffer_respuesta
else:
respuestas.append(respuesta)
respuesta_anterior = respuesta
# Revisar y ejecutar bloque MCP si existe
resultado_mcp_anterior = None
if "```mcp" in respuesta_anterior:
resultados_mcp = await self.ejecutar_multiples_bloques_mcp(respuesta_anterior)
if resultados_mcp:
resultado_mcp_anterior = "\n".join(resultados_mcp)
if stream:
yield "\n" + resultado_mcp_anterior
else:
respuestas.append(resultado_mcp_anterior)
# Guardar historial si hay memoria
if self.memoria:
self.memoria.guardar_turno("user", prompt_actual)
self.memoria.guardar_turno("assistant", respuesta_anterior)
self.numero_interacciones += 1
self.updated_at = datetime.now()
if "<end>" in respuesta_anterior.lower() and "```mcp" not in respuesta_anterior.lower():
break
iteration += 1
return generador() if stream else await generador_to_list(generador())
# Helper para consumir generador asincrónico si no es stream
async def generador_to_list(gen: AsyncGenerator[str, None]) -> List[str]:
buffer = ""
async for chunk in gen:
buffer += chunk
return [buffer]
@@ -1,8 +1,8 @@
from typing import List
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
from src.ApiKeys.openai_apikey import OpenAICredencial
from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
from domains.ApiKeys.openai_apikey import OpenAICredencial
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
from domains.Security.GenerarIDs import GeneradorIDUnico
class OpenAIEmbedder(EmbedderABC):
def __init__(self, credencial: OpenAICredencial,
@@ -3,15 +3,15 @@ from dotenv import load_dotenv
from sqlalchemy import Column, String
from sqlalchemy import Column, String, ForeignKey
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey import OpenAICredencial
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from domains.ApiKeys.openai_apikey import OpenAICredencial
# ----------------------
# Cargar configuración desde .env si se requiere
@@ -9,6 +9,7 @@ from fastmcp.client.transports import (
)
from mcp.types import *
from fastmcp.exceptions import ClientError
import asyncio
class MCPClient:
@@ -52,10 +53,13 @@ class MCPClient:
# Delegación MCP
async def call_tool(
self, name: str, arguments: dict[str, Any] | None = None
) -> list[TextContent | ImageContent | EmbeddedResource]:
return await self.client.call_tool(name, arguments)
async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[TextContent | ImageContent | EmbeddedResource]:
try:
return await asyncio.wait_for(
self.client.call_tool(name, arguments), timeout=10
)
except asyncio.TimeoutError:
raise RuntimeError(f"Timeout al ejecutar herramienta '{name}'")
async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None
@@ -1,4 +1,4 @@
from src.Llms.MCPs.McpClient import MCPClient
from domains.Llms.MCPs.McpClient import MCPClient
from typing import Any
class ClientRegistry:
@@ -22,35 +22,35 @@ class ClientRegistry:
def __contains__(self, name: str) -> bool:
return name in self._clients
async def listar_tools_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
async def listar_tools_por_cliente(self) -> dict[str, Any]:
resultado = {"tools": {}, "errores": {}}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_tools()
resultado["tools"][name] = await wrapper.list_tools()
except Exception as e:
print(f"[TOOLS] ❌ Error en '{name}': {e}")
resultado[name] = []
resultado["errores"][name] = str(e)
resultado["tools"][name] = []
return resultado
async def listar_prompts_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
async def listar_prompts_por_cliente(self) -> dict[str, Any]:
resultado = {"prompts": {}, "errores": {}}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_prompts()
resultado["prompts"][name] = await wrapper.list_prompts()
except Exception as e:
print(f"[PROMPTS] ❌ Error en '{name}': {e}")
resultado[name] = []
resultado["errores"][name] = str(e)
resultado["prompts"][name] = []
return resultado
async def listar_resources_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
async def listar_resources_por_cliente(self) -> dict[str, Any]:
resultado = {"resources": {}, "errores": {}}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_resources()
resultado["resources"][name] = await wrapper.list_resources()
except Exception as e:
print(f"[RESOURCES] ❌ Error en '{name}': {e}")
resultado[name] = []
resultado["errores"][name] = str(e)
resultado["resources"][name] = []
return resultado
+48
View File
@@ -0,0 +1,48 @@
# server_runner.py
import subprocess
import asyncio
import socket
import re
from pathlib import Path
async def wait_for_port(host: str, port: int, timeout: float = 10.0):
for _ in range(int(timeout * 10)):
try:
with socket.create_connection((host, port), timeout=0.5):
return True
except (OSError, ConnectionRefusedError):
await asyncio.sleep(0.1)
raise TimeoutError(f"No se pudo conectar al servidor en {host}:{port}")
class MCPServerRunner:
def __init__(self, server_script_path: str, python_path: str = "python"):
self.server_script_path = server_script_path
self.python_path = python_path
self.port: int = self._extraer_puerto()
self.process: subprocess.Popen | None = None
def _extraer_puerto(self) -> int:
contenido = Path(self.server_script_path).read_text(encoding="utf-8")
coincidencias = re.findall(r"port\s*=\s*(\d+)", contenido)
if not coincidencias:
raise ValueError(f"No se pudo detectar el puerto en {self.server_script_path}")
return int(coincidencias[0])
async def start(self):
if self.process is None or self.process.poll() is not None:
self.process = subprocess.Popen(
[self.python_path, self.server_script_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
await wait_for_port("127.0.0.1", self.port)
print(f"🟢 Servidor MCP iniciado en puerto {self.port}")
async def stop(self):
if self.process and self.process.poll() is None:
self.process.terminate()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
print("🔴 Servidor MCP detenido")
@@ -0,0 +1,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")
@@ -0,0 +1,133 @@
from fastmcp import FastMCP
from pathlib import Path
import shutil
from datetime import datetime
# Directorio base seguro
SANDBOX_DIR = Path("./sandbox").resolve()
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)
def safe_path(requested_path: str) -> Path:
"""Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
# Normaliza la ruta quitando el primer '/'
normalized = requested_path.strip().lstrip("/")
full_path = (SANDBOX_DIR / normalized).resolve()
if not full_path.is_relative_to(SANDBOX_DIR):
raise ValueError("Ruta fuera del directorio permitido.")
return full_path
mcp = FastMCP()
@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
def read_file(path: str) -> str:
try:
file_path = safe_path(path)
if not file_path.is_file():
raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
return file_path.read_text(encoding="utf-8")
except Exception as e:
return f"⚠️ Error al leer archivo '{path}': {str(e)}"
@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
def write_file(path: str, content: str) -> str:
file_path = safe_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return "Archivo guardado correctamente."
@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
def delete_file(path: str) -> str:
file_path = safe_path(path)
if not file_path.is_file():
raise FileNotFoundError("Archivo no encontrado.")
file_path.unlink()
return "Archivo eliminado."
@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
def create_folder(path: str) -> str:
folder_path = safe_path(path)
folder_path.mkdir(parents=True, exist_ok=True)
return "Carpeta creada."
@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
def list_directory(path: str = ".") -> list[str]:
folder = safe_path(path)
if not folder.is_dir():
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())
@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
def tree(path: str = ".", depth: int = 3) -> str:
base = safe_path(path)
if not base.is_dir():
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
tree_output = []
def walk(dir_path: Path, prefix: str = "", level: int = 0):
if level > depth:
return
entries = sorted(dir_path.iterdir())
for i, entry in enumerate(entries):
connector = "└── " if i == len(entries) - 1 else "├── "
tree_output.append(f"{prefix}{connector}{entry.name}")
if entry.is_dir():
extension = " " if i == len(entries) - 1 else ""
walk(entry, prefix + extension, level + 1)
tree_output.append(f"{base.name}/")
walk(base)
return "\n".join(tree_output)
@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
def file_info(path: str) -> dict:
fpath = safe_path(path)
if not fpath.exists():
raise FileNotFoundError("Archivo no encontrado.")
return {
"nombre": fpath.name,
"tipo": "carpeta" if fpath.is_dir() else "archivo",
"tamaño_bytes": fpath.stat().st_size,
"última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
"relativo": str(fpath.relative_to(SANDBOX_DIR))
}
@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
def copy_file(src: str, dest: str) -> str:
src_path = safe_path(src)
dest_path = safe_path(dest)
if src_path.is_dir():
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
else:
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest_path)
return "Copia completada."
@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
def move_file(src: str, dest: str) -> str:
src_path = safe_path(src)
dest_path = safe_path(dest)
dest_path.parent.mkdir(parents=True, exist_ok=True)
src_path.rename(dest_path)
return "Movimiento completado."
@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
def clear_folder(path: str) -> str:
folder_path = safe_path(path)
if not folder_path.is_dir():
raise NotADirectoryError("La ruta no es una carpeta.")
for item in folder_path.iterdir():
if item.is_file() or item.is_symlink():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
return "Carpeta vaciada."
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/")
@@ -87,6 +87,6 @@ def is_prime(n: int) -> bool:
if __name__ == "__main__":
# mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/")
mcp.run(transport="stdio")
# mcp.run(transport="stdio")
@@ -65,4 +65,5 @@ def current_timestamp() -> float:
return datetime.datetime.now().timestamp()
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 typing import Literal
from src.Credenciales.postgres_credencial import PostgresCredencial
from src.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
from domains.Credenciales.postgres_credencial import PostgresCredencial
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
class MemoryConvPostgres(MemoryConvABC):
+67
View File
@@ -0,0 +1,67 @@
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.Security.GenerarIDs import GeneradorIDUnico
from typing import AsyncGenerator, Union
from domains.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
import asyncio
class ModeloOllama(ModeloABC):
def __init__(
self,
cliente: OllamaCliente,
model: str = "llama3",
id: str = None,
temperature: float = 0.7,
top_p: float = 1.0,
top_k: int = None,
frecuencia_penalizacion: float = 0.0,
num_tokens_maximos: int = 512
):
if not isinstance(cliente, OllamaCliente):
raise TypeError("El parámetro 'cliente' debe ser una instancia de OllamaCliente")
self.id = id if id else GeneradorIDUnico("MOOL").generar()
super().__init__(
model=model,
temperature=temperature,
top_p=top_p,
top_k=top_k,
frecuencia_penalizacion=frecuencia_penalizacion,
num_tokens_maximos=num_tokens_maximos
)
self.cliente = cliente
async def responder(
self,
prompt: str,
system_prompt: str = "",
stream: bool = False,
**kwargs
) -> Union[str, AsyncGenerator[str, None]]:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
def sync_call():
return self.cliente.chat_completion(
model=self.model,
messages=messages,
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.num_tokens_maximos,
frequency_penalty=self.frecuencia_penalizacion,
stream=stream,
**kwargs
)
loop = asyncio.get_event_loop()
resultado = await loop.run_in_executor(None, sync_call)
if stream:
async def generador():
for token in resultado:
yield token
return generador()
else:
return resultado.choices[0].message.content
@@ -1,6 +1,6 @@
from src.Llms.Modelos.Base_model import ModeloABC
from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
from domains.Security.GenerarIDs import GeneradorIDUnico
import asyncio
from typing import AsyncGenerator, Union
@@ -2,15 +2,15 @@ import os
from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String, Float, Boolean
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from typing import Optional
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
# ----------------------
# Cargar clave maestra
+62
View File
@@ -0,0 +1,62 @@
from loguru import logger
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from domains.ArquitectureLayer.Model import Model_base
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from domains.Credenciales.postgres_credencial import PostgresCredencial
class LoggerDB:
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
# 🔥 Elimina todos los sinks activos, incluso los automáticos
logger.remove()
self.conexion = PostgresConexion(credencial)
self.engine = self.conexion.get_engine()
self.Session = sessionmaker(bind=self.engine)
self.nombre_tabla = nombre_tabla
self.created_by = created_by
self.modelo_logger = self._generar_modelo_logger()
self._crear_tabla_si_no_existe()
logger.add(self._sink, level="DEBUG")
def _generar_modelo_logger(self):
class LoggerTable(Model_base):
__tablename__ = self.nombre_tabla
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
id = Column(Integer, primary_key=True)
nivel = Column(String, nullable=False)
mensaje = Column(Text, nullable=False)
fecha = Column(TIMESTAMP(timezone=True), nullable=False)
modulo = Column(String, nullable=True)
funcion = Column(String, nullable=True)
linea = Column(Integer, nullable=True)
return LoggerTable
def _crear_tabla_si_no_existe(self):
self.modelo_logger.__table__.create(self.engine, checkfirst=True)
def _sink(self, message):
record = message.record
try:
session = self.Session()
log_entry = self.modelo_logger(
nivel=record["level"].name,
mensaje=record["message"],
fecha=record["time"],
modulo=record["module"],
funcion=record["function"],
linea=record["line"],
sys_created_by=self.created_by
)
session.add(log_entry)
session.commit()
session.close()
except SQLAlchemyError as e:
print(f"[LoggerDB] Error guardando log en BD: {e}")
+190
View File
@@ -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
+193
View File
@@ -0,0 +1,193 @@
import asyncio
import os
import signal
import subprocess
import json
from typing import Optional
import aiohttp
class Navegador:
def __init__(self,
chrome_path: str,
user_data_dir: str,
id: Optional[int] = None,
download_dir: Optional[str] = None,
debugging_port: int = 9222,
headless: bool = False,
user_agent: Optional[str] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"):
self.chrome_path = chrome_path
self.user_data_dir = user_data_dir
self.id = id
self.download_dir = download_dir or os.path.join(self.user_data_dir, "downloads")
self.debugging_port = debugging_port
self.headless = headless
self.user_agent = user_agent
self.chrome_process: Optional[subprocess.Popen] = None
async def _esperar_debugger(self, timeout=10):
url = f"http://127.0.0.1:{self.debugging_port}/json"
for _ in range(timeout * 10): # 10 intentos por segundo
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
print("✅ Chrome listo para debugging.")
return
except Exception:
pass
await asyncio.sleep(0.1)
raise RuntimeError("❌ Chrome no respondió en el puerto de debugging.")
def _preconfigurar_preferencias(self):
prefs_path = os.path.join(self.user_data_dir, "Default", "Preferences")
os.makedirs(os.path.dirname(prefs_path), exist_ok=True)
os.makedirs(self.download_dir, exist_ok=True)
prefs = {
"profile": {
"exit_type": "Normal",
"exited_cleanly": True
},
"browser": {
"has_seen_welcome_page": True
},
"distribution": {
"skip_first_run_ui": True
},
"download": {
"default_directory": self.download_dir,
"prompt_for_download": False,
"directory_upgrade": True,
"extensions_to_open": ""
},
"savefile": {
"default_directory": self.download_dir
}
}
if os.path.exists(prefs_path):
try:
with open(prefs_path, "r", encoding="utf-8") as f:
existing = json.load(f)
existing.update(prefs)
prefs = existing
except Exception:
pass
with open(prefs_path, "w", encoding="utf-8") as f:
json.dump(prefs, f, indent=2)
def _build_args(self):
os.makedirs(self.user_data_dir, exist_ok=True)
self._preconfigurar_preferencias()
args = [
f"--remote-debugging-port={self.debugging_port}",
f"--user-data-dir={self.user_data_dir}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
# "--disable-web-security",
# "--disable-extensions",
# "--disable-dev-shm-usage",
"--disable-infobars",
"--disable-popup-blocking",
"--disable-default-apps",
"--mute-audio",
"--window-size=1024,1024",
"--no-first-run",
"--no-default-browser-check",
"--disable-features=DefaultBrowserPrompt",
"--disable-component-update",
"--disable-background-networking",
"--disable-sync",
"--disable-translate",
"--disable-background-timer-throttling",
"--disable-client-side-phishing-detection",
"--disable-component-extensions-with-background-pages",
"--metrics-recording-only",
"--safebrowsing-disable-auto-update",
]
if self.headless:
args.append("--headless=new")
if self.user_agent:
args.append(f"--user-agent={self.user_agent}")
return args
async def inyectar_spoof_chrome(self):
script = """
window.chrome = {
app: {
isInstalled: false,
InstallState: {
DISABLED: 'disabled',
INSTALLED: 'installed',
NOT_INSTALLED: 'not_installed'
},
RunningState: {
CANNOT_RUN: 'cannot_run',
READY_TO_RUN: 'ready_to_run',
RUNNING: 'running'
}
},
runtime: {
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' }
}
};
"""
url = f"http://127.0.0.1:{self.debugging_port}/json"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
targets = await resp.json()
for target in targets:
if "webSocketDebuggerUrl" not in target:
continue
target_id = target["id"]
async with session.post(
f"http://127.0.0.1:{self.debugging_port}/json/protocol",
json={"targetId": target_id}
):
pass # CDP protocol fetch optional
async with session.post(
f"http://127.0.0.1:{self.debugging_port}/json/send",
json={
"id": 1,
"method": "Page.addScriptToEvaluateOnNewDocument",
"params": {"source": script}
}
) as inject_resp:
if inject_resp.status == 200:
print("✅ chrome.* spoof inyectado.")
async def iniciar(self):
args = self._build_args()
self.chrome_process = subprocess.Popen([self.chrome_path] + args)
print(f"Chrome iniciado (headless={self.headless}). Esperando disponibilidad del debugger...")
await self._esperar_debugger()
await self.inyectar_spoof_chrome()
async def cerrar(self):
if self.chrome_process and self.chrome_process.poll() is None:
self.chrome_process.terminate()
try:
await asyncio.wait_for(asyncio.to_thread(self.chrome_process.wait), timeout=5)
except asyncio.TimeoutError:
self.chrome_process.kill()
print("🛑 Chrome cerrado correctamente.")
+138
View File
@@ -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
+206
View File
@@ -0,0 +1,206 @@
import asyncio
import json
import base64
import websockets
from typing import Optional, List
from .ElementoWeb import ElementoWeb
import os
class Tab:
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
self.websocket = websocket
self.ws_url = ws_url
self._message_id = 0
self._pending = {}
self._load_event = asyncio.Event()
self.verbose = verbose
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self.cerrar()
@classmethod
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
tab = cls(websocket, ws_url)
asyncio.create_task(tab._recibir_eventos())
await tab._enviar("Page.enable")
await tab._enviar("Network.enable")
return tab
async def _recibir_eventos(self):
async for mensaje in self.websocket:
data = json.loads(mensaje)
if "id" in data and data["id"] in self._pending:
future = self._pending.pop(data["id"])
if "result" in data:
future.set_result(data["result"])
elif "error" in data:
future.set_exception(Exception(data["error"]))
elif data.get("method") == "Page.loadEventFired":
self._load_event.set()
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
self._message_id += 1
msg_id = self._message_id
mensaje = {
"id": msg_id,
"method": metodo,
"params": parametros or {}
}
future = asyncio.get_event_loop().create_future()
self._pending[msg_id] = future
await self.websocket.send(json.dumps(mensaje))
return await asyncio.wait_for(future, timeout=timeout)
async def navegar(self, url: str, wait_time: float = 5.0):
self._load_event.clear()
if self.verbose:
print(f"🌍 Navegando a: {url}")
await self._enviar("Page.navigate", {"url": url})
try:
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
if self.verbose:
print("✅ Página cargada correctamente.")
except asyncio.TimeoutError:
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
async def evaluar_js(self, js_code: str) -> Optional[str]:
try:
result = await self._enviar("Runtime.evaluate", {
"expression": js_code,
"returnByValue": True
})
if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al ejecutar JS: {e}")
return None
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
if not os.path.exists(ruta_archivo):
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
return None
with open(ruta_archivo, "r", encoding="utf-8") as f:
js_code = f.read()
if reemplazos:
for key, value in reemplazos.items():
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
# 🔧 Eliminamos el `return` externo
js_code_final = f"(async () => {{\n{js_code}\n}})();"
try:
result = await self._enviar("Runtime.evaluate", {
"expression": js_code_final,
"returnByValue": True
})
if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
return None
async def obtener_user_agent(self) -> Optional[str]:
return await self.evaluar_js("navigator.userAgent")
async def capturar_screenshot(self, output_path: str = "screenshot.png"):
try:
result = await self._enviar("Page.captureScreenshot")
data = result["data"]
with open(output_path, "wb") as f:
f.write(base64.b64decode(data))
if self.verbose:
print(f"📸 Screenshot guardado como {output_path}")
except Exception as e:
print(f"⚠️ Error al capturar screenshot: {e}")
async def cerrar(self):
try:
if not self.websocket.closed:
await self.websocket.close()
if self.verbose:
print("🛑 WebSocket cerrado.")
except Exception as e:
print(f"⚠️ Error al cerrar pestaña: {e}")
async def obtener_html_completo(self) -> Optional[str]:
try:
result = await self._enviar("Runtime.evaluate", {
"expression": "document.documentElement.outerHTML",
"returnByValue": True
})
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al obtener HTML: {e}")
return None
async def obtener_dominio(self) -> Optional[str]:
try:
dominio = await self.evaluar_js("window.location.hostname")
if self.verbose and dominio:
print(f"🌐 Dominio actual: {dominio}")
return dominio
except Exception as e:
print(f"⚠️ Error al obtener dominio: {e}")
return None
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
try:
doc = await self._enviar("DOM.getDocument")
root_node_id = doc["root"]["nodeId"]
result = await self._enviar("DOM.querySelector", {
"nodeId": root_node_id,
"selector": selector
})
node_id = result.get("nodeId")
if not node_id:
print(f"⚠️ Nodo no encontrado con selector: {selector}")
return None
return ElementoWeb.from_node(self, node_id=node_id)
except Exception as e:
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
return None
async def get_elements_by_css_selector(self, selector: str) -> List["ElementoWeb"]:
try:
result = await self._enviar("Runtime.evaluate", {
"expression": f'Array.from(document.querySelectorAll("{selector}"))',
"objectGroup": "grupo_elementos_css",
"includeCommandLineAPI": True,
"returnByValue": False
})
array_id = result["result"]["objectId"]
props = await self._enviar("Runtime.getProperties", {
"objectId": array_id,
"ownProperties": True
})
elementos = []
for prop in props["result"]:
if "value" in prop and "objectId" in prop["value"]:
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
if self.verbose:
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
return elementos
except Exception as e:
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
return []
async def enfocar(self):
try:
await self._enviar("Page.bringToFront")
if self.verbose:
print("🪟 Pestaña enfocada (bringToFront).")
except Exception as e:
print(f"⚠️ Error al enfocar pestaña: {e}")
@@ -1,11 +1,11 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
from typing import List, Optional
from src.ConexionSql.Base_conexion import ConexionBase
from domains.ConexionSql.Base_conexion import ConexionBase
from sqlalchemy import MetaData # Asegúrate de importar esto
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from sqlalchemy import inspect
from src.base import Base
from domains.base import Base
class Biblioteca:
@@ -3,16 +3,16 @@ import base64
from dotenv import load_dotenv
from sqlalchemy import Column, String, Integer
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Security.Encriptar import Encriptar_fernet
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC
from src.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Security.Encriptar import Encriptar_fernet
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC
from domains.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
# ----------------------
# Cargar clave maestra
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
from typing import List
class Nota:
@@ -3,18 +3,42 @@ from dotenv import load_dotenv
from sqlalchemy import Table, Column, String, Text, MetaData
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota
from src.ConexionSql.Base_conexion import ConexionBase
from domains.TextManager.nota import Nota
from domains.ConexionSql.Base_conexion import ConexionBase
from typing import Tuple
import re
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from src.base import Base # Este es tu declarative_base()
titulo = os.getenv('DB_TITLE')
usuario = os.getenv('DB_USER')
passwrd = os.getenv('DB_PASSWORD')
host = os.getenv('DB_HOST')
port = os.getenv('DB_PORT')
db_name = os.getenv('DB_NAME')
db_credencial = PostgresCredencial(
titulo=titulo,
user=usuario,
password=passwrd,
host=host,
port=port,
dbname=db_name
)
# from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
from domains.base import Base # Este es tu declarative_base()
# ----------------------
# Cargar .env
@@ -37,11 +61,11 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
"""
try:
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
logger.info(f"Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
# Nombre SQL-safe
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'")
logger.debug(f"Nombre de tabla SQL-safe: '{nombre_tabla}'")
# Modelo ORM dinámico
class NotaModel(Base, Model_base):
@@ -57,15 +81,15 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
vector = Column(Vector(vector_dim), nullable=True)
vector_resumen = Column(Vector(vector_dim), nullable=True)
print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
logger.debug(f"Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
logger.debug(f"Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
logger.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
return NotaModel.__table__, NotaModel
except Exception as e:
print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
logger.error(f"Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
raise
# ----------------------
+15
View File
@@ -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})"
+82
View File
@@ -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
View File
+8 -8
View File
@@ -1,14 +1,14 @@
# entrypoint/init_db.py
from src.base import Base
from src.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from domains.base import Base
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
from src.TextManager.biblioteca_mmr import BibliotecaModel
from domains.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
from domains.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
from domains.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
from domains.TextManager.biblioteca_mmr import BibliotecaModel
from dotenv import load_dotenv
+2984 -36
View File
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -20,16 +20,25 @@
"storybook:build": "storybook build"
},
"dependencies": {
"@mantine/core": "8.0.0",
"@mantine/hooks": "8.0.0",
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/tiptap": "^8.0.1",
"@react-three/fiber": "^9.1.2",
"@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@uiw/react-markdown-preview": "^5.1.4",
"@uiw/react-md-editor": "^4.0.7",
"axios": "^1.9.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-rnd": "^10.5.2",
"react-router-dom": "^7.4.0"
"react-router-dom": "^7.4.0",
"turndown": "^7.2.0"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
@@ -44,6 +53,7 @@
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/three": "^0.176.0",
"@types/turndown": "^5.0.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0",
"eslint-config-mantine": "^4.0.3",
+67 -11
View File
@@ -1,29 +1,85 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { HomePage } from './pages/Home.page';
import { Consulta_API } from './pages/Consulta_api';
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './pages/Biblioteca';
import { HomePage } from './frontend_domains/Home/Home.page';
import { Consulta_API } from './frontend_domains/Experiments/Consulta_api';
import { Error_404 } from './frontend_domains/FitzStudio/404/404'; // Ajusta si está en otra carpeta
import { Grid_Dashboard } from './frontend_domains/Experiments/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './frontend_domains/TextEditor/Biblioteca';
import { VisualizacionesRandom } from './frontend_domains/Experiments/Visualizaciones_Random';
import { Camara_noir } from './frontend_domains/CamaraNoir/Camaras_noir';
import EditorTest from "./frontend_domains/TextEditor/Editor_Test";
import { ChatPage } from './frontend_domains/Llms/Chat/ChatPage';
import { LoginPage } from './frontend_domains/Usuarios/Login.page';
const router = createBrowserRouter([
// Home Principal
{
path: '/',
element: <HomePage />,
},
// Biblioteca
{
path: '/Consulta_API',
element: <Consulta_API />,
path: '/bibliot/Biblioteca',
element: <Biblioteca />,
},
{
path: '/Grid_Dashboard',
path: '/bibliot/editortest',
element: <EditorTest />,
},
// Chat LLM
{
path: '/llms/chat',
element: <ChatPage />,
},
// CamaraNoir
{
path: '/camara/principal',
element: <Camara_noir />,
},
// Experimentos
{
path: '/experiments/Consulta_API',
element: <Consulta_API />,
},
{
path: '/experiments/Grid_Dashboard',
element: <Grid_Dashboard />,
},
{
path: '/Biblioteca',
element: <Biblioteca />,
path: '/experiments/Visualizaciones_Random',
element: <VisualizacionesRandom />,
},
// Login
{
path: '/login',
element: <LoginPage />,
},
// FitzStudio Pages -------------------------------------------------------
// Error 404
{
path: '*',
element: <Error_404 />,
+6 -1
View File
@@ -1,3 +1,5 @@
// https://tabler.io/icons
// OUTLINED
export { default as IconArrowLeft } from './outlined/arrow-left.svg?react';
export { default as IconHomeOutline } from './outlined/home.svg?react';
@@ -11,7 +13,10 @@ export { default as IconSettings } from './outlined/settings.svg?react';
export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react';
export { default as IconArrowBarRight } from './outlined/arrow-bar-right.svg?react';
export { default as IconCheck } from './outlined/check.svg?react';
export { default as CameraPlus } from './outlined/camera-plus.svg?react';
export { default as Flask } from './outlined/flask.svg?react';
export { default as Users } from './outlined/users.svg?react';
export { default as IconNotebook } from './outlined/notebook.svg?react';
// FILLED
export { default as IconHomeFilled } from './filled/home.svg?react';
+8 -5
View File
@@ -4,17 +4,20 @@ import {
IconDeviceDesktopAnalytics,
IconFingerprint,
IconGauge,
IconNotebook,
IconHome2,
IconSettings,
IconUserOutline as IconUser,
CameraPlus,
Flask,
Users
} from '../assets/icons'; // ajusta según tu estructura de proyecto
export const mainLinksdata = [
{ icon: IconHome2, label: 'Home' },
{ icon: IconGauge, label: 'Dashboard' },
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' },
{ icon: IconCalendarStats, label: 'Releases' },
{ icon: IconUser, label: 'Account' },
{ icon: IconFingerprint, label: 'Security' },
{ icon: IconNotebook, label: 'Biblioteca' },
{ icon: Users, label: 'AgentesLLMs' },
{ icon: CameraPlus, label: 'CameraNoir' },
{ icon: Flask, label: 'Experimentos' },
{ icon: IconSettings, label: 'Settings' },
];
+35 -21
View File
@@ -1,35 +1,49 @@
// src/data/submenuLinks.ts
import { Biblioteca } from "@/frontend_domains/TextEditor/Biblioteca";
export const submenuLinks = {
// Home Principal
Home: [
{ label: 'Inicio', to: '/' },
{ label: 'Consulta Api', to: '/Consulta_API' },
{ label: 'Biblioteca', to: '/Biblioteca' },
],
Dashboard: [
{ label: 'Resumen', to: '/dashboard/resumen' },
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
{ label: 'Estadísticas', to: '/dashboard/estadisticas' },
{ label: 'Usuarios', to: '/dashboard/usuarios' },
// Biblioteca
Biblioteca: [
{ label: 'Biblioteca', to: '/bibliot/Biblioteca' },
{ label: 'test', to: '/bibliot/editortest' },
],
Analytics: [
{ label: 'Conversiones', to: '/analytics/conversiones' },
{ label: 'Tráfico', to: '/analytics/trafico' },
{ label: 'Tendencias', to: '/analytics/tendencias' },
// Experimentos
Experimentos: [
{ label: 'Consulta Api', to: '/experiments/Consulta_API' },
{ label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
],
Releases: [
{ label: 'Notas de versión', to: '/releases/notas-de-version' },
{ label: 'Historial', to: '/releases/historial' },
// Camara
CameraNoir: [
{ label: 'Camara_principal', to: '/camara/principal' },
],
Account: [
{ label: 'Perfil', to: '/account/perfil' },
{ label: 'Suscripciones', to: '/account/suscripciones' },
],
Security: [
{ label: 'Contraseña', to: '/security/contraseña' },
{ label: '2FA', to: '/security/2fa' },
// LLms
AgentesLLMs: [
{ label: 'LLMs', to: '/llms' },
{ label: 'Chat', to: '/llms/chat' },
{ label: 'Documentos', to: '/llms/documentos' },
],
// Settings
Settings: [
{ label: 'Preferencias', to: '/settings/preferencias' },
{ label: 'Notificaciones', to: '/settings/notificaciones' },
@@ -0,0 +1,39 @@
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { Card, Text, Container } from '@mantine/core';
export function Camara_noir() {
return (
<AppShellWithMenu>
<Container
size="lg"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<Card shadow="sm" padding="xl" radius="md" withBorder>
<Text size="lg" mb="md">
Cámara Noir en Vivo
</Text>
<img
src="http://10.8.0.9:8000/video"
alt="Stream MJPEG en vivo desde Raspberry Pi"
style={{
width: '640px',
height: '480px',
borderRadius: '8px',
border: '1px solid #ccc',
objectFit: 'cover',
}}
/>
<Text size="sm" color="dimmed" mt="sm">
Transmisión MJPEG en vivo vía FastAPI / libcamera-vid
</Text>
</Card>
</Container>
</AppShellWithMenu>
);
}
@@ -1,6 +1,6 @@
import { LlamadorAPI } from '../components/LlamadorAPI';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { LlamadorAPI } from './LlamadorAPI';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
export function Consulta_API() {
@@ -1,6 +1,6 @@
import { Grid } from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { GridDashboard } from '../components/Grid_dashboard';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { GridDashboard } from './Grid_dashboard_component';
export function Grid_Dashboard() {
return (
@@ -1,5 +1,5 @@
import { Select, Group } from '@mantine/core';
import { IconCheck } from '../assets/icons';
import { IconCheck } from '../../assets/icons';
interface MetodoSelectProps {
metodo: string;
@@ -0,0 +1,53 @@
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { Card, Grid, Title, Loader } from '@mantine/core';
import { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';
type ChartOption = any;
function useChartOption(endpoint: string) {
const [option, setOption] = useState<ChartOption | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/v1/charts/${endpoint}`)
.then((res) => res.json())
.then((json) => setOption(json))
.catch(console.error)
.finally(() => setLoading(false));
}, [endpoint]);
return { option, loading };
}
export function VisualizacionesRandom() {
const charts = [
{ title: 'Gráfico de barras', endpoint: 'bar' },
{ title: 'Gráfico de líneas', endpoint: 'line' },
{ title: 'Gráfico de pastel', endpoint: 'pie' },
{ title: 'Scatter plot', endpoint: 'scatter' },
];
return (
<AppShellWithMenu>
<Grid>
{charts.map(({ title, endpoint }, idx) => {
const { option, loading } = useChartOption(endpoint);
return (
<Grid.Col span={{ base: 12, sm: 6, md: 6, lg: 3 }} key={idx}>
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Title order={4}>{title}</Title>
{loading || !option ? (
<Loader mt="md" />
) : (
<ReactECharts option={option} style={{ height: 250, marginTop: 16 }} />
)}
</Card>
</Grid.Col>
);
})}
</Grid>
</AppShellWithMenu>
);
}
@@ -1,9 +1,10 @@
import { Box, Title, Text, Button, Group, Stack, Image, Center } from '@mantine/core';
import { useMantineTheme } from '@mantine/core';
import { IconArrowLeft } from '../assets/icons';
import { IconArrowLeft } from '../../../assets/icons';
import { Link } from 'react-router-dom';
import { MantineCardWithShader } from '../components/HoloShader'; // Ajusta ruta si es necesario
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { MantineCardWithShader } from './HoloShader_404'; // Ajusta ruta si es necesario
import { AppShellWithMenu } from '../Appshell/Appshell';
export function Error_404() {
@@ -11,9 +11,9 @@ import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { default as LogoIcon } from '../../assets/icons/favicon';
import { mainLinksdata } from '../../data/navigationsLinks_1';
import { submenuLinks } from '../../data/submenuLinks_1';
import { default as LogoIcon } from '../../../assets/icons/favicon';
import { mainLinksdata } from '../../../data/navigationsLinks_1';
import { submenuLinks } from '../../../data/submenuLinks_1';
import classes from './Appshell.module.css';
@@ -1,4 +1,4 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { AppShellWithMenu } from './Appshell/Appshell';
export function Plantilla() {
@@ -1,6 +1,6 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { Welcome } from '@/components/Welcome/Welcome';
import { ColorSchemeToggle } from '@/components/ColorSchemeToggle/ColorSchemeToggle';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { Welcome } from '@/frontend_domains/FitzStudio/Welcome/Welcome';
import { ColorSchemeToggle } from '@/frontend_domains/FitzStudio/ColorSchemeToggle/ColorSchemeToggle';
export function HomePage() {
@@ -0,0 +1,27 @@
import { useState } from "react";
import { Textarea, Button, Group } from "@mantine/core";
export function ChatInput({ onSend }: { onSend: (text: string) => void }) {
const [text, setText] = useState("");
const handleSend = () => {
if (!text.trim()) return;
onSend(text.trim());
setText("");
};
return (
<Group>
<Textarea
value={text}
onChange={(e) => setText(e.currentTarget.value)}
autosize
minRows={1}
maxRows={4}
placeholder="Escribe tu mensaje..."
style={{ flex: 1 }}
/>
<Button onClick={handleSend}>Enviar</Button>
</Group>
);
}
@@ -0,0 +1,64 @@
import { useState, useRef } from "react";
import { Container, Stack, Paper, ScrollArea, Title } from "@mantine/core";
import { ChatInput } from "./ChatInput";
import { MessageList } from "./MessageList";
import { AppShellWithMenu } from "../../FitzStudio/Appshell/Appshell";
export function ChatPage() {
const [messages, setMessages] = useState([
{ sender: "bot", content: "Hola, ¿en qué puedo ayudarte hoy?" },
]);
const wsRef = useRef<WebSocket | null>(null);
const handleSend = async (content: string) => {
const newMessages = [...messages, { sender: "user", content }];
setMessages(newMessages);
let currentResponse = "";
setMessages((prev) => [...prev, { sender: "bot", content: "" }]);
wsRef.current = new WebSocket("ws://localhost:8000/ws/chat");
wsRef.current.onopen = () => {
wsRef.current?.send(JSON.stringify({ prompt: content }));
};
wsRef.current.onmessage = (event) => {
const token = event.data;
currentResponse += token;
setMessages((prev) => {
const updated = [...prev];
updated[updated.length - 1] = { sender: "bot", content: currentResponse };
return updated;
});
};
wsRef.current.onerror = (err) => {
console.error("WebSocket error:", err);
setMessages((prev) => [
...prev.slice(0, -1),
{ sender: "bot", content: "⚠️ Error al comunicarse con el servidor." },
]);
};
wsRef.current.onclose = () => {
wsRef.current = null;
};
};
return (
<AppShellWithMenu>
<Container size="sm" p="md">
<Stack>
<Title order={2}>Chat LLM</Title>
<Paper shadow="xs" p="md" withBorder>
<ScrollArea h={400}>
<MessageList messages={messages} />
</ScrollArea>
</Paper>
<ChatInput onSend={handleSend} />
</Stack>
</Container>
</AppShellWithMenu>
);
}
@@ -0,0 +1,28 @@
import { Paper, Text, useMantineTheme, useMantineColorScheme } from "@mantine/core";
export function MessageBubble({ sender, content }: { sender: string; content: string }) {
const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const isUser = sender === "user";
const userBg = theme.colors[theme.primaryColor][0];
const botBg = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0];
const userTextColor = theme.colors[theme.primaryColor][9];
return (
<Paper
p="sm"
radius="md"
withBorder
style={{
alignSelf: isUser ? "flex-end" : "flex-start",
backgroundColor: isUser ? userBg : botBg,
maxWidth: "80%",
}}
>
<Text size="sm" c={isUser ? userTextColor : undefined}>
{content}
</Text>
</Paper>
);
}
@@ -0,0 +1,12 @@
import { Stack } from "@mantine/core";
import { MessageBubble } from "./MessageBubble";
export function MessageList({ messages }: { messages: { sender: string; content: string }[] }) {
return (
<Stack>
{messages.map((msg, i) => (
<MessageBubble key={i} sender={msg.sender} content={msg.content} />
))}
</Stack>
);
}

Some files were not shown because too many files have changed in this diff Show More