Compare commits
3 Commits
develop
...
cf6a768f6b
| Author | SHA1 | Date | |
|---|---|---|---|
| cf6a768f6b | |||
| a62778a030 | |||
| 6b491a9a41 |
Generated
+53
-1
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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())
|
|
||||||
+90
-37
@@ -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,26 +141,30 @@ 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:
|
||||||
respuesta_gen = await agente2.interactuar_en_bucle(
|
prompt = input("\n📝 Escribe tu pregunta: ")
|
||||||
"¿Cuál es mi nombre de usuario en este sistema?",
|
if prompt.strip().lower() in ("salir", "exit", "quit"):
|
||||||
stream=True
|
print("\n👋 Hasta pronto.")
|
||||||
)
|
break
|
||||||
|
|
||||||
async for token in respuesta_gen:
|
print("\n💬 Respuesta en streaming:\n")
|
||||||
print(token, end="", flush=True)
|
respuesta_gen = await agente2.interactuar_en_bucle(
|
||||||
|
prompt=prompt,
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async for token in respuesta_gen:
|
||||||
|
print(token, end="", flush=True)
|
||||||
|
|
||||||
|
|
||||||
await probar_interaccion_stream()
|
await probar_interaccion_stream()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Ejecutar
|
# Ejecutar
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+23
-23
@@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -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...
|
||||||
@@ -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...
|
||||||
@@ -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...
|
||||||
@@ -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...
|
||||||
@@ -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...
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# main.py
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('Bienvenido al sistema ERP')
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# test_sample.py
|
||||||
|
|
||||||
|
def test_placeholder():
|
||||||
|
assert True
|
||||||
@@ -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")
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
+290
-122
@@ -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,219 +56,379 @@ class AgenteAI:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
async def generar_system_prompt(self) -> str:
|
||||||
async def full_system_prompt(self) -> str:
|
|
||||||
tools_str = await self._obtener_herramientas_disponibles_str()
|
|
||||||
return f"""
|
|
||||||
Eres un agente conversacional con acceso a herramientas MCP (Model Context Protocol).
|
|
||||||
|
|
||||||
Tu comportamiento sigue este flujo:
|
info = f"""Eres un agente de texto y te llamas {self.nombre}
|
||||||
|
|
||||||
1. **Piensa** para razonar tu decisión.
|
### Descripción:
|
||||||
2. **Decide** si:
|
{self.descripcion}
|
||||||
- puedes responder tú mismo,
|
|
||||||
- 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.**
|
|
||||||
|
|
||||||
---
|
### Rol:
|
||||||
|
{self.rol}
|
||||||
|
|
||||||
# Formato MCP
|
### Objetivos:
|
||||||
|
{chr(10).join(f"- {o}" for o in self.objetivos)}
|
||||||
|
|
||||||
```mcp
|
### System Prompt:
|
||||||
{{
|
{self.system_prompt}
|
||||||
"tool": "<nombre_de_la_herramienta>",
|
|
||||||
"input": {{
|
|
||||||
"clave": "valor"
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
Reglas clave:
|
|
||||||
|
|
||||||
Razonas antes de actuar.
|
Siempre estructura tus respuestas con claridad, y termina con <END> cuando hayas completado la tarea principal del usuario.
|
||||||
|
""".strip()
|
||||||
|
|
||||||
Nunca hables después de un bloque MCP.
|
return info
|
||||||
|
|
||||||
No combines respuestas y herramientas.
|
|
||||||
|
|
||||||
Piensa. Decide. Actúa.
|
|
||||||
|
|
||||||
Herramientas disponibles para usar con MCP:
|
|
||||||
{tools_str}
|
|
||||||
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def construir_prompt_usuario(self, prompt_usuario: str) -> str:
|
||||||
|
bloques = []
|
||||||
|
|
||||||
# Conseguir las herramientas disponibles
|
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:
|
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."
|
||||||
|
|
||||||
herramientas = []
|
try:
|
||||||
tools_por_cliente = await self.mcp.listar_tools_por_cliente()
|
resultado = await self.mcp.listar_tools_por_cliente()
|
||||||
|
tools_por_cliente = resultado.get("tools", {})
|
||||||
|
errores = resultado.get("errores", {})
|
||||||
|
|
||||||
for name, tools in tools_por_cliente.items():
|
logger.debug(f"Tools obtenidas: {list(tools_por_cliente.keys())}")
|
||||||
if not tools:
|
logger.debug(f"Errores detectados: {list(errores.keys())}")
|
||||||
continue
|
|
||||||
herramientas.append(f"\n🔌 Cliente: {name}")
|
herramientas = []
|
||||||
for tool in tools:
|
|
||||||
props = tool.inputSchema.get("properties", {})
|
for name, tools in tools_por_cliente.items():
|
||||||
parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items())
|
if not tools:
|
||||||
herramientas.append(f"""Nombre: {tool.name}
|
logger.info(f"Servidor {name} no tiene herramientas disponibles.")
|
||||||
Descripción: {tool.description}
|
continue
|
||||||
Parámetros:
|
|
||||||
{parametros}
|
herramientas.append(f"\n🔌 Server: {name}")
|
||||||
""")
|
for tool in tools:
|
||||||
return "\n".join(herramientas) or "No hay herramientas disponibles actualmente."
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 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:
|
return respuesta
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###----------- 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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# server_runner.py
|
||||||
|
import subprocess
|
||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
async def wait_for_port(host: str, port: int, timeout: float = 10.0):
|
||||||
|
for _ in range(int(timeout * 10)):
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=0.5):
|
||||||
|
return True
|
||||||
|
except (OSError, ConnectionRefusedError):
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
raise TimeoutError(f"No se pudo conectar al servidor en {host}:{port}")
|
||||||
|
|
||||||
|
class MCPServerRunner:
|
||||||
|
def __init__(self, server_script_path: str, python_path: str = "python"):
|
||||||
|
self.server_script_path = server_script_path
|
||||||
|
self.python_path = python_path
|
||||||
|
self.port: int = self._extraer_puerto()
|
||||||
|
self.process: subprocess.Popen | None = None
|
||||||
|
|
||||||
|
def _extraer_puerto(self) -> int:
|
||||||
|
contenido = Path(self.server_script_path).read_text(encoding="utf-8")
|
||||||
|
coincidencias = re.findall(r"port\s*=\s*(\d+)", contenido)
|
||||||
|
if not coincidencias:
|
||||||
|
raise ValueError(f"No se pudo detectar el puerto en {self.server_script_path}")
|
||||||
|
return int(coincidencias[0])
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if self.process is None or self.process.poll() is not None:
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
[self.python_path, self.server_script_path],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
await wait_for_port("127.0.0.1", self.port)
|
||||||
|
print(f"🟢 Servidor MCP iniciado en puerto {self.port}")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
if self.process and self.process.poll() is None:
|
||||||
|
self.process.terminate()
|
||||||
|
try:
|
||||||
|
self.process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.process.kill()
|
||||||
|
print("🔴 Servidor MCP detenido")
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
from fastmcp import FastMCP
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Directorio base seguro
|
||||||
|
SANDBOX_DIR = Path("./sandbox").resolve()
|
||||||
|
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def safe_path(requested_path: str) -> Path:
|
||||||
|
"""Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
|
||||||
|
# Normaliza la ruta quitando el primer '/'
|
||||||
|
normalized = requested_path.strip().lstrip("/")
|
||||||
|
full_path = (SANDBOX_DIR / normalized).resolve()
|
||||||
|
|
||||||
|
if not full_path.is_relative_to(SANDBOX_DIR):
|
||||||
|
raise ValueError("Ruta fuera del directorio permitido.")
|
||||||
|
return full_path
|
||||||
|
|
||||||
|
mcp = FastMCP()
|
||||||
|
|
||||||
|
@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
|
||||||
|
def read_file(path: str) -> str:
|
||||||
|
try:
|
||||||
|
file_path = safe_path(path)
|
||||||
|
if not file_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
|
||||||
|
return file_path.read_text(encoding="utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
return f"⚠️ Error al leer archivo '{path}': {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
|
||||||
|
def write_file(path: str, content: str) -> str:
|
||||||
|
file_path = safe_path(path)
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
file_path.write_text(content, encoding="utf-8")
|
||||||
|
return "Archivo guardado correctamente."
|
||||||
|
|
||||||
|
@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
|
||||||
|
def delete_file(path: str) -> str:
|
||||||
|
file_path = safe_path(path)
|
||||||
|
if not file_path.is_file():
|
||||||
|
raise FileNotFoundError("Archivo no encontrado.")
|
||||||
|
file_path.unlink()
|
||||||
|
return "Archivo eliminado."
|
||||||
|
|
||||||
|
@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
|
||||||
|
def create_folder(path: str) -> str:
|
||||||
|
folder_path = safe_path(path)
|
||||||
|
folder_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return "Carpeta creada."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
|
||||||
|
def list_directory(path: str = ".") -> list[str]:
|
||||||
|
folder = safe_path(path)
|
||||||
|
if not folder.is_dir():
|
||||||
|
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||||
|
return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())
|
||||||
|
|
||||||
|
@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
|
||||||
|
def tree(path: str = ".", depth: int = 3) -> str:
|
||||||
|
base = safe_path(path)
|
||||||
|
if not base.is_dir():
|
||||||
|
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||||
|
|
||||||
|
tree_output = []
|
||||||
|
|
||||||
|
def walk(dir_path: Path, prefix: str = "", level: int = 0):
|
||||||
|
if level > depth:
|
||||||
|
return
|
||||||
|
entries = sorted(dir_path.iterdir())
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
connector = "└── " if i == len(entries) - 1 else "├── "
|
||||||
|
tree_output.append(f"{prefix}{connector}{entry.name}")
|
||||||
|
if entry.is_dir():
|
||||||
|
extension = " " if i == len(entries) - 1 else "│ "
|
||||||
|
walk(entry, prefix + extension, level + 1)
|
||||||
|
|
||||||
|
tree_output.append(f"{base.name}/")
|
||||||
|
walk(base)
|
||||||
|
return "\n".join(tree_output)
|
||||||
|
|
||||||
|
@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
|
||||||
|
def file_info(path: str) -> dict:
|
||||||
|
fpath = safe_path(path)
|
||||||
|
if not fpath.exists():
|
||||||
|
raise FileNotFoundError("Archivo no encontrado.")
|
||||||
|
return {
|
||||||
|
"nombre": fpath.name,
|
||||||
|
"tipo": "carpeta" if fpath.is_dir() else "archivo",
|
||||||
|
"tamaño_bytes": fpath.stat().st_size,
|
||||||
|
"última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
|
||||||
|
"relativo": str(fpath.relative_to(SANDBOX_DIR))
|
||||||
|
}
|
||||||
|
|
||||||
|
@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
|
||||||
|
def copy_file(src: str, dest: str) -> str:
|
||||||
|
src_path = safe_path(src)
|
||||||
|
dest_path = safe_path(dest)
|
||||||
|
if src_path.is_dir():
|
||||||
|
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
|
||||||
|
else:
|
||||||
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src_path, dest_path)
|
||||||
|
return "Copia completada."
|
||||||
|
|
||||||
|
@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
|
||||||
|
def move_file(src: str, dest: str) -> str:
|
||||||
|
src_path = safe_path(src)
|
||||||
|
dest_path = safe_path(dest)
|
||||||
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
src_path.rename(dest_path)
|
||||||
|
return "Movimiento completado."
|
||||||
|
|
||||||
|
@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
|
||||||
|
def clear_folder(path: str) -> str:
|
||||||
|
folder_path = safe_path(path)
|
||||||
|
if not folder_path.is_dir():
|
||||||
|
raise NotADirectoryError("La ruta no es una carpeta.")
|
||||||
|
for item in folder_path.iterdir():
|
||||||
|
if item.is_file() or item.is_symlink():
|
||||||
|
item.unlink()
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.rmtree(item)
|
||||||
|
return "Carpeta vaciada."
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")
|
||||||
|
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}")
|
||||||
@@ -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}"')
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class Navegador:
|
||||||
|
def __init__(self,
|
||||||
|
chrome_path: str,
|
||||||
|
user_data_dir: str,
|
||||||
|
id: Optional[int] = None,
|
||||||
|
download_dir: Optional[str] = None,
|
||||||
|
debugging_port: int = 9222,
|
||||||
|
headless: bool = False,
|
||||||
|
user_agent: Optional[str] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"):
|
||||||
|
self.chrome_path = chrome_path
|
||||||
|
self.user_data_dir = user_data_dir
|
||||||
|
self.id = id
|
||||||
|
self.download_dir = download_dir or os.path.join(self.user_data_dir, "downloads")
|
||||||
|
self.debugging_port = debugging_port
|
||||||
|
self.headless = headless
|
||||||
|
self.user_agent = user_agent
|
||||||
|
self.chrome_process: Optional[subprocess.Popen] = None
|
||||||
|
|
||||||
|
async def _esperar_debugger(self, timeout=10):
|
||||||
|
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||||
|
for _ in range(timeout * 10): # 10 intentos por segundo
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
print("✅ Chrome listo para debugging.")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
raise RuntimeError("❌ Chrome no respondió en el puerto de debugging.")
|
||||||
|
|
||||||
|
def _preconfigurar_preferencias(self):
|
||||||
|
prefs_path = os.path.join(self.user_data_dir, "Default", "Preferences")
|
||||||
|
os.makedirs(os.path.dirname(prefs_path), exist_ok=True)
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
prefs = {
|
||||||
|
"profile": {
|
||||||
|
"exit_type": "Normal",
|
||||||
|
"exited_cleanly": True
|
||||||
|
},
|
||||||
|
"browser": {
|
||||||
|
"has_seen_welcome_page": True
|
||||||
|
},
|
||||||
|
"distribution": {
|
||||||
|
"skip_first_run_ui": True
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"default_directory": self.download_dir,
|
||||||
|
"prompt_for_download": False,
|
||||||
|
"directory_upgrade": True,
|
||||||
|
"extensions_to_open": ""
|
||||||
|
},
|
||||||
|
"savefile": {
|
||||||
|
"default_directory": self.download_dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.path.exists(prefs_path):
|
||||||
|
try:
|
||||||
|
with open(prefs_path, "r", encoding="utf-8") as f:
|
||||||
|
existing = json.load(f)
|
||||||
|
existing.update(prefs)
|
||||||
|
prefs = existing
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open(prefs_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(prefs, f, indent=2)
|
||||||
|
|
||||||
|
def _build_args(self):
|
||||||
|
os.makedirs(self.user_data_dir, exist_ok=True)
|
||||||
|
self._preconfigurar_preferencias()
|
||||||
|
|
||||||
|
args = [
|
||||||
|
f"--remote-debugging-port={self.debugging_port}",
|
||||||
|
f"--user-data-dir={self.user_data_dir}",
|
||||||
|
"--disable-blink-features=AutomationControlled",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-web-security",
|
||||||
|
# "--disable-extensions",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-infobars",
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
"--disable-default-apps",
|
||||||
|
"--mute-audio",
|
||||||
|
"--window-size=1024,1024",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--disable-features=DefaultBrowserPrompt",
|
||||||
|
"--disable-component-update",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--disable-sync",
|
||||||
|
"--disable-translate",
|
||||||
|
"--disable-background-timer-throttling",
|
||||||
|
"--disable-client-side-phishing-detection",
|
||||||
|
"--disable-component-extensions-with-background-pages",
|
||||||
|
"--metrics-recording-only",
|
||||||
|
"--safebrowsing-disable-auto-update",
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.headless:
|
||||||
|
args.append("--headless=new")
|
||||||
|
|
||||||
|
if self.user_agent:
|
||||||
|
args.append(f"--user-agent={self.user_agent}")
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def inyectar_spoof_chrome(self):
|
||||||
|
script = """
|
||||||
|
window.chrome = {
|
||||||
|
app: {
|
||||||
|
isInstalled: false,
|
||||||
|
InstallState: {
|
||||||
|
DISABLED: 'disabled',
|
||||||
|
INSTALLED: 'installed',
|
||||||
|
NOT_INSTALLED: 'not_installed'
|
||||||
|
},
|
||||||
|
RunningState: {
|
||||||
|
CANNOT_RUN: 'cannot_run',
|
||||||
|
READY_TO_RUN: 'ready_to_run',
|
||||||
|
RUNNING: 'running'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
|
||||||
|
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||||
|
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||||
|
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
|
||||||
|
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
|
||||||
|
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
targets = await resp.json()
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
if "webSocketDebuggerUrl" not in target:
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_id = target["id"]
|
||||||
|
async with session.post(
|
||||||
|
f"http://127.0.0.1:{self.debugging_port}/json/protocol",
|
||||||
|
json={"targetId": target_id}
|
||||||
|
):
|
||||||
|
pass # CDP protocol fetch optional
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"http://127.0.0.1:{self.debugging_port}/json/send",
|
||||||
|
json={
|
||||||
|
"id": 1,
|
||||||
|
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||||
|
"params": {"source": script}
|
||||||
|
}
|
||||||
|
) as inject_resp:
|
||||||
|
if inject_resp.status == 200:
|
||||||
|
print("✅ chrome.* spoof inyectado.")
|
||||||
|
|
||||||
|
|
||||||
|
async def iniciar(self):
|
||||||
|
args = self._build_args()
|
||||||
|
self.chrome_process = subprocess.Popen([self.chrome_path] + args)
|
||||||
|
print(f"Chrome iniciado (headless={self.headless}). Esperando disponibilidad del debugger...")
|
||||||
|
await self._esperar_debugger()
|
||||||
|
await self.inyectar_spoof_chrome()
|
||||||
|
|
||||||
|
async def cerrar(self):
|
||||||
|
if self.chrome_process and self.chrome_process.poll() is None:
|
||||||
|
self.chrome_process.terminate()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.to_thread(self.chrome_process.wait), timeout=5)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
self.chrome_process.kill()
|
||||||
|
print("🛑 Chrome cerrado correctamente.")
|
||||||
@@ -0,0 +1,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()
|
||||||
@@ -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 []
|
||||||
Reference in New Issue
Block a user