saber: catalogo de awesome lists en el RAG
- awesome.py parsea awesome lists (markdown y RST): limpia enlaces y badges, agrupa cada categoria en un fragmento, y etiqueta fuente + licencia. - indexa.py las incluye; en el indice PUBLICO solo entran las redistribuibles: awesome (sindresorhus, CC0) y awesome-selfhosted (CC BY-SA 3.0). awesome-hacking (sin licencia) se indexa solo en el indice personal si se clona en CODERS. - saber-publico.jsonl pasa a 699 entradas (glosario + conocimiento + catalogo). - ATRIBUCION.md acredita fuentes y licencias; docs/rag.md lo documenta.
This commit is contained in:
parent
9fa0a40edf
commit
43335d329f
5 changed files with 500 additions and 6 deletions
168
nucleo/saber/awesome.py
Normal file
168
nucleo/saber/awesome.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Indexa las 'awesome lists' clonadas en ~/COFRE/CODERS como catalogo del RAG.
|
||||
|
||||
./awesome.py di cuantas entradas saldrian, por fuente
|
||||
./awesome.py --volcado imprime unas cuantas entradas de muestra
|
||||
|
||||
Las awesome lists son catalogos curados: "herramienta -> que hace + categoria".
|
||||
No son comandos ni metodologia; son el "que existe para hacer X". Aqui se parsean
|
||||
—markdown y reStructuredText— a entradas limpias, agrupadas por categoria, para
|
||||
que busca.py las recupere igual que todo lo demas.
|
||||
|
||||
Cada fuente lleva su LICENCIA. Solo las redistribuibles (CC0, CC-BY-SA) entran en
|
||||
el indice PUBLICO que se sube al repo; el resto se indexa solo en la maquina del
|
||||
usuario (indexado personal = uso legitimo). indexa.py respeta esa marca.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CODERS = os.path.expanduser("~/COFRE/CODERS")
|
||||
MAX_BYTES = 4 * 1024 * 1024 # una lista no pasa de esto; guarda la swap chica
|
||||
MAX_FRAGMENTO = 1100 # como el resto del indice
|
||||
|
||||
# Secciones que no son catalogo, se saltan vengan como vengan.
|
||||
RUIDO = re.compile(r"^(table of contents|contents|contributing|license|"
|
||||
r"list of licenses|anti.?features|external links|related|"
|
||||
r"footnotes|backers|sponsors|thanks|credits|acknowledge|"
|
||||
r"see also|index|legend|notes?)\b", re.I)
|
||||
|
||||
# (carpeta en CODERS, fichero, formato, etiqueta de fuente, perfil, licencia, publico)
|
||||
CATALOGOS = [
|
||||
("awesome-selfhosted", "README.md", "md", "awesome-selfhosted",
|
||||
"AUTOHOSPEDADO", "CC-BY-SA-3.0", True),
|
||||
("awesome", "readme.md", "md", "awesome (sindresorhus)",
|
||||
"LISTAS", "CC0-1.0", True),
|
||||
("awesome-hacking", os.path.join("_pages", "index.rst"), "rst",
|
||||
"awesome-hacking (jekil)", "PENTESTERS", "", False),
|
||||
]
|
||||
|
||||
# markdown: - [Nombre](http...) - Descripcion ... `Lic` `Lenguaje`
|
||||
IT_MD = re.compile(r"^\s*[-*]\s+\[([^\]]+)\]\((https?://[^)]+)\)\s*[-–—:]?\s*(.*)$")
|
||||
# rst: - `Nombre <http...>`_ - Descripcion
|
||||
IT_RST = re.compile(r"^\s*[-*]\s+`([^<]+?)\s*<([^>]+)>`_+\s*[-–—:]?\s*(.*)$")
|
||||
# parentesis de enlaces auxiliares: ([Source Code](...), [Demo](...))
|
||||
PARENS = re.compile(r"\s*\((?:\[[^\]]+\]\([^)]*\)[,\s]*)+\)")
|
||||
TAGS = re.compile(r"`([^`]+)`") # `MIT` `Docker` -> MIT, Docker
|
||||
|
||||
|
||||
def _limpia_desc(desc):
|
||||
desc = PARENS.sub("", desc) # fuera ([Source Code]...) etc.
|
||||
tags = TAGS.findall(desc) # licencia/lenguaje entre backticks
|
||||
desc = TAGS.sub("", desc).strip(" .-–—")
|
||||
if tags: # se conservan como pista (lenguaje, lic)
|
||||
desc = f"{desc} [{', '.join(t.strip() for t in tags)}]"
|
||||
return re.sub(r"\s+", " ", desc).strip()
|
||||
|
||||
|
||||
def _lee(ruta):
|
||||
if not os.path.exists(ruta) or os.path.getsize(ruta) > MAX_BYTES:
|
||||
return None
|
||||
with open(ruta, encoding="utf-8", errors="ignore") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _secciones_md(texto):
|
||||
"""Cede (categoria, [(nombre, desc)...]) por cada seccion ## / ###."""
|
||||
categoria, items = "", []
|
||||
for linea in texto.splitlines():
|
||||
h = re.match(r"^#{2,4}\s+(.+?)\s*$", linea)
|
||||
if h:
|
||||
if items:
|
||||
yield categoria, items
|
||||
categoria, items = re.sub(r"[#*`\[\]]", "", h.group(1)).strip(), []
|
||||
continue
|
||||
m = IT_MD.match(linea)
|
||||
if m and m.group(2).startswith("http"): # ignora enlaces de TOC (#ancla)
|
||||
desc = _limpia_desc(m.group(3))
|
||||
items.append((m.group(1).strip(), desc))
|
||||
if items:
|
||||
yield categoria, items
|
||||
|
||||
|
||||
def _secciones_rst(texto):
|
||||
"""En RST el titulo es una linea con otra de === o --- debajo."""
|
||||
lineas = texto.splitlines()
|
||||
categoria, items = "", []
|
||||
for i, linea in enumerate(lineas):
|
||||
sig = lineas[i + 1] if i + 1 < len(lineas) else ""
|
||||
if linea.strip() and re.match(r"^[=~^\"'`#*+.-]{3,}\s*$", sig) \
|
||||
and len(sig.strip()) >= len(linea.strip()) - 2 \
|
||||
and not linea.startswith(("-", "*", " ")):
|
||||
if items:
|
||||
yield categoria, items
|
||||
categoria, items = linea.strip(), []
|
||||
continue
|
||||
m = IT_RST.match(linea)
|
||||
if m:
|
||||
items.append((m.group(1).strip(), _limpia_desc(m.group(3))))
|
||||
if items:
|
||||
yield categoria, items
|
||||
|
||||
|
||||
def _fragmenta(fuente, perfil, licencia, publico, secciones):
|
||||
"""Agrupa los items de cada categoria en fragmentos con tope de tamaño."""
|
||||
for categoria, items in secciones:
|
||||
if not categoria or RUIDO.match(categoria):
|
||||
continue
|
||||
cabecera = f"Catalogo {fuente} / {categoria}:"
|
||||
buffer, n = cabecera, 0
|
||||
for nombre, desc in items:
|
||||
linea = f" {nombre}" + (f" — {desc}." if desc else ".")
|
||||
if len(buffer) + len(linea) > MAX_FRAGMENTO and n:
|
||||
yield _entrada(fuente, perfil, licencia, publico, categoria, buffer)
|
||||
buffer, n = cabecera, 0
|
||||
buffer += linea
|
||||
n += 1
|
||||
if n:
|
||||
yield _entrada(fuente, perfil, licencia, publico, categoria, buffer)
|
||||
|
||||
|
||||
def _entrada(fuente, perfil, licencia, publico, categoria, texto):
|
||||
return {
|
||||
"tipo": "catalogo", "herramienta": categoria, "perfil": perfil,
|
||||
"fuente": fuente, "licencia": licencia, "publico": publico,
|
||||
"tema": categoria.lower(),
|
||||
"fichero": f"awesome: {fuente} / {categoria}",
|
||||
"texto": texto,
|
||||
}
|
||||
|
||||
|
||||
def entradas(solo_publico=False):
|
||||
"""Todas las entradas de catalogo de las awesome lists disponibles."""
|
||||
for carpeta, fichero, formato, fuente, perfil, lic, publico in CATALOGOS:
|
||||
if solo_publico and not publico:
|
||||
continue
|
||||
texto = _lee(os.path.join(CODERS, carpeta, fichero))
|
||||
if not texto:
|
||||
continue
|
||||
secciones = _secciones_rst(texto) if formato == "rst" else _secciones_md(texto)
|
||||
yield from _fragmenta(fuente, perfil, lic, publico, secciones)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--volcado", action="store_true", help="imprime entradas de muestra")
|
||||
a = p.parse_args()
|
||||
|
||||
porfuente = {}
|
||||
muestras = []
|
||||
for e in entradas():
|
||||
porfuente[e["fuente"]] = porfuente.get(e["fuente"], 0) + 1
|
||||
if len(muestras) < 6:
|
||||
muestras.append(e)
|
||||
total = sum(porfuente.values())
|
||||
for fuente, n in porfuente.items():
|
||||
pub = next(c[5] or "sin licencia" for c in CATALOGOS if c[3] == fuente)
|
||||
print(f" {fuente:26s} {n:5d} fragmentos [{pub}]")
|
||||
print(f" total: {total} fragmentos de catalogo")
|
||||
if a.volcado:
|
||||
print("\n--- muestras ---")
|
||||
for e in muestras:
|
||||
print(f"\n[{e['fuente']} / {e['herramienta']}]\n {e['texto'][:300]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -149,6 +149,22 @@ def trocea(ruta):
|
|||
yield buffer.strip()
|
||||
|
||||
|
||||
def _catalogo(solo_publico=False):
|
||||
"""Las awesome lists clonadas en CODERS, si el modulo y las listas estan."""
|
||||
try:
|
||||
try:
|
||||
from saber import awesome
|
||||
except ImportError:
|
||||
import awesome
|
||||
except ImportError:
|
||||
return []
|
||||
fragmentos = []
|
||||
for e in awesome.entradas(solo_publico=solo_publico):
|
||||
e.pop("publico", None) # marca interna de filtrado, no va al indice
|
||||
fragmentos.append(e)
|
||||
return fragmentos
|
||||
|
||||
|
||||
def _conocimiento():
|
||||
"""El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
|
||||
fragmentos = []
|
||||
|
|
@ -201,8 +217,10 @@ def construye_publico(solo_cuenta=False):
|
|||
import glosario
|
||||
glo = list(glosario.entradas())
|
||||
con = _conocimiento()
|
||||
todo = glo + con
|
||||
print(f" glosario {len(glo)} + conocimiento {len(con)} = {len(todo)} entradas publicas")
|
||||
cat = _catalogo(solo_publico=True) # solo las awesome de licencia redistribuible
|
||||
todo = glo + con + cat
|
||||
print(f" glosario {len(glo)} + conocimiento {len(con)} + catalogo {len(cat)} "
|
||||
f"= {len(todo)} entradas publicas")
|
||||
if solo_cuenta:
|
||||
return 0
|
||||
_guarda_indice(todo, INDICE_PUBLICO)
|
||||
|
|
@ -262,9 +280,16 @@ def construye(solo_cuenta=False, solo_cofre=False):
|
|||
conocimiento = _conocimiento()
|
||||
print(f" CONOCIMIENTO {len(conocimiento):5d} fragmentos (pack del repo)")
|
||||
|
||||
todo = fichas + fragmentos + sistema + conocimiento
|
||||
# Las awesome lists clonadas en CODERS (catalogo de que-existe-para-X). En el
|
||||
# indice personal entran todas; el filtro por licencia solo aplica al publico.
|
||||
catalogo = _catalogo(solo_publico=False)
|
||||
if catalogo:
|
||||
print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)")
|
||||
|
||||
todo = fichas + fragmentos + sistema + conocimiento + catalogo
|
||||
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
|
||||
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento = {len(todo)} entradas")
|
||||
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
|
||||
f"{len(catalogo)} catalogo = {len(todo)} entradas")
|
||||
if solo_cuenta:
|
||||
return 0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue