openprompt-lang 1.2.7 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ # Web Scraper Evolution — Plan Ollama-Driven
2
+
3
+ > **Versión:** 2.0 (revisado)
4
+ > **Objetivo:** Reemplazar `opl webscrape` (Readability.js + Turndown) por un extractor
5
+ > **inteligente asistido por Ollama** que analiza la estructura de cada página y genera
6
+ > un plan de extracción personalizado.
7
+ >
8
+ > **Stack:** 100% Node.js + Ollama (local) → Sin dependencia de Python.
9
+ > **Principio:** Cada página es única → cada página merece un plan de extracción único.
10
+
11
+ ---
12
+
13
+ ## 1. Filosofía: Por qué IA en vez de Reglas Fijas
14
+
15
+ ### Enfoque tradicional (hardcoded)
16
+ ```python
17
+ # Reglas fijas que fallan en el 50% de las páginas
18
+ extract_og_tags(html)
19
+ extract_metadata(html)
20
+ extract_articles(html)
21
+ extract_tables(html)
22
+ ```
23
+ ❌ Cada sitio tiene markup distinto → reglas imposibles de mantener
24
+ ❌ No entiende el **significado** de las secciones
25
+ ❌ No distingue contenido principal de navegación/publicidad
26
+ ❌ Cada nuevo tipo de página requiere más reglas
27
+
28
+ ### Enfoque Ollama-driven
29
+ ```
30
+ fetch(url) → HTML + layout estructurado
31
+
32
+ 🧠 Ollama analiza: "¿qué hay aquí?"
33
+
34
+ ← plan de extracción JSON ←
35
+
36
+ Ejecuta el plan (selectores dinámicos)
37
+
38
+ knowledge/<domain>/<slug>/
39
+ ```
40
+ ✅ Se adapta a cualquier página
41
+ ✅ Entiende semántica del contenido
42
+ ✅ Decide QUÉ extraer según el tipo de página
43
+ ✅ Sin reglas hardcodeadas — el LLM decide
44
+
45
+ ---
46
+
47
+ ## 2. Arquitectura General
48
+
49
+ ```
50
+ ┌──────────────────────────────────────────────────────────────────┐
51
+ │ Input │
52
+ │ url → fetchPage(url) │
53
+ │ ├── simple: node-fetch (rápido, HTML estático) │
54
+ │ └── headless: @playwright (SPA, JS-heavy, lazy-load) │
55
+ │ │
56
+ │ Resultado: PageSnapshot { html, text, links, meta, ...} │
57
+ └──────────────────┬───────────────────────────────────────────────┘
58
+
59
+ ┌──────────────────┴───────────────────────────────────────────────┐
60
+ │ PASO 1: 🧠 Escaneo con Ollama (analyzePage) │
61
+ │ │
62
+ │ Envía a Ollama: │
63
+ │ - Título de la página │
64
+ │ - Meta tags básicos │
65
+ │ - HTML estructural resumido (árbol de secciones, no el HTML │
66
+ │ completo — eso sería demasiado tokens) │
67
+ │ - Texto visible (primeros ~2000 chars) │
68
+ │ - Cantidad y tipos de elementos (cuántos <article>, <section>, │
69
+ │ <table>, <img>, <form>, cards, listas, etc.) │
70
+ │ │
71
+ │ Ollama responde: │
72
+ │ { │
73
+ │ pageType: "article" | "product" | "docs" | "blog" | │
74
+ │ "catalog" | "landing" | "profile" | "generic", │
75
+ │ title: "Título detectado", │
76
+ │ description: "Descripción detectada", │
77
+ │ contentBlocks: [ │
78
+ │ { │
79
+ │ type: "main-content" | "sidebar" | "nav" | "footer" | │
80
+ │ "cards" | "table" | "form" | "gallery", │
81
+ │ selector: "main article", ← selector CSS sugerido │
82
+ │ importance: 1-10, ← qué tan importante │
83
+ │ extractMode: "markdown" | "json" | "table" | "cards", │
84
+ │ description: "El artículo principal con su contenido" │
85
+ │ } │
86
+ │ ], │
87
+ │ metadata: { │
88
+ │ og: true, twitter: true, jsonld: true, microdata: false │
89
+ │ }, │
90
+ │ specialFeatures: ["scroll-infinity", "load-more", │
91
+ │ "pagination", "tabs", "accordion"], │
92
+ │ extractionPlan: { │
93
+ │ steps: ["extract-main-content", "extract-og", │
94
+ │ "extract-images", "extract-tables"], │
95
+ │ outputFormat: "markdown+json" │
96
+ │ } │
97
+ │ } │
98
+ └──────────────────┬───────────────────────────────────────────────┘
99
+
100
+ ┌──────────────────┴───────────────────────────────────────────────┐
101
+ │ PASO 2: Ejecutar el plan (executePlan) │
102
+ │ │
103
+ │ Según el plan de Ollama, ejecuta SOLO los extractores │
104
+ │ necesarios para esta página: │
105
+ │ │
106
+ │ ├── main-content: Readability.js + limpieza │
107
+ │ ├── og/twitter/jsonld: extractores específicos (jsdom) │
108
+ │ ├── images: src, alt, caption, dimensiones │
109
+ │ ├── tables: caption + headers + rows (markdown/JSON) │
110
+ │ ├── cards: tarjetas de producto/artículo/post │
111
+ │ ├── forms: action, method, inputs │
112
+ │ └── special: auto-scroll, load-more, pagination │
113
+ │ │
114
+ │ Resultado: ExtractedPage { ... } │
115
+ └──────────────────┬───────────────────────────────────────────────┘
116
+
117
+ ┌──────────────────┴───────────────────────────────────────────────┐
118
+ │ PASO 3: Post-procesamiento (postProcess) │
119
+ │ │
120
+ │ ├── Convertir a markdown estructurado │
121
+ │ ├── Clasificar contenido (dominio OPL) │
122
+ │ ├── Generar resumen con Ollama (opcional) │
123
+ │ ├── Generar embeddings vía pipeline existente │
124
+ │ └── Guardar en knowledge/<domain>/<slug>/ │
125
+ │ ├── page.md ← markdown completo │
126
+ │ ├── metadata.json ← datos estructurados │
127
+ │ ├── plan.json ← el plan que usó Ollama │
128
+ │ └── embeddings.json ← vectores │
129
+ └──────────────────────────────────────────────────────────────────┘
130
+ ```
131
+
132
+ ---
133
+
134
+ ## 3. Componentes a Implementar
135
+
136
+ ### 3.1 Core (`src/core/webscrape/`)
137
+
138
+ | Archivo | Descripción | Deps |
139
+ |---------|-------------|------|
140
+ | `analyzer.js` | Envía estructura de página a Ollama, recibe plan JSON | `@xenova/transformers`? No, es puro Ollama API |
141
+ | `executor.js` | Ejecuta el plan de extracción paso a paso | `jsdom` |
142
+ | `fetcher.js` | Fetch de página (simple + headless) | `node-fetch`, `playwright` (opt) |
143
+ | `models.js` | PageSnapshot, ExtractPlan, ExtractedPage, PageType | — |
144
+ | `postprocessor.js` | Markdown, metadata, embeddings, guardado | — |
145
+ | `config.js` | URL de Ollama, timeouts, modelos, cache | — |
146
+
147
+ ### 3.2 Extractores (`src/core/webscrape/extractors/`)
148
+
149
+ | Archivo | Descripción |
150
+ |---------|-------------|
151
+ | `extract-content.js` | Extrae contenido principal (Readability.js mejorado) |
152
+ | `extract-og.js` | Open Graph + Twitter Cards |
153
+ | `extract-jsonld.js` | JSON-LD structured data |
154
+ | `extract-microdata.js` | Microdata / schema.org |
155
+ | `extract-images.js` | Imágenes con alt, caption, dimensiones |
156
+ | `extract-tables.js` | Tablas con caption, headers, rows |
157
+ | `extract-cards.js` | Tarjetas de producto/artículo/post |
158
+ | `extract-forms.js` | Formularios con campos |
159
+ | `extract-special.js` | Load-more, paginación, scroll infinito |
160
+
161
+ ### 3.3 CLI (`src/commands/opl-webscrape.js`)
162
+
163
+ ```bash
164
+ opl webscrape <url> # Modo normal (Ollama analyze + execute)
165
+ opl webscrape <url> --plan-only # Solo mostrar el plan de Ollama, no ejecutar
166
+ opl webscrape <url> --plan <plan.json> # Usar un plan guardado (re-ejecutar)
167
+ opl webscrape <url> --simple # Forzar Readability.js (sin Ollama)
168
+ opl webscrape <url> --headless # Forzar Playwright (para SPAs)
169
+ opl webscrape <url> --output json # Salida JSON estructurada
170
+ opl webscrape <url> --no-embeddings # No generar embeddings
171
+ opl webscrape <url> --dry-run # Mostrar plan sin guardar
172
+ opl webscrape plan <url> # Shorthand para --plan-only
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 4. Prompt de Análisis (Ollama)
178
+
179
+ El prompt que se envía a Ollama es **la pieza clave**. Debe ser:
180
+
181
+ - Compacto (no enviar HTML completo — solo resumen estructural)
182
+ - Rico en señales semánticas (tipo de elementos, no su contenido)
183
+ - Determinista en la respuesta (JSON estructurado)
184
+
185
+ ### Formato del prompt
186
+
187
+ ```
188
+ Eres un analista de páginas web. Tu trabajo es examinar la estructura
189
+ de una página web y generar un plan de extracción.
190
+
191
+ ## Información de la página
192
+ URL: {url}
193
+ Título: {title}
194
+ Meta description: {description}
195
+ Tamaño HTML: {htmlSize} chars
196
+
197
+ ## Resumen estructural
198
+ - Tipo de elementos en la página:
199
+ {summary: article: 3, section: 5, nav: 1, main: 1, aside: 2,
200
+ footer: 1, table: 1, form: 0, img: 12, video: 0, article_card: 6,
201
+ list: 4, breadcrumb: 0}
202
+ - Primeros 2000 caracteres del texto visible:
203
+ "{truncatedText}"
204
+
205
+ ## Enlaces principales (primeros 10)
206
+ {links: [
207
+ {href: "/articulo-1", text: "Cómo hacer X", type: "internal"},
208
+ {href: "/categoria/y", text: "Categoría Y", type: "internal"},
209
+ ...
210
+ ]}
211
+
212
+ ## Meta tags encontrados
213
+ {meta: {
214
+ og: ["og:title", "og:description", "og:image"],
215
+ twitter: ["twitter:card", "twitter:site"],
216
+ jsonld: true,
217
+ microdata: false
218
+ }}
219
+
220
+ ## Instrucciones
221
+ Analiza esta página y devuelve un JSON con:
222
+ 1. pageType: qué tipo de página es
223
+ 2. contentBlocks: qué bloques de contenido identificas y cómo extraerlos
224
+ 3. extractionPlan: pasos concretos a seguir
225
+ 4. metadata: qué meta tags extraer
226
+ 5. specialFeatures: si hay scroll infinito, load-more, paginación, etc.
227
+
228
+ Responde ÚNICAMENTE con el JSON. No incluyas explicaciones.
229
+ ```
230
+
231
+ ### Respuesta esperada
232
+
233
+ ```json
234
+ {
235
+ "pageType": "article",
236
+ "confidence": 0.92,
237
+ "title": "Cómo implementar un scraper con IA",
238
+ "description": "Guía paso a paso para construir un extractor web inteligente",
239
+ "contentBlocks": [
240
+ {
241
+ "type": "main-content",
242
+ "selector": "main article",
243
+ "importance": 10,
244
+ "extractMode": "markdown",
245
+ "description": "Artículo principal con contenido completo"
246
+ },
247
+ {
248
+ "type": "sidebar",
249
+ "selector": "aside.related-posts",
250
+ "importance": 3,
251
+ "extractMode": "links",
252
+ "description": "Artículos relacionados en la barra lateral"
253
+ }
254
+ ],
255
+ "metadata": {
256
+ "og": true,
257
+ "twitter": true,
258
+ "jsonld": true,
259
+ "microdata": false,
260
+ "priority": ["og:title", "og:description", "og:image"]
261
+ },
262
+ "specialFeatures": [],
263
+ "extractionPlan": {
264
+ "steps": [
265
+ "extract-main-content",
266
+ "extract-og",
267
+ "extract-jsonld",
268
+ "extract-images"
269
+ ],
270
+ "outputFormat": "markdown+json",
271
+ "notes": "Página de blog estándar con contenido principal rico en imágenes"
272
+ }
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 5. Integración con el Sistema OPL Existente
279
+
280
+ ### Pipeline completo con embeddings
281
+
282
+ ```
283
+ cli: opl webscrape https://ejemplo.com/articulo
284
+
285
+ fetcher.fetch(url) → PageSnapshot { html, text, links, meta, elementCounts }
286
+
287
+ analyzer.analyze(snapshot, ollama) → ExtractPlan { pageType, blocks, steps }
288
+
289
+ executor.execute(html, plan) → ExtractedPage { content, metadata, media, ... }
290
+
291
+ postprocessor.process(extracted) → { markdown, metadata, cards }
292
+
293
+ → knowledge/<domain>/<slug>/page.md
294
+ → knowledge/<domain>/<slug>/metadata.json
295
+ → knowledge/<domain>/<slug>/plan.json
296
+ → knowledge/<domain>/<slug>/cards.json (si aplica)
297
+
298
+ embedder.indexDocument(slug) ← pipeline de embeddings existente
299
+ ```
300
+
301
+ ### Reutilización de infraestructura existente
302
+
303
+ | Componente OPL existente | Se reutiliza |
304
+ |--------------------------|:------------:|
305
+ | `src/embeddings/pipeline.js` | ✅ Indexación automática |
306
+ | `src/embeddings/sqlite-store.js` | ✅ Cache + almacenamiento |
307
+ | `src/commands/opl-webscrape.js` | ✅ Refactorizado como entry point |
308
+ | `src/cli/commands-opl.js` | ✅ Flags nuevos (`--plan-only`, `--headless`) |
309
+ | Readability.js + Turndown | ✅ Como extractores de contenido (PASO 2) |
310
+ | `opl knowledge` | ✅ Integración con knowledge base |
311
+
312
+ ---
313
+
314
+ ## 6. Estrategia de Implementación por Fases
315
+
316
+ | Fase | Qué se hace | Archivos | Esfuerzo |
317
+ |------|-------------|----------|:--------:|
318
+ | **F0** | Models + Fetcher | `models.js`, `fetcher.js`, `config.js` | 1 día |
319
+ | **F1** | **Analyzer (Ollama) — el corazón** | `analyzer.js`, prompt template | 2-3 días |
320
+ | **F2** | Executor básico (main-content + og + jsonld) | `executor.js`, `extract-content.js`, `extract-og.js`, `extract-jsonld.js` | 3-4 días |
321
+ | **F3** | Extractores avanzados (images, tables, cards, forms) | `extract-images.js`, `extract-tables.js`, `extract-cards.js`, `extract-forms.js` | 3-4 días |
322
+ | **F4** | Post-processor + integración knowledge/embeddings | `postprocessor.js`, refactor `opl-webscrape.js` | 2 días |
323
+ | **F5** | Headless browser (Playwright opcional) | `fetcher.js` (modo headless), `extract-special.js` | 2 días |
324
+ | **F6** | Tests + documentación + CLI flags | tests, docs, `commands-opl.js` | 2 días |
325
+
326
+ **Total estimado:** 2-3 semanas (trabajo parcial, OLLAMA-first)
327
+
328
+ ---
329
+
330
+ ## 7. Beneficios Clave del Enfoque IA
331
+
332
+ | Aspecto | Readability.js | Scraper Python hardcoded | **Ollama-driven (esta propuesta)** |
333
+ |---------|:--------------:|:------------------------:|:----------------------------------:|
334
+ | **Adaptabilidad** | Solo artículos | Media (reglas fijas) | **Alta (analiza cada página)** |
335
+ | **Clasificación** | No | Heurística | **IA entiende el tipo** |
336
+ | **Metadata** | Mínima | Completa | **Selectiva (solo lo relevante)** |
337
+ | **SPA/JS** | No | Sí (Playwright) | **Sí (Playwright opcional)** |
338
+ | **Mantenimiento** | Nulo | Alto (nuevas reglas) | **Bajo (mejora el prompt)** |
339
+ | **Precisión** | 60-70% | 70-85% | **85-95% (IA entiende contexto)** |
340
+ | **Cobertura** | Blogs/news | Sitios conocidos | **Cualquier página web** |
341
+ | **Dependencias** | Mínimas | Python + librerías | **Node.js + Ollama** |
342
+
343
+ ---
344
+
345
+ ## 8. Riesgos y Mitigaciones
346
+
347
+ | Riesgo | Probabilidad | Impacto | Mitigación |
348
+ |--------|:------------:|:-------:|------------|
349
+ | Ollama no disponible | Alta | Alto | Fallback automático a Readability.js + Turndown |
350
+ | Prompts largos (>4K tokens) | Media | Medio | Resumir HTML estructuralmente (árbol, no HTML crudo) |
351
+ | Respuesta JSON malformada de Ollama | Media | Medio | Validar JSON + reintentar con `--strict` |
352
+ | Páginas con anti-bot | Media | Alto | Modo headless + User-Agent rotación |
353
+ | Latencia (Ollama analyse = 2-5s) | Alta | Bajo | Cache de planes por dominio + extracción async |
354
+ | Precisión del plan depende del modelo | Alta | Medio | Usar modelos >= 7B (phi-4, llama3.2, qwen2.5) |
355
+
356
+ ---
357
+
358
+ ## 9. Comandos Futuros (Post-F6)
359
+
360
+ ```bash
361
+ # Extracción básica con IA
362
+ opl webscrape https://ejemplo.com
363
+
364
+ # Solo ver el plan que Ollama generaría (sin extraer)
365
+ opl webscrape https://ejemplo.com --plan-only
366
+
367
+ # Re-ejecutar con un plan guardado
368
+ opl webscrape https://ejemplo.com --plan ./plan.json
369
+
370
+ # Crawl completo de un sitio con IA por página
371
+ opl webscrape https://ejemplo.com --crawl --depth 2
372
+
373
+ # Forzar Playwright para SPAs pesados
374
+ opl webscrape https://spa-ejemplo.com --headless
375
+
376
+ # Salida JSON para pipeline
377
+ opl webscrape https://ejemplo.com --output json
378
+
379
+ # Forzar Readability.js (sin IA)
380
+ opl webscrape https://ejemplo.com --simple
381
+
382
+ # Extraer diseño visual (colores, tipografía, componentes)
383
+ opl webscrape https://ejemplo.com --extract-design
384
+
385
+ # Ver el prompt que se envía a Ollama
386
+ opl webscrape https://ejemplo.com --debug-prompt
387
+ ```
388
+
389
+ ---
390
+
391
+ ## 10. Primeros Pasos Concretos
392
+
393
+ ### Fase 0: Preparación
394
+
395
+ 1. Verificar que Ollama corre localmente:
396
+ ```bash
397
+ curl http://localhost:11434/api/tags
398
+ ```
399
+
400
+ 2. Elegir modelo base (recomendado: `phi-4:14b` o `llama3.2:8b`):
401
+ ```bash
402
+ ollama pull phi-4:14b # Buen balance calidad/velocidad
403
+ ```
404
+
405
+ 3. Crear estructura de directorios:
406
+ ```
407
+ src/core/webscrape/
408
+ ├── index.js # Re-export
409
+ ├── config.js # Ollama URL, modelo, timeouts
410
+ ├── models.js # PageSnapshot, ExtractPlan, ExtractedPage
411
+ ├── fetcher.js # node-fetch + Playwright opcional
412
+ ├── analyzer.js # Ollama prompt + parseo JSON
413
+ ├── executor.js # Ejecuta plan paso a paso
414
+ ├── postprocessor.js # Markdown + metadata + guardado
415
+ └── extractors/
416
+ ├── extract-content.js
417
+ ├── extract-og.js
418
+ ├── extract-jsonld.js
419
+ ├── extract-images.js
420
+ ├── extract-tables.js
421
+ └── ...
422
+ ```
423
+
424
+ 4. Implementar `models.js` + `config.js` + `fetcher.js` (F0, 1 día)
425
+
426
+ ### Fase 1: Analyzer (el corazón)
427
+
428
+ 5. Implementar `analyzer.js`:
429
+ - Construir resumen estructural del HTML (árbol de secciones, conteo de elementos)
430
+ - Extraer texto visible (primeros ~2000 chars)
431
+ - Detectar meta tags OG, Twitter, JSON-LD
432
+ - Enviar prompt a Ollama (`POST /api/generate`)
433
+ - Parsear y validar respuesta JSON
434
+ - Cachear plan por URL en SQLite
435
+
436
+ 6. Probar con 5 tipos de página distintos (article, product, docs, blog, landing)
437
+
438
+ ### Fase 2: Executor básico
439
+
440
+ 7. Implementar extractores core + `executor.js`
441
+ 8. Probar end-to-end con `opl webscrape <url>`
442
+
443
+ ---
444
+
445
+ ## 11. Medición de Éxito
446
+
447
+ | Métrica | Objetivo | Cómo se mide |
448
+ |---------|:--------:|--------------|
449
+ | Precisión del plan | >90% | Validación manual de 50 páginas |
450
+ | Tiempo total por URL | <10s | Benchmark automático |
451
+ | Fallback a Readability | <20% de los casos | Log de modo usado |
452
+ | Tokens por análisis | <3000 | Prompt logging |
453
+ | Cobertura de tipos de página | >80% | Test con 100 URLs diversas |
454
+ | Embeddings generados por scraper | >90% de URLs | Pipeline count |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openprompt-lang",
3
- "version": "1.2.7",
3
+ "version": "1.3.0",
4
4
  "description": "PromptLang CLI — Context Engine de anotaciones para desarrollo asistido por IA",
5
5
  "type": "module",
6
6
  "main": "./bin/cli.js",
@@ -72,14 +72,17 @@
72
72
  "node": ">=18"
73
73
  },
74
74
  "dependencies": {
75
+ "@mozilla/readability": "^0.6.0",
75
76
  "ajv": "^8.20.0",
76
77
  "better-sqlite3": "^12.10.0",
77
78
  "chalk": "^5.3.0",
78
79
  "commander": "^12.0.0",
79
80
  "handlebars": "^4.7.9",
80
81
  "inquirer": "^9.3.0",
82
+ "jsdom": "^29.1.1",
81
83
  "pdf-parse": "^2.4.5",
82
84
  "tesseract.js": "^7.0.0",
85
+ "turndown": "^7.2.4",
83
86
  "typescript": "^6.0.3"
84
87
  },
85
88
  "devDependencies": {
@@ -105,5 +108,8 @@
105
108
  "*.{ts,tsx,js,jsx}": [
106
109
  "openprompt-lang lint-file"
107
110
  ]
111
+ },
112
+ "optionalDependencies": {
113
+ "@xenova/transformers": "^2.17.2"
108
114
  }
109
115
  }
@@ -90,6 +90,43 @@ try {
90
90
  console.log(" 💡 O instálalo manualmente según tu plataforma.")
91
91
  }
92
92
  }
93
+ // ──────────────────────────────────────────────
94
+ // Información sobre embeddings y Ollama
95
+ // ──────────────────────────────────────────────
96
+ try {
97
+ const { execSync } = await import("child_process")
98
+ const ollamaPath = execSync("which ollama 2>/dev/null || echo ''", {
99
+ encoding: "utf-8",
100
+ }).trim()
101
+
102
+ if (ollamaPath) {
103
+ const models = execSync("ollama list 2>/dev/null || echo ''", {
104
+ encoding: "utf-8",
105
+ }).trim()
106
+ const hasEmbedModel = models.toLowerCase().includes("nomic-embed-text")
107
+
108
+ if (hasEmbedModel) {
109
+ console.log("✅ Ollama + nomic-embed-text listos para embeddings semánticos")
110
+ } else {
111
+ console.log("📦 Ollama detectado. Para embeddings semánticos ejecuta:")
112
+ console.log(" ollama pull nomic-embed-text")
113
+ }
114
+ } else {
115
+ console.log("📦 Ollama no detectado. Usando Transformers.js como fallback.")
116
+ console.log(" 💡 Instala Ollama para mejor rendimiento: https://ollama.com")
117
+ console.log(" 💡 Luego: ollama pull nomic-embed-text")
118
+ }
119
+ } catch {
120
+ // No crítico
121
+ }
122
+
123
+ console.log("")
124
+ console.log("🔍 Búsqueda semántica disponible:")
125
+ console.log(' opl search "consulta" --mode vector')
126
+ console.log("")
127
+ console.log("📊 Indexa documentos en el vector store:")
128
+ console.log(" opl embeddings index <docId>")
129
+ console.log("")
93
130
  console.log("💡 Ejecuta 'opl init' para crear un nuevo proyecto con planificación")
94
131
  } catch (err) {
95
132
  console.log(` ⚠️ opencode: ${err.message}`)
@@ -23,6 +23,7 @@ export function register(program) {
23
23
  .option("--auto-ocr", "Detectar automáticamente si necesita OCR (baja densidad de texto)")
24
24
  .option("--force", "Re-procesar PDF aunque ya exista en el índice")
25
25
  .option("--verbose", "Mostrar progreso detallado (especialmente OCR)")
26
+ .option("--no-embed", "Saltar la generación automática de embeddings")
26
27
  .action(async (file, options) => {
27
28
  const { ingest } = await import("../commands/knowledge.js")
28
29
  await ingest(file, options)
@@ -61,7 +61,7 @@ export function register(program) {
61
61
  .command("search <query>")
62
62
  .description("Buscar en el conocimiento: tags + texto completo + semántica")
63
63
  .usage("<consulta>")
64
- .option("--mode <mode>", "Modo de búsqueda: tags, fulltext, semantic, hybrid (default)")
64
+ .option("--mode <mode>", "Modo de búsqueda: tags, fulltext, semantic, vector, hybrid (default)")
65
65
  .action(async (query, options) => {
66
66
  const { search } = await import("../commands/opl-search.js")
67
67
  await search(query, options)
@@ -249,6 +249,84 @@ export function register(program) {
249
249
  await contractSearch(query)
250
250
  })
251
251
 
252
+ // ──────────────────────────────────────────────
253
+ // opl embeddings — Gestión de vectores de embeddings
254
+ // ──────────────────────────────────────────────
255
+ const embeddings = program
256
+ .command("embeddings")
257
+ .description("Gestionar embeddings vectoriales: indexar, consultar estado, eliminar")
258
+
259
+ embeddings
260
+ .command("index [docId]")
261
+ .description("Indexar un documento: chunking → embedding → almacenamiento vectorial")
262
+ .usage("<docId>")
263
+ .option("--strategy <s>", "Estrategia de chunking: section, paragraph, fixed", "section")
264
+ .option("--provider <p>", "Proveedor de embeddings: ollama, transformers")
265
+ .option("--dry-run", "Simular indexación sin persistir")
266
+ .option("--resume", "Saltar chunks ya indexados")
267
+ .action(async (docId, options) => {
268
+ const { embeddings } = await import("../commands/opl-embeddings.js")
269
+ await embeddings("index", { ...options, docId })
270
+ })
271
+
272
+ embeddings
273
+ .command("status")
274
+ .description("Mostrar estado del índice de embeddings")
275
+ .action(async () => {
276
+ const { embeddings } = await import("../commands/opl-embeddings.js")
277
+ await embeddings("status")
278
+ })
279
+
280
+ embeddings
281
+ .command("remove <docId>")
282
+ .description("Eliminar embeddings de un documento")
283
+ .usage("<docId>")
284
+ .action(async (docId) => {
285
+ const { embeddings } = await import("../commands/opl-embeddings.js")
286
+ await embeddings("remove", { docId })
287
+ })
288
+
289
+ embeddings
290
+ .command("config")
291
+ .description("Ver o configurar el proveedor de embeddings")
292
+ .option("--provider <p>", "Establecer proveedor: ollama, transformers")
293
+ .action(async (options) => {
294
+ const { embeddings } = await import("../commands/opl-embeddings.js")
295
+ await embeddings("config", options)
296
+ })
297
+
298
+ // ──────────────────────────────────────────────
299
+ // opl webscrape — Extraer contenido web e indexar
300
+ // ──────────────────────────────────────────────
301
+ program
302
+ .command("webscrape <url>")
303
+ .description("Extraer contenido de una web, guardarlo como conocimiento e indexar embeddings")
304
+ .usage("<url>")
305
+ .option("--domain <d>", "Dominio de conocimiento destino (default: web)")
306
+ .option("--no-embed", "No indexar embeddings automáticamente")
307
+ .option("--clean", "Limpiar contenido con Ollama")
308
+ .option("--deep", "Deep scrape: navega categorías y subcategorías recursivamente")
309
+ .option("--max-depth <n>", "Profundidad máxima para deep scrape (default: 2)", parseInt)
310
+ .option("--concurrency <n>", "Fetchs simultáneos para deep scrape (default: 3)", parseInt)
311
+ .option("--verbose", "Log detallado del proceso")
312
+ .option("--no-ollama", "No usar Ollama para analizar páginas (solo heurísticas)")
313
+ .action(async (url, options) => {
314
+ if (options.deep) {
315
+ const { deepScrape } = await import("../core/webscrape/deep-scraper.js")
316
+ await deepScrape(url, {
317
+ maxDepth: options.maxDepth || 2,
318
+ concurrency: options.concurrency || 3,
319
+ domain: options.domain || "web",
320
+ noEmbed: options.noEmbed === true,
321
+ verbose: options.verbose === true,
322
+ ollama: options.ollama !== false, // default: true
323
+ })
324
+ } else {
325
+ const { webscrape } = await import("../commands/opl-webscrape.js")
326
+ await webscrape(url, options)
327
+ }
328
+ })
329
+
252
330
  // ──────────────────────────────────────────────
253
331
  // opl upgrade — Actualizar config a versión actual
254
332
  // ──────────────────────────────────────────────