saber: preguntas-gancho sintetizadas horneadas en el indice publico
- sintetiza_catalogo.py genera preguntas en castellano (indexacion multi- representacion) para el pack de conocimiento y el catalogo de awesome lists. - indexa.py hornea los ganchos: al indice publico solo los de fragmentos publicos (~1856); el personal ademas rehornea la sintesis nocturna de COFRE (privada), para que reconstruir no pise ese trabajo. - saber-publico.jsonl pasa a 2555 entradas con los vectores de las preguntas: un clon recien hecho consulta con recall alto sin correr sintesis. - eval del indice personal: hit@1 82%, hit@5 97%, sim 0.927.
This commit is contained in:
parent
43335d329f
commit
873a22ea6a
4 changed files with 2043 additions and 6 deletions
13
docs/rag.md
13
docs/rag.md
|
|
@ -93,7 +93,7 @@ forma de medirlo— sí está; el corpus lo pone cada quien con sus notas.
|
||||||
| El glosario de tareas de Linux en castellano (`glosario.py`) | **sí** (genérico) |
|
| El glosario de tareas de Linux en castellano (`glosario.py`) | **sí** (genérico) |
|
||||||
| **El pack de metodología y manuales** (`saber/conocimiento/`) | **sí** — limpio y genérico |
|
| **El pack de metodología y manuales** (`saber/conocimiento/`) | **sí** — limpio y genérico |
|
||||||
| **El catálogo de *awesome lists*** (`saber/awesome.py`) | **sí** — solo lo redistribuible (CC0, CC BY-SA), ver [ATRIBUCION](../ATRIBUCION.md) |
|
| **El catálogo de *awesome lists*** (`saber/awesome.py`) | **sí** — solo lo redistribuible (CC0, CC BY-SA), ver [ATRIBUCION](../ATRIBUCION.md) |
|
||||||
| **El índice público pre-construido** (`datos/saber-publico.jsonl`) | **sí** — glosario + pack + catálogo, con vectores |
|
| **El índice público pre-construido** (`datos/saber-publico.jsonl`) | **sí** — glosario + pack + catálogo + preguntas-gancho, con vectores |
|
||||||
| El índice personal (`saber.jsonl`) y sus copias | **no** — los apuntes indexados |
|
| El índice personal (`saber.jsonl`) y sus copias | **no** — los apuntes indexados |
|
||||||
| El diario (`diario.jsonl`) | **no** — las conversaciones |
|
| El diario (`diario.jsonl`) | **no** — las conversaciones |
|
||||||
| Lo derivado (`cosecha.jsonl`, `sintesis.jsonl`) | **no** — salen de los apuntes |
|
| Lo derivado (`cosecha.jsonl`, `sintesis.jsonl`) | **no** — salen de los apuntes |
|
||||||
|
|
@ -118,3 +118,14 @@ licencia: al índice **público** solo van las redistribuibles —`awesome`
|
||||||
[ATRIBUCION.md](../ATRIBUCION.md))—. `awesome-hacking`, sin licencia explícita, se
|
[ATRIBUCION.md](../ATRIBUCION.md))—. `awesome-hacking`, sin licencia explícita, se
|
||||||
indexa solo en el índice **personal** si se clona en `~/COFRE/CODERS/`. Para
|
indexa solo en el índice **personal** si se clona en `~/COFRE/CODERS/`. Para
|
||||||
ampliar el catálogo, se clona otra lista ahí y se reindexa.
|
ampliar el catálogo, se clona otra lista ahí y se reindexa.
|
||||||
|
|
||||||
|
## Las preguntas-gancho, ya horneadas
|
||||||
|
|
||||||
|
El salto de recall (48→82 % hit@1) viene de la *indexación multi-representación*:
|
||||||
|
por cada fragmento, preguntas en castellano que lo encuentran. Generarlas cuesta
|
||||||
|
horas de modelo local, así que `enriquece/sintetiza_catalogo.py` las produce para
|
||||||
|
el pack y el catálogo, y el índice público **las trae ya calculadas**. Un clon
|
||||||
|
recién hecho consulta con esa calidad sin correr ni un minuto de síntesis: sobre
|
||||||
|
el `saber-publico.jsonl` hay ~1.900 preguntas-gancho además de los fragmentos en
|
||||||
|
crudo. Las de fragmentos no públicos (p. ej. `awesome-hacking`) se quedan fuera,
|
||||||
|
como el resto de su contenido.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
127
nucleo/saber/enriquece/sintetiza_catalogo.py
Normal file
127
nucleo/saber/enriquece/sintetiza_catalogo.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Genera preguntas-gancho para el pack de conocimiento y el catalogo de awesome.
|
||||||
|
|
||||||
|
./sintetiza_catalogo.py procesa todo (reanudable, ~1-2 h)
|
||||||
|
./sintetiza_catalogo.py --muestra 5 ensena 5 y para
|
||||||
|
|
||||||
|
Misma idea multi-representacion que sintetiza.py, pero sobre el contenido que
|
||||||
|
VIENE con el repo (conocimiento/ + awesome lists) en vez de sobre la cosecha de
|
||||||
|
COFRE. Por que importa: el indice PUBLICO pre-construido trae los fragmentos en
|
||||||
|
crudo pero SIN preguntas-gancho, asi que un clon recien hecho no tiene el salto de
|
||||||
|
recall que dan (48->82 % hit@1 medido en la metodologia). Esto lo genera para que
|
||||||
|
si venga horneado.
|
||||||
|
|
||||||
|
Respeta la marca `publico` de cada fragmento: los ganchos heredan esa marca, y
|
||||||
|
indexa.py mete en el indice publico solo los publicos (los de awesome-hacking, sin
|
||||||
|
licencia, se quedan en el personal).
|
||||||
|
|
||||||
|
Reanudable: apunta en un .hecho por que fragmento iba. El contenido publico se
|
||||||
|
procesa PRIMERO, para que si se corta, lo que se sube ya este.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
RAIZ = os.path.dirname(os.path.dirname(AQUI))
|
||||||
|
SALIDA = os.path.join(RAIZ, "datos", "sintesis-catalogo.jsonl")
|
||||||
|
MARCA = SALIDA + ".hecho"
|
||||||
|
|
||||||
|
# Reutiliza el modelo, la llamada a ollama y el reanudado de sintetiza.py.
|
||||||
|
sys.path.insert(0, RAIZ) # nucleo/ en el path para saber.*
|
||||||
|
from saber import awesome # noqa: E402
|
||||||
|
from saber import indexa # noqa: E402 (_conocimiento)
|
||||||
|
from saber.enriquece import sintetiza # noqa: E402
|
||||||
|
|
||||||
|
# Instruccion mas general que la de sintetiza.py (aquello era solo pentest).
|
||||||
|
sintetiza.INSTRUCCION = """Eres un generador de preguntas para un buscador de
|
||||||
|
herramientas y software. Te doy un fragmento con una lista de programas o tecnicas
|
||||||
|
de una categoria, cada uno con una breve descripcion. Devuelve de UNA a TRES
|
||||||
|
preguntas, en castellano, que una persona haria por voz buscando algo de esa
|
||||||
|
categoria y que este fragmento ayuda a responder.
|
||||||
|
|
||||||
|
Reglas:
|
||||||
|
- Preguntas naturales: "que programa hay para...", "con que puedo montar un...",
|
||||||
|
"cual es una alternativa libre a...", "que herramienta sirve para...".
|
||||||
|
- Piensa en la NECESIDAD (que quiere hacer), no en nombres concretos.
|
||||||
|
- NO respondas, NO copies nombres ni comandos: solo las preguntas.
|
||||||
|
- Si el fragmento es ruido (un titulo suelto, sin descripciones), lista vacia.
|
||||||
|
|
||||||
|
Responde SOLO con JSON: {"preguntas": ["...", "..."]}"""
|
||||||
|
|
||||||
|
|
||||||
|
def _fragmentos():
|
||||||
|
"""Conocimiento (publico) primero, luego catalogo publico, y al final el
|
||||||
|
personal (awesome-hacking). Cada uno con su marca `publico`."""
|
||||||
|
frags = []
|
||||||
|
for e in indexa._conocimiento(): # el pack del repo: todo publico
|
||||||
|
e = dict(e)
|
||||||
|
e["publico"] = True
|
||||||
|
frags.append(e)
|
||||||
|
for e in awesome.entradas(): # catalogo: ya trae `publico`
|
||||||
|
frags.append(e)
|
||||||
|
frags.sort(key=lambda d: (0 if d.get("publico") else 1,
|
||||||
|
0 if d.get("tipo") == "metodologia" else 1))
|
||||||
|
return frags
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--muestra", type=int, default=0)
|
||||||
|
a = p.parse_args()
|
||||||
|
|
||||||
|
frags = _fragmentos()
|
||||||
|
modelo = sintetiza._modelo()
|
||||||
|
npub = sum(1 for f in frags if f.get("publico"))
|
||||||
|
print(f" {len(frags)} fragmentos ({npub} publicos) · modelo {modelo}", flush=True)
|
||||||
|
|
||||||
|
if a.muestra:
|
||||||
|
for fr in frags[:a.muestra]:
|
||||||
|
print(f"\n ── {fr.get('fichero')} ──")
|
||||||
|
for q in sintetiza._pide(fr["texto"], modelo):
|
||||||
|
print(f" P: {q}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
desde = 0
|
||||||
|
if os.path.exists(MARCA) and os.path.exists(SALIDA):
|
||||||
|
try:
|
||||||
|
desde = int(open(MARCA).read().strip()) + 1
|
||||||
|
except (ValueError, OSError):
|
||||||
|
desde = 0
|
||||||
|
modo = "a" if desde else "w"
|
||||||
|
hechas = sum(1 for _ in open(SALIDA)) if desde and os.path.exists(SALIDA) else 0
|
||||||
|
if desde:
|
||||||
|
print(f" reanudando desde {desde} ({hechas} preguntas)", flush=True)
|
||||||
|
|
||||||
|
total = len(frags)
|
||||||
|
with open(SALIDA, modo, encoding="utf-8", buffering=1) as out:
|
||||||
|
for i in range(desde, total):
|
||||||
|
if i % 25 == 0:
|
||||||
|
modelo = sintetiza._modelo()
|
||||||
|
fr = frags[i]
|
||||||
|
for q in sintetiza._pide(fr["texto"], modelo):
|
||||||
|
out.write(json.dumps({
|
||||||
|
"tipo": "pregunta",
|
||||||
|
"herramienta": fr.get("herramienta"),
|
||||||
|
"perfil": fr.get("perfil"),
|
||||||
|
"fichero": fr.get("fichero"),
|
||||||
|
"tema": fr.get("tema", ""),
|
||||||
|
"fuente": fr.get("fuente"),
|
||||||
|
"licencia": fr.get("licencia"),
|
||||||
|
"publico": bool(fr.get("publico")),
|
||||||
|
"texto": fr["texto"],
|
||||||
|
"indexar": q,
|
||||||
|
}, ensure_ascii=False) + "\n")
|
||||||
|
hechas += 1
|
||||||
|
with open(MARCA, "w") as m:
|
||||||
|
m.write(str(i))
|
||||||
|
if (i + 1) % 25 == 0:
|
||||||
|
print(f" PROGRESO catalogo hecho={i+1} total={total} "
|
||||||
|
f"preguntas={hechas}", flush=True)
|
||||||
|
print(f"\n hecho: {hechas} preguntas en {SALIDA}", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -165,6 +165,41 @@ def _catalogo(solo_publico=False):
|
||||||
return fragmentos
|
return fragmentos
|
||||||
|
|
||||||
|
|
||||||
|
def _ganchos(solo_publico=False):
|
||||||
|
"""Las preguntas-gancho sintetizadas, si existen. Multi-representacion: cada
|
||||||
|
una embebe una pregunta (`indexar`) y muestra el fragmento (`texto`). Es lo
|
||||||
|
que sube el recall (48->82 % hit@1). Dos fuentes:
|
||||||
|
|
||||||
|
- sintesis-catalogo.jsonl: pack del repo + catalogo (enriquece/
|
||||||
|
sintetiza_catalogo.py). Lleva marca `publico` por fragmento.
|
||||||
|
- sintesis.jsonl: la sintesis NOCTURNA sobre los apuntes de COFRE
|
||||||
|
(enriquece/sintetiza.py). Es privada, nunca va al indice publico.
|
||||||
|
|
||||||
|
Se incluye aqui para que un `indexa.py` no pise el trabajo de la noche: la
|
||||||
|
reconstruccion desde cero rehornea ambas.
|
||||||
|
"""
|
||||||
|
fuentes = [
|
||||||
|
(os.path.join(AQUI, "datos", "sintesis-catalogo.jsonl"), None),
|
||||||
|
(os.path.join(AQUI, "datos", "sintesis.jsonl"), False), # COFRE: privada
|
||||||
|
]
|
||||||
|
ganchos = []
|
||||||
|
for ruta, forzar in fuentes:
|
||||||
|
if not os.path.exists(ruta):
|
||||||
|
continue
|
||||||
|
with open(ruta, encoding="utf-8") as f:
|
||||||
|
for l in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(l)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
pub = e.get("publico") if forzar is None else forzar
|
||||||
|
if solo_publico and not pub:
|
||||||
|
continue
|
||||||
|
e.pop("publico", None)
|
||||||
|
ganchos.append(e)
|
||||||
|
return ganchos
|
||||||
|
|
||||||
|
|
||||||
def _conocimiento():
|
def _conocimiento():
|
||||||
"""El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
|
"""El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
|
||||||
fragmentos = []
|
fragmentos = []
|
||||||
|
|
@ -194,7 +229,10 @@ def _guarda_indice(todo, ruta):
|
||||||
modelo = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
|
modelo = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
|
||||||
|
|
||||||
print(" calculando vectores...", flush=True)
|
print(" calculando vectores...", flush=True)
|
||||||
vectores = modelo.encode([e["texto"] for e in todo], show_progress_bar=False)
|
# se embebe `indexar` si existe (la pregunta-gancho sintetizada), si no `texto`.
|
||||||
|
# busca.py solo lee `texto` y `v`, asi la respuesta mostrada es el fragmento.
|
||||||
|
vectores = modelo.encode([e.get("indexar") or e["texto"] for e in todo],
|
||||||
|
show_progress_bar=False)
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(ruta), exist_ok=True)
|
os.makedirs(os.path.dirname(ruta), exist_ok=True)
|
||||||
with open(ruta, "w", encoding="utf-8") as f:
|
with open(ruta, "w", encoding="utf-8") as f:
|
||||||
|
|
@ -218,9 +256,10 @@ def construye_publico(solo_cuenta=False):
|
||||||
glo = list(glosario.entradas())
|
glo = list(glosario.entradas())
|
||||||
con = _conocimiento()
|
con = _conocimiento()
|
||||||
cat = _catalogo(solo_publico=True) # solo las awesome de licencia redistribuible
|
cat = _catalogo(solo_publico=True) # solo las awesome de licencia redistribuible
|
||||||
todo = glo + con + cat
|
gan = _ganchos(solo_publico=True) # preguntas-gancho de fragmentos publicos
|
||||||
|
todo = glo + con + cat + gan
|
||||||
print(f" glosario {len(glo)} + conocimiento {len(con)} + catalogo {len(cat)} "
|
print(f" glosario {len(glo)} + conocimiento {len(con)} + catalogo {len(cat)} "
|
||||||
f"= {len(todo)} entradas publicas")
|
f"+ ganchos {len(gan)} = {len(todo)} entradas publicas")
|
||||||
if solo_cuenta:
|
if solo_cuenta:
|
||||||
return 0
|
return 0
|
||||||
_guarda_indice(todo, INDICE_PUBLICO)
|
_guarda_indice(todo, INDICE_PUBLICO)
|
||||||
|
|
@ -286,10 +325,14 @@ def construye(solo_cuenta=False, solo_cofre=False):
|
||||||
if catalogo:
|
if catalogo:
|
||||||
print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)")
|
print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)")
|
||||||
|
|
||||||
todo = fichas + fragmentos + sistema + conocimiento + catalogo
|
ganchos = _ganchos(solo_publico=False)
|
||||||
|
if ganchos:
|
||||||
|
print(f" GANCHOS {len(ganchos):5d} preguntas sintetizadas")
|
||||||
|
|
||||||
|
todo = fichas + fragmentos + sistema + conocimiento + catalogo + ganchos
|
||||||
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
|
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
|
||||||
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
|
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
|
||||||
f"{len(catalogo)} catalogo = {len(todo)} entradas")
|
f"{len(catalogo)} catalogo + {len(ganchos)} ganchos = {len(todo)} entradas")
|
||||||
if solo_cuenta:
|
if solo_cuenta:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue