3 Commits

Author SHA1 Message Date
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
41 changed files with 2166 additions and 236 deletions
+53 -1
View File
@@ -14,6 +14,8 @@
"@tabler/icons": "^3.31.0", "@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
@@ -4182,6 +4184,36 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
"integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4906,7 +4938,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
@@ -8596,6 +8627,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/size-sensor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
"integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==",
"license": "ISC"
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -10644,6 +10681,21 @@
"zod": "^3.24.1" "zod": "^3.24.1"
} }
}, },
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/zustand": { "node_modules/zustand": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz",
+2
View File
@@ -26,6 +26,8 @@
"@tabler/icons": "^3.31.0", "@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
+10
View File
@@ -4,6 +4,8 @@ import { Consulta_API } from './pages/Consulta_api';
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta 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 { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './pages/Biblioteca'; import { Biblioteca } from './pages/Biblioteca';
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
import { Camara_noir } from './pages/Camaras_noir';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -22,6 +24,14 @@ const router = createBrowserRouter([
path: '/Biblioteca', path: '/Biblioteca',
element: <Biblioteca />, element: <Biblioteca />,
}, },
{
path: '/analytics/Visualizaciones_Random',
element: <VisualizacionesRandom />,
},
{
path: '/analytics/Camaras',
element: <Camara_noir />,
},
{ {
@@ -0,0 +1,26 @@
// Archivo: components/CanvasDisplay.tsx
import { Box } from '@mantine/core';
import { RefObject } from 'react';
interface CanvasDisplayProps {
canvasRef: RefObject<HTMLCanvasElement>;
}
export function CanvasDisplay({ canvasRef }: CanvasDisplayProps) {
return (
<Box style={{ position: 'relative', width: '100%', height: 480 }}>
<canvas
ref={canvasRef}
width={640}
height={480}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
borderRadius: 8,
backgroundColor: '#000',
}}
/>
</Box>
);
}
@@ -0,0 +1,74 @@
// Archivo: components/CaptureGrid.tsx
import { useCallback } from 'react';
import { SimpleGrid } from '@mantine/core';
import { FrameCard } from './FrameCard';
interface CaptureGridProps {
totalSlots: number;
capturas: string[][];
setCapturas: (value: string[][]) => void;
frameIndices: number[];
setFrameIndices: (value: number[]) => void;
fijados: boolean[];
setFijados: (value: boolean[]) => void;
frameTemporal: number | null;
onScroll: (e: React.WheelEvent, index: number) => void;
onSliderChange: (val: number) => void;
onSliderEnd: (index: number, val: number) => void;
numColumnas: number;
}
export function CaptureGrid({
totalSlots,
capturas,
setCapturas,
frameIndices,
setFrameIndices,
fijados,
setFijados,
frameTemporal,
onScroll,
onSliderChange,
onSliderEnd,
numColumnas,
}: CaptureGridProps) {
const handleImageClick = useCallback((index: number, dataUrl: string) => {
const nuevasCapturas = [...capturas];
nuevasCapturas[index] = [dataUrl];
setCapturas(nuevasCapturas);
const nuevosFijados = [...fijados];
nuevosFijados[index] = true;
setFijados(nuevosFijados);
const nuevosIndices = [...frameIndices];
nuevosIndices[index] = 0;
setFrameIndices(nuevosIndices);
}, [capturas, fijados, frameIndices, setCapturas, setFijados, setFrameIndices]);
return (
<SimpleGrid cols={numColumnas} style={{ flex: 1, columnGap: 12, rowGap: 4 }}>
{Array.from({ length: totalSlots }).map((_, i) => {
const frames = capturas[i] ?? [];
const currentIndex =
!fijados[i] && frameTemporal !== null && frames.length > 0
? Math.max(0, Math.min(frames.length - 1, frameTemporal))
: frameIndices[i];
return (
<FrameCard
key={i}
index={i}
frames={frames}
currentIndex={currentIndex}
fijado={fijados[i]}
onScroll={onScroll}
onSliderChange={onSliderChange}
onSliderEnd={onSliderEnd}
onImageClick={handleImageClick}
/>
);
})}
</SimpleGrid>
);
}
@@ -0,0 +1,25 @@
// Archivo: components/ControlPanel.tsx
import { Button, Group } from '@mantine/core';
interface ControlPanelProps {
grabando: boolean;
onAlternar: () => void;
onLimpiar: () => void;
onDesfijar: () => void;
}
export function ControlPanel({ grabando, onAlternar, onLimpiar, onDesfijar }: ControlPanelProps) {
return (
<Group gap="sm">
<Button onClick={onAlternar} variant="light" color={grabando ? 'orange' : 'blue'}>
{grabando ? 'Grabando... (Presiona Espacio)' : 'Iniciar grabación'}
</Button>
<Button onClick={onLimpiar} variant="filled" color="red">
Eliminar imágenes
</Button>
<Button onClick={onDesfijar} variant="default" color="gray">
Desfijar todos (D)
</Button>
</Group>
);
}
@@ -0,0 +1,125 @@
// Archivo: components/FrameCard.tsx
import { Box, HoverCard, Image, Slider, Stack, Text } from '@mantine/core';
interface FrameCardProps {
index: number;
frames: string[];
currentIndex: number;
fijado: boolean;
onScroll: (e: React.WheelEvent, index: number) => void;
onSliderChange: (val: number) => void;
onSliderEnd: (index: number, val: number) => void;
onImageClick?: (index: number, dataUrl: string) => void;
}
export function FrameCard({
index,
frames,
currentIndex,
fijado,
onScroll,
onSliderChange,
onSliderEnd,
onImageClick,
}: FrameCardProps) {
const borderColor = fijado ? '#4caf50' : '#ccc';
return (
<HoverCard key={index} width={700} shadow="md" position="top" withArrow>
<HoverCard.Target>
<Box
style={{
width: 160,
height: 120,
borderRadius: 8,
border: `2px solid ${borderColor}`,
overflow: 'hidden',
position: 'relative',
background: '#f1f1f1',
cursor: 'pointer',
}}
>
{frames.length > 0 && currentIndex < frames.length && (
<>
<Image
src={frames[currentIndex]}
alt={`Captura ${index}`}
width="100%"
height="100%"
fit="cover"
/>
<Text
size="xs"
style={{
position: 'absolute',
bottom: 4,
right: 6,
backgroundColor: 'rgba(0,0,0,0.6)',
color: 'white',
borderRadius: 4,
padding: '2px 4px',
fontSize: 10,
}}
>
{currentIndex + 1} / {frames.length}
</Text>
</>
)}
</Box>
</HoverCard.Target>
<HoverCard.Dropdown p="xs" onWheel={(e) => onScroll(e, index)}>
{frames.length > 0 && currentIndex < frames.length && (
<Stack gap={6}>
<Image
src={frames[currentIndex]}
alt={`Vista ampliada ${index}`}
style={{ width: '100%', height: 'auto', maxWidth: '100%', cursor: 'zoom-in' }}
fit="contain"
onClick={async (e) => {
const imgElement = e.currentTarget;
const img = document.createElement('img');
img.src = frames[currentIndex];
await img.decode();
const rect = imgElement.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const ratioX = clickX / rect.width;
const ratioY = clickY / rect.height;
const zoomFactor = 2;
const cropWidth = img.width / zoomFactor;
const cropHeight = img.height / zoomFactor;
const centerX = img.width * ratioX;
const centerY = img.height * ratioY;
const x = Math.max(0, Math.min(img.width - cropWidth, centerX - cropWidth / 2));
const y = Math.max(0, Math.min(img.height - cropHeight, centerY - cropHeight / 2));
const canvas = document.createElement('canvas');
canvas.width = cropWidth;
canvas.height = cropHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, x, y, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
const dataUrl = canvas.toDataURL('image/jpeg');
onImageClick?.(index, dataUrl);
}
}}
/>
<Slider
value={currentIndex}
onChange={(val) => onSliderChange(val)}
onChangeEnd={(val) => onSliderEnd(index, val)}
min={0}
max={frames.length - 1}
/>
</Stack>
)}
</HoverCard.Dropdown>
</HoverCard>
);
}
@@ -0,0 +1,36 @@
// Archivo: components/GridConfigPanel.tsx
import { NumberInput, Stack, Text } from '@mantine/core';
interface GridConfigPanelProps {
numFilas: number;
setNumFilas: (val: number) => void;
numColumnas: number;
setNumColumnas: (val: number) => void;
}
export function GridConfigPanel({ numFilas, setNumFilas, numColumnas, setNumColumnas }: GridConfigPanelProps) {
return (
<Stack align="center" gap="xs" ml="md">
<Text size="sm">Cartas</Text>
<NumberInput
value={numFilas}
onChange={(val) => setNumFilas(Number(val))}
min={1}
max={5}
step={1}
size="xs"
style={{ width: 60 }}
/>
<Text size="sm">Jugadores</Text>
<NumberInput
value={numColumnas}
onChange={(val) => setNumColumnas(Number(val))}
min={1}
max={11}
step={1}
size="xs"
style={{ width: 60 }}
/>
</Stack>
);
}
@@ -0,0 +1,6 @@
export * from './CanvasDisplay';
export * from './ControlPanel';
export * from './GridConfigPanel';
export * from './CaptureGrid';
export * from './FrameCard';
export * from './useCamaraNoir';
@@ -0,0 +1,212 @@
import { useEffect, useRef, useState } from 'react';
export function useCamaraNoir() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const bufferPrevioRef = useRef<string[]>([]);
const bufferGrabacionRef = useRef<string[]>([]);
const bufferAcumuladoRef = useRef<string[]>([]);
const pregrabacionActivaRef = useRef(true);
const primeraGrabacionRealizadaRef = useRef(false);
const [bufferGlobal, setBufferGlobal] = useState<string[]>([]);
const [intervaloId, setIntervaloId] = useState<ReturnType<typeof setInterval> | null>(null);
const [grabando, setGrabando] = useState(false);
const [capturas, setCapturas] = useState<string[][]>([]);
const [frameIndices, setFrameIndices] = useState<number[]>([]);
const [fijados, setFijados] = useState<boolean[]>([]);
const [frameTemporal, setFrameTemporal] = useState<number | null>(null);
const [numFilas, setNumFilas] = useState(2);
const [numColumnas, setNumColumnas] = useState(10);
const DELAY_ENTRE_FRAMES_MS = 10;
const SEGUNDOS_PRE_GRABACION = 5;
const FPS = 1000 / DELAY_ENTRE_FRAMES_MS;
const FRAMES_PRE_GRABACION = Math.floor(FPS * SEGUNDOS_PRE_GRABACION);
const totalSlots = numFilas * numColumnas;
useEffect(() => {
const id = setInterval(() => {
if (!pregrabacionActivaRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const frame = canvas.toDataURL('image/jpeg');
bufferPrevioRef.current.push(frame);
if (bufferPrevioRef.current.length > FRAMES_PRE_GRABACION) {
bufferPrevioRef.current.shift();
}
}, DELAY_ENTRE_FRAMES_MS);
return () => clearInterval(id);
}, []);
const desfijarTodos = () => {
setFijados(Array(totalSlots).fill(false));
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const socket = new WebSocket('ws://10.8.0.9:8000/ws');
socket.binaryType = 'blob';
socket.onmessage = async (event) => {
if (event.data instanceof Blob) {
const imgBitmap = await createImageBitmap(event.data);
ctx.drawImage(imgBitmap, 0, 0, canvas.width, canvas.height);
}
};
socket.onerror = (e) => console.error('WebSocket error:', e);
socket.onclose = () => console.warn('WebSocket cerrado');
return () => socket.close();
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === 'Space') {
event.preventDefault();
alternarGrabacion();
} else if (event.key.toLowerCase() === 'f') {
event.preventDefault();
limpiarCapturas();
} else if (event.key.toLowerCase() === 'd') {
event.preventDefault();
desfijarTodos();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [grabando, bufferGlobal, totalSlots]);
useEffect(() => {
const total = numFilas * numColumnas;
const nuevoCapturas = Array(total)
.fill(null)
.map((_, i) => capturas[i] ?? bufferGlobal);
const nuevoIndices = Array(total)
.fill(null)
.map((_, i) => frameIndices[i] ?? (frameTemporal ?? Math.floor(bufferGlobal.length / 2)));
const nuevosFijados = Array(total)
.fill(null)
.map((_, i) => fijados[i] ?? false);
setCapturas(nuevoCapturas);
setFrameIndices(nuevoIndices);
setFijados(nuevosFijados);
}, [numFilas, numColumnas]);
const iniciarGrabacion = () => {
if (grabando) return;
setGrabando(true);
bufferGrabacionRef.current = [];
if (!primeraGrabacionRealizadaRef.current) {
pregrabacionActivaRef.current = false;
}
const id = setInterval(() => {
const frame = capturarFrame();
if (frame) {
bufferGrabacionRef.current.push(frame);
}
}, DELAY_ENTRE_FRAMES_MS);
setIntervaloId(id);
};
const capturarFrame = (): string | null => {
const canvas = canvasRef.current;
if (!canvas) return null;
return canvas.toDataURL('image/jpeg');
};
const detenerGrabacion = () => {
if (!grabando) return;
setGrabando(false);
if (intervaloId) clearInterval(intervaloId);
const nuevaSesion = primeraGrabacionRealizadaRef.current
? [...bufferGrabacionRef.current] // solo frames nuevos
: [...bufferPrevioRef.current, ...bufferGrabacionRef.current];
bufferAcumuladoRef.current.push(...nuevaSesion);
primeraGrabacionRealizadaRef.current = true;
setBufferGlobal([...bufferAcumuladoRef.current]);
setCapturas(Array(totalSlots).fill([...bufferAcumuladoRef.current]));
const frameInicial = Math.floor(bufferAcumuladoRef.current.length / 2);
setFrameIndices(Array(totalSlots).fill(frameInicial));
setFijados(Array(totalSlots).fill(false));
setFrameTemporal(frameInicial);
};
const alternarGrabacion = () => {
grabando ? detenerGrabacion() : iniciarGrabacion();
};
const limpiarCapturas = () => {
setCapturas([]);
setFrameIndices([]);
setFijados([]);
setBufferGlobal([]);
bufferGrabacionRef.current = [];
bufferPrevioRef.current = [];
bufferAcumuladoRef.current = [];
primeraGrabacionRealizadaRef.current = false;
pregrabacionActivaRef.current = true;
setFrameTemporal(null);
if (intervaloId) clearInterval(intervaloId);
setGrabando(false);
};
const moverFrame = (capturaIndex: number, nuevoIndice: number) => {
setFrameIndices((prev) => {
const nuevos = [...prev];
const max = capturas[capturaIndex]?.length - 1 ?? 0;
nuevos[capturaIndex] = Math.max(0, Math.min(max, nuevoIndice));
return nuevos;
});
};
const moverFrameYFijar = (capturaIndex: number, nuevoIndice: number) => {
moverFrame(capturaIndex, nuevoIndice);
setFijados((prev) => {
const actualizados = [...prev];
actualizados[capturaIndex] = true;
return actualizados;
});
setFrameTemporal(nuevoIndice);
};
const manejarScrollEnSlider = (e: React.WheelEvent, index: number) => {
e.preventDefault();
if (!fijados[index]) return;
moverFrame(index, frameIndices[index] + (e.deltaY > 0 ? 1 : -1));
};
return {
canvasRef,
bufferGlobal,
grabando,
capturas,
frameIndices,
fijados,
frameTemporal,
numFilas,
setNumFilas,
numColumnas,
setNumColumnas,
alternarGrabacion,
limpiarCapturas,
desfijarTodos,
manejarScrollEnSlider,
moverFrameYFijar,
setFrameTemporal,
};
}
+2 -2
View File
@@ -14,8 +14,8 @@ export const submenuLinks = {
{ label: 'Usuarios', to: '/dashboard/usuarios' }, { label: 'Usuarios', to: '/dashboard/usuarios' },
], ],
Analytics: [ Analytics: [
{ label: 'Conversiones', to: '/analytics/conversiones' }, { label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
{ label: 'Tráfico', to: '/analytics/trafico' }, { label: 'Camaras', to: '/analytics/Camaras' },
{ label: 'Tendencias', to: '/analytics/tendencias' }, { label: 'Tendencias', to: '/analytics/tendencias' },
], ],
Releases: [ Releases: [
+57
View File
@@ -0,0 +1,57 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { Stack, Box } from '@mantine/core';
import {
CanvasDisplay,
ControlPanel,
CaptureGrid,
GridConfigPanel,
useCamaraNoir
} from '../components/Camara_noir';
export function Camara_noir() {
const camara = useCamaraNoir();
const totalSlots = camara.numFilas * camara.numColumnas;
return (
<AppShellWithMenu>
<Stack p="md" gap="md">
{/* Contenedor para botones encima del video */}
<Box style={{ position: 'relative', width: '100%' }}>
<ControlPanel
grabando={camara.grabando}
onAlternar={camara.alternarGrabacion}
onLimpiar={camara.limpiarCapturas}
onDesfijar={camara.desfijarTodos}
/>
<Box style={{ marginTop: 8 }}>
<CanvasDisplay canvasRef={camara.canvasRef} />
</Box>
</Box>
{/* Grilla de capturas + configurador lateral */}
<Box style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<CaptureGrid
totalSlots={totalSlots}
capturas={camara.capturas}
frameIndices={camara.frameIndices}
fijados={camara.fijados}
frameTemporal={camara.frameTemporal}
onScroll={camara.manejarScrollEnSlider}
onSliderChange={camara.setFrameTemporal}
onSliderEnd={camara.moverFrameYFijar}
numColumnas={camara.numColumnas}
/>
<GridConfigPanel
numFilas={camara.numFilas}
setNumFilas={camara.setNumFilas}
numColumnas={camara.numColumnas}
setNumColumnas={camara.setNumColumnas}
/>
</Box>
</Stack>
</AppShellWithMenu>
);
}
@@ -0,0 +1,53 @@
import { AppShellWithMenu } from '../components/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/${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>
);
}
+34 -1
View File
@@ -1735,6 +1735,22 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
echarts-for-react@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz"
integrity sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==
dependencies:
fast-deep-equal "^3.1.3"
size-sensor "^1.0.1"
"echarts@^3.0.0 || ^4.0.0 || ^5.0.0", echarts@^5.6.0:
version "5.6.0"
resolved "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz"
integrity sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==
dependencies:
tslib "2.3.0"
zrender "5.6.1"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
@@ -3810,7 +3826,7 @@ react-use-measure@^2.1.7:
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", "react@>= 16.3.0", react@>=16.13, react@>=16.3.0, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0: "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^15.0.0 || >=16.0.0", "react@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", "react@>= 16.3.0", react@>=16.13, react@>=16.3.0, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0:
version "19.1.0" version "19.1.0"
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
@@ -4145,6 +4161,11 @@ signal-exit@^4.0.1:
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
size-sensor@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz"
integrity sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==
slash@^3.0.0: slash@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
@@ -4620,6 +4641,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@2.6.2: tslib@2.6.2:
version "2.6.2" version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
@@ -5064,6 +5090,13 @@ zod@^3.23.8, zod@^3.24.1, zod@^3.24.2:
resolved "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz" resolved "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz"
integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==
zrender@5.6.1:
version "5.6.1"
resolved "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz"
integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==
dependencies:
tslib "2.3.0"
zustand@^5.0.3: zustand@^5.0.3:
version "5.0.4" version "5.0.4"
resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz" resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz"
-29
View File
@@ -1,29 +0,0 @@
# client.py
import asyncio
from src.Llms.MCPs.Mcp_client import MCPClient
from src.Llms.MCPs.Http_mcp_server import HttpMCPServer
async def main():
client = MCPClient()
client.register_server(HttpMCPServer(
name="tools",
path="IGNORED_IN_CLIENT", # no importa aquí
host="127.0.0.1",
port=4300,
path_http="/tools"
))
await client.connect_all()
result = await client.call_tool({
"server": "tools",
"tool": "get_hostname",
"input": {}
})
print("RESULT:", result)
await client.disconnect_all()
if __name__ == "__main__":
asyncio.run(main())
+85 -32
View File
@@ -11,28 +11,71 @@ from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.client import Client from fastmcp.client import Client
from src.Llms.MCPs.McpClient import MCPClient # ya tienes esta clase from src.Llms.MCPs.McpClient import MCPClient # ya tienes esta clase
from src.Llms.MCPs.McpClient_Registry import ClientRegistry # o ajusta según tu estructura from src.Llms.MCPs.McpClient_Registry import ClientRegistry # o ajusta según tu estructura
from src.Credenciales.ollama_credencial import OllamaCredencial
from src.ConexionApis.Ollama_cliente import OllamaCliente
from src.Llms.Modelos.Ollama_model import ModeloOllama
from src.Llms.MCPs.McpServer import MCPServerRunner
import asyncio import asyncio
async def main(): async def main():
# Usar Credencial openai # # Usar Credencial openai
conexion_admin = PostgresConexion(db_credencial) conexion_admin = PostgresConexion(db_credencial)
repo = OpenAICredencialRepo(conexion_admin) repo = OpenAICredencialRepo(conexion_admin)
credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014") credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014")
if credencial_openai is None:
raise ValueError("No se encontró la credencial OpenAI con el ID proporcionado.")
cliente = OpenAICliente(credencial_openai) cliente = OpenAICliente(credencial_openai)
# # Llamamos a los servidores para iniciarlos
# crea el modelo (openai) # venv_python = r"E:\Fitz_Studio\.venv\Scripts\python.exe"
# # runner_math = MCPServerRunner(
# # r"E:\Fitz_Studio\src\Llms\MCPs\McpServers\server_math.py",
# # python_path=venv_python
# # )
# # runner_tools = MCPServerRunner(
# # r"E:\Fitz_Studio\src\Llms\MCPs\McpServers\server_utils.py",
# # python_path=venv_python
# # )
# runner_files = MCPServerRunner(
# r"E:\Fitz_Studio\src\Llms\MCPs\McpServers\server_files.py",
# python_path=venv_python
# )
# # await runner_math.start()
# # await runner_tools.start()
# await runner_files.start()
# # Esperamos un poco para asegurarnos de que los servidores estén listos
# await asyncio.sleep(2)
# Usar Credencial ollama
# credencial_ollama = OllamaCredencial(titulo="Ollama")
# cliente = OllamaCliente(credencial_ollama)
# modelo = ModeloOllama(
# cliente=cliente,
# model="llama3.1:8b")
# # crea el modelo (openai)
modelo = ModeloOpenAI( modelo = ModeloOpenAI(
cliente=cliente, cliente=cliente,
model="gpt-4o", model="gpt-4o",
temperature=1, temperature=1
top_p=1.0
) )
# Le otorga memoria # Le otorga memoria
@@ -46,45 +89,51 @@ async def main():
# Cargamos las herramientas # Cargamos las herramientas
herramientas = MCPClient.from_http( # herramientas = MCPClient.from_http(
name="tools", # name="tools",
url="http://127.0.0.1:4300/tools/" # url="http://127.0.0.1:4300/tools/"
# )
# math = MCPClient.from_http(
# name="math",
# url="http://127.0.0.1:4200/math/"
# )
archivos = MCPClient.from_http(
name="files",
url="http://127.0.0.1:4201/fs"
) )
math = MCPClient.from_http(
name="math",
url="http://127.0.0.1:4200/math/"
)
# Las añadimos al registro de herramientas # Las añadimos al registro de herramientas
registry = ClientRegistry() registry = ClientRegistry()
registry.add("tools", herramientas) # registry.add("tools", herramientas)
registry.add("math", math) # registry.add("math", math)
registry.add("files", archivos)
# --- INICIALIZACIÓN DEL AGENTE --- # --- INICIALIZACIÓN DEL AGENTE ---
agente2 = AgenteAI( agente2 = AgenteAI(
modelo=modelo, modelo=modelo,
nombre="Asistente Inteligente", nombre="Asistente Inteligente",
descripcion="Un asistente conversacional versátil, capaz de resolver problemas, acceder a herramientas y proporcionar respuestas útiles.", descripcion="",
system_prompt=( system_prompt="",
"Eres un asistente inteligente que ayuda al usuario a resolver tareas, responder preguntas y usar herramientas disponibles si es necesario. " # system_prompt = """
"Debes razonar paso a paso, y si se detecta que una herramienta MCP es útil, actúa generando el bloque MCP apropiado sin dar más explicaciones. " # Eres un asistente general. No tienes acceso a herramientas externas ni herramientas MCP. No debes mencionar herramientas MCP, servidores ni bloques de código.
"Siempre estructura tus respuestas con claridad, y termina con <END> cuando creas haber completado la tarea." # Responde de forma clara y amigable a cualquier pregunta general del usuario.
), # """.strip(),
rol="asistente", rol="asistente",
objetivos=[ objetivos=[
"Resolver tareas del usuario", # "Resolver tareas del usuario",
"Usar herramientas MCP si es útil", # "Usar herramientas MCP si es útil",
"Responder de forma clara y útil" # "Responder de forma clara y útil"
], ],
# max_iterations=3, max_iterations=0,
# memoria=memoria, memoria=memoria,
mcp=registry # ← ✅ Integración del cliente MCP mcp=registry # ← ✅ Integración del cliente MCP
) )
@@ -92,12 +141,17 @@ async def main():
# --- FUNCIÓN DE EJECUCIÓN --- # --- FUNCIÓN DE EJECUCIÓN ---
async def probar_interaccion_stream(): async def probar_interaccion_stream():
# # 🔌 Conectar a los servidores MCP registrados print("🧠 Agente iniciado. Escribe 'salir' para terminar.\n")
# await mcp_client.connect_all()
print("Respuesta en streaming:\n") while True:
prompt = input("\n📝 Escribe tu pregunta: ")
if prompt.strip().lower() in ("salir", "exit", "quit"):
print("\n👋 Hasta pronto.")
break
print("\n💬 Respuesta en streaming:\n")
respuesta_gen = await agente2.interactuar_en_bucle( respuesta_gen = await agente2.interactuar_en_bucle(
"¿Cuál es mi nombre de usuario en este sistema?", prompt=prompt,
stream=True stream=True
) )
@@ -108,10 +162,9 @@ async def main():
await probar_interaccion_stream() await probar_interaccion_stream()
# Ejecutar # Ejecutar
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
+23 -23
View File
@@ -1,29 +1,29 @@
import asyncio
from src.Llms.MCPs.McpServer import MCPServerRunner
async def main():
venv_python = r"E:\Fitz_Studio\.venv\Scripts\python.exe"
runner_math = MCPServerRunner(
r"E:\Fitz_Studio\src\Llms\MCPs\McpServers\server_math.py",
python_path=venv_python
)
runner_tools = MCPServerRunner(
r"E:\Fitz_Studio\src\Llms\MCPs\McpServers\server_utils.py",
python_path=venv_python
)
await runner_math.start()
await runner_tools.start()
async def test_registry(registry: ClientRegistry): try:
tools = await registry.listar_tools_por_cliente() while True:
prompts = await registry.listar_prompts_por_cliente() await asyncio.sleep(1)
resources = await registry.listar_resources_por_cliente() except KeyboardInterrupt:
print("\n⛔ Terminando servidores...")
print("\n🔧 Herramientas:", tools) await runner_math.stop()
await runner_tools.stop()
if __name__ == "__main__":
print("\n📋 Prompts:", prompts) asyncio.run(main())
print("\n📂 Resources:", resources)
asyncio.run(test_registry(registry))
async def test_wrapper():
# 2. Llamar a una herramienta de prueba
result = await herramientas.call_tool("generate_uuid")
print("\n🆔 UUID generado:", result[0].text) # Accedemos al contenido directamente
# asyncio.run(test_wrapper())
@@ -0,0 +1,69 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Permitir acceso desde el frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # cámbialo por ["http://localhost:5173"] si es necesario
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/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"}]
}
@app.get("/api/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"}]
}
@app.get("/api/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"}
]
}]
}
@app.get("/api/scatter")
def get_scatter_chart():
return {
"xAxis": {},
"yAxis": {},
"series": [{
"symbolSize": 20,
"data": [
[10, 8], [20, 20], [30, 10], [40, 30], [50, 15]
],
"type": "scatter"
}]
}
+1
View File
@@ -0,0 +1 @@
Érase una vez un pequeño ratón que vivía en un bosque mágico. Un día, encontró una pequeña llave dorada...
+1
View File
@@ -0,0 +1 @@
Había una vez un pequeño conejo que soñaba con saltar más alto que las nubes. Un día, encontró unas botas mágicas...
+1
View File
@@ -0,0 +1 @@
En un bosque encantado vivía una pequeña hada que siempre ayudaba a los animales a encontrar su camino de regreso a casa...
+1
View File
@@ -0,0 +1 @@
Había una vez un osito que quería aprender a tocar la flauta mágica para alegrar a los habitantes del bosque...
+1
View File
@@ -0,0 +1 @@
En una colina lejana, vivía un conejo que podía correr tan rápido como el viento. Un día, decidió participar en la gran carrera del bosque...
+4
View File
@@ -0,0 +1,4 @@
# main.py
if __name__ == '__main__':
print('Bienvenido al sistema ERP')
+4
View File
@@ -0,0 +1,4 @@
# test_sample.py
def test_placeholder():
assert True
+8
View File
@@ -0,0 +1,8 @@
from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_eventos", created_by="sistema_agente")
logger.info("Esto solo se verá en la base de datos")
logger.error("No aparecerá en la terminal")
+62
View File
@@ -0,0 +1,62 @@
import requests
from src.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()
+20
View File
@@ -0,0 +1,20 @@
from src.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"
}
+267 -99
View File
@@ -3,6 +3,14 @@ from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
from src.Llms.MCPs.McpClient_Registry import ClientRegistry from src.Llms.MCPs.McpClient_Registry import ClientRegistry
from datetime import datetime from datetime import datetime
from typing import Optional, List, Union, AsyncGenerator from typing import Optional, List, Union, AsyncGenerator
import re
import json
from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
class AgenteAI: class AgenteAI:
@@ -17,7 +25,7 @@ class AgenteAI:
max_iterations: int = 1, max_iterations: int = 1,
memoria: Optional[MemoryConvABC] = None, memoria: Optional[MemoryConvABC] = None,
version: str = "1.0.0", version: str = "1.0.0",
mcp: ClientRegistry = None, mcp: Optional[ClientRegistry] = None,
output_schema: Optional[dict] = None, output_schema: Optional[dict] = None,
): ):
self.modelo = modelo self.modelo = modelo
@@ -48,68 +56,144 @@ class AgenteAI:
@property async def generar_system_prompt(self) -> str:
async def full_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() tools_str = await self._obtener_herramientas_disponibles_str()
return f""" bloques.append(f"### Herramientas disponibles (MCP):\n{tools_str}")
Eres un agente conversacional con acceso a herramientas MCP (Model Context Protocol). 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:
Tu comportamiento sigue este flujo: ```mcp
{
1. **Piensa** para razonar tu decisión. "server": "tools",
2. **Decide** si: "tool": "get_current_user",
- puedes responder tú mismo, "input": {}
- necesitas más información del usuario, }
- o necesitas una herramienta MCP. ```
3. **Actúa**:
- Cuando uses MCP, termina **solo** con un bloque de código MCP y **nada más**.
- Ten en cuenta EXACTAMENTE los parámetros especificados.
- **No expliques, no hables después del bloque. Termina tu turno.**
--- ---
# Formato MCP ### ❗ 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 ```mcp
{{ {
"tool": "<nombre_de_la_herramienta>", "server": "tools",
"input": {{ "tool": "generate_uuid",
"clave": "valor" "input": {}
}} }
}}
Reglas clave:
Razonas antes de actuar. """)
Nunca hables después de un bloque MCP. 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}")
No combines respuestas y herramientas. if self.output_schema:
schema_str = str(self.output_schema)
bloques.append(f"### Salida esperada:\n{schema_str}")
Piensa. Decide. Actúa. bloques.append(f"### Prompt del usuario:\n{prompt_usuario}")
Herramientas disponibles para usar con MCP: return "\n\n".join(bloques)
{tools_str}
""".strip()
### Conseguir las herramientas disponibles
# Conseguir las herramientas disponibles
async def _obtener_herramientas_disponibles_str(self) -> str: async def _obtener_herramientas_disponibles_str(self) -> str:
logger.info("Inicio de obtención de herramientas disponibles")
if not self.mcp: if not self.mcp:
logger.warning("No se ha definido el cliente MCP.")
return "No se han definido herramientas disponibles." 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 = [] herramientas = []
tools_por_cliente = await self.mcp.listar_tools_por_cliente()
for name, tools in tools_por_cliente.items(): for name, tools in tools_por_cliente.items():
if not tools: if not tools:
logger.info(f"Servidor {name} no tiene herramientas disponibles.")
continue continue
herramientas.append(f"\n🔌 Cliente: {name}")
herramientas.append(f"\n🔌 Server: {name}")
for tool in tools: for tool in tools:
props = tool.inputSchema.get("properties", {}) props = tool.inputSchema.get("properties", {})
parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items()) parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items())
@@ -118,61 +202,146 @@ Herramientas disponibles para usar con MCP:
Parámetros: Parámetros:
{parametros} {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." 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
# Formatear prompt para agentes
def _formatear_prompt(self, mensajes: List[dict]) -> str: def _formatear_prompt(self, mensajes: List[dict]) -> str:
return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes]) 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 ###----------- Funcion para interactuar
async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]: async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]:
historial = self.memoria.cargar_historial_chat() if self.memoria else [] mensaje_usuario = await self.construir_prompt_usuario(prompt)
contexto = historial + [{"role": "user", "content": prompt}] contexto = [{"role": "user", "content": mensaje_usuario}]
prompt_final = self._formatear_prompt(contexto) prompt_final = self._formatear_prompt(contexto)
respuesta = await self.modelo.responder( respuesta = await self.modelo.responder(
prompt=prompt_final, prompt=prompt_final,
system_prompt=await self.full_system_prompt, # ✅ correcto system_prompt=await self.generar_system_prompt(),
stream=stream stream=stream
) )
if stream:
async def wrapper():
buffer_respuesta = ""
async for token in respuesta:
buffer_respuesta += token
yield token
if self.memoria:
self.memoria.guardar_turno("user", prompt)
self.memoria.guardar_turno("assistant", buffer_respuesta)
self.numero_interacciones += 1
self.updated_at = datetime.now()
return wrapper()
else:
if self.memoria:
self.memoria.guardar_turno("user", prompt)
self.memoria.guardar_turno("assistant", respuesta)
self.numero_interacciones += 1
self.updated_at = datetime.now()
return respuesta return respuesta
@@ -180,87 +349,86 @@ Herramientas disponibles para usar con MCP:
###----------- Funcion para interactuar en bucle ###----------- Funcion para interactuar en bucle
async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]: async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]:
print("🚀 [interactuar_en_bucle] Iniciando interacción")
historial = self.memoria.cargar_historial_chat() if self.memoria else []
print(f"📜 [interactuar_en_bucle] Historial cargado: {historial}")
respuestas = [] if not stream else None respuestas = [] if not stream else None
respuesta_anterior = None respuesta_anterior = ""
resultado_mcp_anterior = None # <-- Guarda último resultado del MCP
iteration = 0 iteration = 0
prompt_original = prompt.strip() prompt_original = prompt.strip()
print(f"✏️ [interactuar_en_bucle] Prompt original: {prompt_original}")
async def generador(): async def generador():
nonlocal iteration, respuesta_anterior nonlocal iteration, respuesta_anterior, resultado_mcp_anterior
prompt_actual = prompt_original
while self.max_iterations == 0 or iteration < self.max_iterations: while self.max_iterations == 0 or iteration < self.max_iterations:
print(f"\n🔁 [generador] Iteración: {iteration}") 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: if iteration == 0:
prompt_actual += ( prompt_actual = prompt_original + instruccion_fin
"\n\nIMPORTANTE:\n"
"Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, "
"di alguna de estas frases: <END>"
)
else: else:
prompt_actual = ( prompt_actual = (
f"Esta es la pregunta original:\n{prompt_original}\n\n" f"Esta es la pregunta original:\n{prompt_original}\n\n"
f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n" f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n\n"
"\n\nIMPORTANTE:\n" f"{instruccion_fin}"
"Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, "
"di alguna de estas frases: <END>"
"En caso contrario, responde a la pregunta original "
"y añade información relevante que no hayas mencionado antes.\n\n"
) )
contexto = historial + [{"role": "user", "content": prompt_actual}] 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) prompt_final = self._formatear_prompt(contexto)
print(f"📨 [generador] Prompt final enviado al modelo:\n{prompt_final}")
print("🤖 [generador] Esperando respuesta del modelo...")
respuesta = await self.modelo.responder( respuesta = await self.modelo.responder(
prompt=prompt_final, prompt=prompt_final,
system_prompt=await self.full_system_prompt, system_prompt=await self.generar_system_prompt(),
stream=stream stream=stream
) )
print("✅ [generador] Respuesta recibida")
if stream: if stream:
buffer_respuesta = "" buffer_respuesta = ""
async for token in respuesta: async for token in respuesta:
buffer_respuesta += token buffer_respuesta += token
# print(f"🔹 [stream] Token: {token}")
yield token yield token
respuesta_anterior = buffer_respuesta respuesta_anterior = buffer_respuesta
# print(f"📦 [stream] Respuesta completa:\n{respuesta_anterior}")
else: else:
respuestas.append(respuesta) respuestas.append(respuesta)
respuesta_anterior = respuesta respuesta_anterior = respuesta
# print(f"📦 [generador] Respuesta completa:\n{respuesta_anterior}")
# 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: if self.memoria:
print("💾 [memoria] Guardando turno en la memoria...")
self.memoria.guardar_turno("user", prompt_actual) self.memoria.guardar_turno("user", prompt_actual)
self.memoria.guardar_turno("assistant", respuesta_anterior) self.memoria.guardar_turno("assistant", respuesta_anterior)
self.numero_interacciones += 1 self.numero_interacciones += 1
self.updated_at = datetime.now() self.updated_at = datetime.now()
print(f"📊 [generador] Interacción #{self.numero_interacciones} registrada")
if "<end>" in respuesta_anterior.lower(): if "<end>" in respuesta_anterior.lower() and "```mcp" not in respuesta_anterior.lower():
print("🛑 [generador] Detectado <end>. Terminando bucle.")
break break
iteration += 1 iteration += 1
prompt_actual = ""
return generador() if stream else await generador_to_list(generador) return generador() if stream else await generador_to_list(generador())
+8 -4
View File
@@ -9,6 +9,7 @@ from fastmcp.client.transports import (
) )
from mcp.types import * from mcp.types import *
from fastmcp.exceptions import ClientError from fastmcp.exceptions import ClientError
import asyncio
class MCPClient: class MCPClient:
@@ -52,10 +53,13 @@ class MCPClient:
# Delegación MCP # Delegación MCP
async def call_tool( async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[TextContent | ImageContent | EmbeddedResource]:
self, name: str, arguments: dict[str, Any] | None = None try:
) -> list[TextContent | ImageContent | EmbeddedResource]: return await asyncio.wait_for(
return await self.client.call_tool(name, arguments) self.client.call_tool(name, arguments), timeout=10
)
except asyncio.TimeoutError:
raise RuntimeError(f"Timeout al ejecutar herramienta '{name}'")
async def get_prompt( async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None self, name: str, arguments: dict[str, str] | None = None
+15 -15
View File
@@ -22,35 +22,35 @@ class ClientRegistry:
def __contains__(self, name: str) -> bool: def __contains__(self, name: str) -> bool:
return name in self._clients return name in self._clients
async def listar_tools_por_cliente(self) -> dict[str, list[Any]]: async def listar_tools_por_cliente(self) -> dict[str, Any]:
resultado = {} resultado = {"tools": {}, "errores": {}}
for name, wrapper in self._clients.items(): for name, wrapper in self._clients.items():
try: try:
async with wrapper: async with wrapper:
resultado[name] = await wrapper.list_tools() resultado["tools"][name] = await wrapper.list_tools()
except Exception as e: except Exception as e:
print(f"[TOOLS] ❌ Error en '{name}': {e}") resultado["errores"][name] = str(e)
resultado[name] = [] resultado["tools"][name] = []
return resultado return resultado
async def listar_prompts_por_cliente(self) -> dict[str, list[Any]]: async def listar_prompts_por_cliente(self) -> dict[str, Any]:
resultado = {} resultado = {"prompts": {}, "errores": {}}
for name, wrapper in self._clients.items(): for name, wrapper in self._clients.items():
try: try:
async with wrapper: async with wrapper:
resultado[name] = await wrapper.list_prompts() resultado["prompts"][name] = await wrapper.list_prompts()
except Exception as e: except Exception as e:
print(f"[PROMPTS] ❌ Error en '{name}': {e}") resultado["errores"][name] = str(e)
resultado[name] = [] resultado["prompts"][name] = []
return resultado return resultado
async def listar_resources_por_cliente(self) -> dict[str, list[Any]]: async def listar_resources_por_cliente(self) -> dict[str, Any]:
resultado = {} resultado = {"resources": {}, "errores": {}}
for name, wrapper in self._clients.items(): for name, wrapper in self._clients.items():
try: try:
async with wrapper: async with wrapper:
resultado[name] = await wrapper.list_resources() resultado["resources"][name] = await wrapper.list_resources()
except Exception as e: except Exception as e:
print(f"[RESOURCES] ❌ Error en '{name}': {e}") resultado["errores"][name] = str(e)
resultado[name] = [] resultado["resources"][name] = []
return resultado 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")
+133
View File
@@ -0,0 +1,133 @@
from fastmcp import FastMCP
from pathlib import Path
import shutil
from datetime import datetime
# Directorio base seguro
SANDBOX_DIR = Path("./sandbox").resolve()
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)
def safe_path(requested_path: str) -> Path:
"""Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
# Normaliza la ruta quitando el primer '/'
normalized = requested_path.strip().lstrip("/")
full_path = (SANDBOX_DIR / normalized).resolve()
if not full_path.is_relative_to(SANDBOX_DIR):
raise ValueError("Ruta fuera del directorio permitido.")
return full_path
mcp = FastMCP()
@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
def read_file(path: str) -> str:
try:
file_path = safe_path(path)
if not file_path.is_file():
raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
return file_path.read_text(encoding="utf-8")
except Exception as e:
return f"⚠️ Error al leer archivo '{path}': {str(e)}"
@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
def write_file(path: str, content: str) -> str:
file_path = safe_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return "Archivo guardado correctamente."
@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
def delete_file(path: str) -> str:
file_path = safe_path(path)
if not file_path.is_file():
raise FileNotFoundError("Archivo no encontrado.")
file_path.unlink()
return "Archivo eliminado."
@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
def create_folder(path: str) -> str:
folder_path = safe_path(path)
folder_path.mkdir(parents=True, exist_ok=True)
return "Carpeta creada."
@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
def list_directory(path: str = ".") -> list[str]:
folder = safe_path(path)
if not folder.is_dir():
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())
@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
def tree(path: str = ".", depth: int = 3) -> str:
base = safe_path(path)
if not base.is_dir():
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
tree_output = []
def walk(dir_path: Path, prefix: str = "", level: int = 0):
if level > depth:
return
entries = sorted(dir_path.iterdir())
for i, entry in enumerate(entries):
connector = "└── " if i == len(entries) - 1 else "├── "
tree_output.append(f"{prefix}{connector}{entry.name}")
if entry.is_dir():
extension = " " if i == len(entries) - 1 else ""
walk(entry, prefix + extension, level + 1)
tree_output.append(f"{base.name}/")
walk(base)
return "\n".join(tree_output)
@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
def file_info(path: str) -> dict:
fpath = safe_path(path)
if not fpath.exists():
raise FileNotFoundError("Archivo no encontrado.")
return {
"nombre": fpath.name,
"tipo": "carpeta" if fpath.is_dir() else "archivo",
"tamaño_bytes": fpath.stat().st_size,
"última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
"relativo": str(fpath.relative_to(SANDBOX_DIR))
}
@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
def copy_file(src: str, dest: str) -> str:
src_path = safe_path(src)
dest_path = safe_path(dest)
if src_path.is_dir():
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
else:
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest_path)
return "Copia completada."
@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
def move_file(src: str, dest: str) -> str:
src_path = safe_path(src)
dest_path = safe_path(dest)
dest_path.parent.mkdir(parents=True, exist_ok=True)
src_path.rename(dest_path)
return "Movimiento completado."
@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
def clear_folder(path: str) -> str:
folder_path = safe_path(path)
if not folder_path.is_dir():
raise NotADirectoryError("La ruta no es una carpeta.")
for item in folder_path.iterdir():
if item.is_file() or item.is_symlink():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
return "Carpeta vaciada."
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")
+2 -2
View File
@@ -87,6 +87,6 @@ def is_prime(n: int) -> bool:
if __name__ == "__main__": if __name__ == "__main__":
# mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math") mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
mcp.run(transport="stdio") # mcp.run(transport="stdio")
+1
View File
@@ -66,3 +66,4 @@ def current_timestamp() -> float:
if __name__ == "__main__": if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools") mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")
+67
View File
@@ -0,0 +1,67 @@
from src.Llms.Modelos.Base_model import ModeloABC
from src.Security.GenerarIDs import GeneradorIDUnico
from typing import AsyncGenerator, Union
from src.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
+60
View File
@@ -0,0 +1,60 @@
from loguru import logger
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from src.ArquitectureLayer.Model import Model_base
from src.ConexionSql.Postgres_conexion import PostgresConexion
from src.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):
if not LoggerDB._sink_removido:
logger.remove() # 🧹 elimina impresión en terminal
LoggerDB._sink_removido = True
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
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}")
+116
View File
@@ -0,0 +1,116 @@
from typing import TYPE_CHECKING, Optional
import random
import asyncio
if TYPE_CHECKING:
from src.ScrappingWeb.Tab import Tab
class ElementoWeb:
def __init__(self, tab: "Tab", object_id: str):
self.tab = tab
self.object_id = object_id
async def scroll_into_view(self):
try:
await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
"awaitPromise": True
})
print("📜 Elemento desplazado a la vista.")
except Exception as e:
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
@classmethod
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
# Creamos un objectId a partir del nodeId usando DOM.resolveNode
cls._node_id = node_id
cls._resolved_object_id = None # Lazy resolution opcional
return cls(tab, object_id=None)
async def click(self):
try:
await self.scroll_into_view()
# Resolver objectId si es necesario
if not self.object_id and hasattr(self, "_node_id"):
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
self.object_id = resolved["object"]["objectId"]
if not self.object_id:
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
# Obtener nodeId
node_result = await self.tab._enviar("DOM.describeNode", {
"objectId": self.object_id
})
node_id = node_result["node"]["nodeId"]
# Obtener coordenadas con fallback
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
# Simular movimiento humano del mouse
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))
# Click humano
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))
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseReleased",
"x": x,
"y": y,
"button": "left",
"clickCount": 1
})
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.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { this.click(); }",
"awaitPromise": True
})
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]:
return await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").textContent')
async def escribir_texto(self, texto: str):
await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").value = "{texto}"')
+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.")
+69
View File
@@ -0,0 +1,69 @@
import aiohttp
import websockets
import json
import asyncio
from src.ScrappingWeb.Tab import Tab
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)
await tab.navegar(url, wait_time)
return tab
async def cerrar_todos(self):
for tab in list(self.tabs):
await tab.cerrar()
self.tabs.clear()
+164
View File
@@ -0,0 +1,164 @@
import asyncio
import json
import base64
import websockets
from typing import Optional
from typing import List
from src.ScrappingWeb.ElementoWeb import ElementoWeb
class Tab:
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str):
self.websocket = websocket
self.ws_url = ws_url
self._message_id = 0
self._pending = {}
self._load_event = asyncio.Event()
@classmethod
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
websocket = await websockets.connect(ws_url)
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"])
future.set_result(data.get("result"))
elif data.get("method") == "Page.loadEventFired":
self._load_event.set()
async def _enviar(self, metodo: str, parametros: Optional[dict] = None) -> 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 future
async def navegar(self, url: str, wait_time: float = 5.0):
self._load_event.clear()
print(f"🌍 Navegando a: {url}")
await self._enviar("Page.navigate", {"url": url})
try:
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
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
})
return result["result"]["value"]
except Exception as e:
print(f"⚠️ Error al ejecutar JS: {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))
print(f"📸 Screenshot guardado como {output_path}")
except Exception as e:
print(f"⚠️ Error al capturar screenshot: {e}")
async def cerrar(self):
try:
await self.websocket.close()
print("🛑 WebSocket cerrado.")
except Exception as e:
print(f"⚠️ Error al cerrar pestaña: {e}")
async def obtener_html_completo(self) -> Optional[str]:
"""
Devuelve el HTML completo de la página actual.
"""
try:
result = await self._enviar("Runtime.evaluate", {
"expression": "document.documentElement.outerHTML",
"returnByValue": True
})
html = result["result"]["value"]
print("📄 HTML completo obtenido.")
return html
except Exception as e:
print(f"⚠️ Error al obtener HTML: {e}")
return None
async def obtener_dominio(self) -> Optional[str]:
"""
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
"""
try:
dominio = await self.evaluar_js("window.location.hostname")
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:
# Obtener nodo raíz del documento
doc = await self._enviar("DOM.getDocument")
root_node_id = doc["root"]["nodeId"]
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
result = await self._enviar("DOM.querySelector", {
"nodeId": root_node_id,
"selector": selector
})
node_id = result["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"]))
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 []