atuq §6.3: la búsqueda semántica, VERDE en la jaula — y la barra lateral tiene las dos preguntas
Los cuatro guardianes pasan sobre los artefactos sellados (atuq bfc14c92, puriy-costura 3f31233e, ia-modelo-embeddings 2c0c4258): semántico 0.6252 gato / 0.2471 red / 0.0973 pan → y la otra pregunta gana la otra página control «no hay modelo de embeddings en …» y NINGÚN orden inventado ia el modelo contesta y el motor se va con el navegador (0 vivos) foco los tres estados, y el estado intacto tras la sesión La barra lateral ahora tiene dos botones: «Al modelo» (genera texto) y «A mis páginas» (ordena lo que ya leíste). No se mezclan a propósito — una inventa y la otra recuerda, y juntas sería imposible saber cuál contestó. Los resultados van EN ORDEN y sin porcentaje: el puntaje es un coseno y leerlo como «85 % de acierto» sería inventarle un significado. ⚠ Y el guardián nació midiendo NADA: metía las tres páginas en `<iframe>` y archivó cero, porque el §6.3 ignora lo que no es marco principal. Encadenadas como navegación de verdad entran las tres; y las páginas de tránsito van sin texto visible para que el archivo las descarte y la evidencia no liste coincidencias sin título. Dos cosas más del camino, las dos medidas: el `cp` del install buscaba el nombre de upstream y el tar —nuestro— lleva el fichero con nombre corto; y el build murió dos veces por DISCO LLENO (0 bytes en /mnt/vvv), no por el lock. `scripts/poda-fuentes.sh --horas 6` liberó 4,6 G, que es exactamente para lo que existe.
This commit is contained in:
@@ -716,18 +716,27 @@ siempre la más reciente— pasaría sin haber entendido nada. Y las preguntas *
|
||||
con el texto de su página; si las compartieran, la búsqueda literal también acertaría y esto no
|
||||
probaría nada semántico.
|
||||
|
||||
⚠ **Y lo que todavía NO corrió** (2026-09-12): ese guardián espera el artefacto sellado del modelo, y
|
||||
el build lleva 45 min en la cola del `flock` detrás de otro agente. Lo que sí está medido con el
|
||||
modelo de verdad es el camino completo `archive.add` → índice → `archive.ask` **fuera de la jaula**,
|
||||
contra el binario del host: 3/3, con los puntajes de abajo. Esta línea se borra cuando el guardián de
|
||||
la jaula dé verde.
|
||||
**MEDIDO en la jaula, sobre los artefactos sellados** (`atuq bfc14c92`, `puriy-costura 3f31233e`,
|
||||
`ia-modelo-embeddings 2c0c4258`):
|
||||
|
||||
```
|
||||
«¿dónde se echó a dormir el gato?» 0.6466 gato · 0.2194 red · 0.1463 pan
|
||||
«cómo bloqueo internet a un programa» 0.4082 red · 0.1408 pan · 0.0954 gato
|
||||
«qué lleva el pan» 0.2393 pan · 0.2028 gato · 0.1331 red
|
||||
ARCHIVADA … /gato.html ARCHIVADA … /red.html ARCHIVADA … /pan.html
|
||||
«¿dónde se echó a dormir el gato?» 0.6252 gato · 0.2471 red · 0.0973 pan
|
||||
«cómo bloqueo internet a un programa» 0.4039 red · 0.1398 pan · 0.1066 gato
|
||||
por la barra lateral 0.6252 /gato.html …
|
||||
```
|
||||
|
||||
Y el control negativo: sin modelo, `no hay modelo de embeddings en /usr/share/takana/ia/embeddings.gguf:
|
||||
esta imagen no trae ninguno pineado` — **y ningún orden inventado**, que es el único fallo inaceptable
|
||||
en una búsqueda.
|
||||
|
||||
⚠ **El guardián nació midiendo nada, y lo dijo el propio arnés.** La primera versión metía las tres
|
||||
páginas en `<iframe>` para archivarlas en una sola carga, y archivó **cero**: el §6.3 ignora a
|
||||
propósito lo que no es el marco principal —un iframe de publicidad no es una página que alguien
|
||||
leyó—. Encadenadas como navegación de verdad (cada página lleva a la siguiente con un `meta
|
||||
refresh`), las tres entran. De paso: las dos páginas de tránsito van **sin texto visible**, así el
|
||||
archivo las descarta y la evidencia no lista dos coincidencias sin título.
|
||||
|
||||
#### 6.3.quater El modelo que elegimos NO SERVÍA, y el síntoma no se parecía a la causa (2026-09-12)
|
||||
|
||||
Esto es la unidad de trabajo más caras de esta tanda y vale escribirla entera, porque la clase de
|
||||
|
||||
@@ -252,7 +252,13 @@ sobrevive y medio giga de modelo en RAM no.
|
||||
|
||||
El puerto nativo vive en el **fondo**, no en la página del panel: si viviera en la página, cerrar la
|
||||
barra lateral se llevaría el proceso del host y el modelo cargado con ella. `panel.html?q=…` abre el
|
||||
panel con la pregunta ya hecha.
|
||||
panel con la pregunta ya hecha, y `?p=…` la manda **a tus páginas** en vez de al modelo.
|
||||
|
||||
**Dos preguntas, dos botones, y no se mezclan a propósito.** «Al modelo» genera texto; «A mis
|
||||
páginas» ordena por parecido lo que ya leíste (§6.3, `archive.ask`) y muestra los títulos **en
|
||||
orden**, sin porcentaje: el puntaje es un coseno y leerlo como «85 % de acierto» sería inventarle un
|
||||
significado. Presentar las dos juntas haría imposible saber cuál contestó — una inventa y la otra
|
||||
recuerda.
|
||||
|
||||
Cuando algo falta, el panel **muestra la causa** («no hay modelo en …», «no hay motor de inferencia
|
||||
en …»). Una IA que ante un fallo contesta algo genérico es peor que una que no contesta: el usuario
|
||||
|
||||
@@ -36,11 +36,17 @@ function conectar() {
|
||||
const pendiente = r && r.id ? enVuelo.get(r.id) : null;
|
||||
if (!pendiente) return;
|
||||
enVuelo.delete(r.id);
|
||||
if (r.ok) {
|
||||
if (!r.ok) {
|
||||
di("ERROR " + (r.error || "sin causa"));
|
||||
} else if (r.verb === "archive.ask") {
|
||||
di("ARCHIVO " + (r.matches || []).length + " de " + r.archived +
|
||||
" (indexadas " + r.indexed + ")");
|
||||
for (const m of r.matches || []) {
|
||||
di("COINCIDENCIA " + m.score.toFixed(4) + " " + m.page.url);
|
||||
}
|
||||
} else {
|
||||
di("RESPUESTA tokens=" + (r.tokens === undefined ? "?" : r.tokens) +
|
||||
" modelo=" + (r.model || "?") + " texto=" + JSON.stringify(r.answer || ""));
|
||||
} else {
|
||||
di("ERROR " + (r.error || "sin causa"));
|
||||
}
|
||||
pendiente(r);
|
||||
});
|
||||
@@ -59,26 +65,30 @@ function conectar() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function preguntar(texto) {
|
||||
// El mismo puerto sirve las dos preguntas del panel, y no es casualidad: las dos las
|
||||
// contesta el MISMO host con el MISMO motor local — una con el modelo de chat y la otra
|
||||
// con el de embeddings sobre lo que ya leíste (§6.3).
|
||||
function pedir(verbo, campo, texto) {
|
||||
if (!puerto && !conectar()) {
|
||||
return Promise.resolve({ ok: false, error: "no hay host nativo en esta imagen" });
|
||||
}
|
||||
const id = siguiente++;
|
||||
di("PREGUNTA id=" + id + " " + JSON.stringify(texto));
|
||||
di("PIDE " + verbo + " id=" + id + " " + JSON.stringify(texto));
|
||||
return new Promise((resolver) => {
|
||||
enVuelo.set(id, resolver);
|
||||
try {
|
||||
puerto.postMessage({ id, verb: "ai.ask", prompt: texto });
|
||||
puerto.postMessage({ id, verb: verbo, [campo]: texto });
|
||||
} catch (e) {
|
||||
enVuelo.delete(id);
|
||||
resolver({ ok: false, error: "no pude enviar la pregunta: " + e });
|
||||
resolver({ ok: false, error: "no pude enviar: " + e });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((msg) => {
|
||||
if (!msg || msg.tipo !== "preguntar" || typeof msg.texto !== "string") return;
|
||||
return preguntar(msg.texto);
|
||||
if (!msg || typeof msg.texto !== "string") return;
|
||||
if (msg.tipo === "preguntar") return pedir("ai.ask", "prompt", msg.texto);
|
||||
if (msg.tipo === "archivo") return pedir("archive.ask", "query", msg.texto);
|
||||
});
|
||||
|
||||
di("LISTA v1 el modelo corre en esta máquina; no sale nada a la red");
|
||||
|
||||
@@ -8,3 +8,6 @@ main { display: flex; flex-direction: column; height: 100vh; box-sizing: border-
|
||||
.turno.error { color: #b3261e; }
|
||||
form { display: flex; gap: 6px; }
|
||||
textarea { flex: 1; resize: vertical; font: inherit; }
|
||||
.botones { display: flex; flex-direction: column; gap: 4px; }
|
||||
.fuente { display: block; margin-top: 2px; opacity: .8; }
|
||||
.fuente a { color: inherit; }
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
<div id="hilo"></div>
|
||||
<form id="forma">
|
||||
<textarea id="texto" rows="3" placeholder="Preguntá algo…"></textarea>
|
||||
<button id="enviar" type="submit">Preguntar</button>
|
||||
<div class="botones">
|
||||
<button id="enviar" type="submit">Al modelo</button>
|
||||
<button id="archivo" type="button">A mis páginas</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
<script src="panel.js"></script>
|
||||
|
||||
@@ -38,6 +38,44 @@ async function preguntar(texto) {
|
||||
esperando.textContent = r.answer;
|
||||
}
|
||||
|
||||
// Las páginas ya leídas (§6.3): la misma caja, otra pregunta. No se mezcla con la del
|
||||
// modelo a propósito — una INVENTA texto y la otra devuelve lo que vos leíste, y presentarlas
|
||||
// juntas haría imposible saber cuál de las dos contestó.
|
||||
async function buscarEnMisPaginas(texto) {
|
||||
if (!texto.trim()) return;
|
||||
agregar("yo", texto);
|
||||
const esperando = agregar("", "buscando en lo que leíste…");
|
||||
let r;
|
||||
try {
|
||||
r = await browser.runtime.sendMessage({ tipo: "archivo", texto });
|
||||
} catch (e) {
|
||||
r = { ok: false, error: String(e) };
|
||||
}
|
||||
if (!r || !r.ok) {
|
||||
esperando.className = "turno error";
|
||||
esperando.textContent = (r && r.error) || "el host no contestó";
|
||||
return;
|
||||
}
|
||||
const m = r.matches || [];
|
||||
if (!m.length) {
|
||||
esperando.textContent = `no encontré nada entre tus ${r.archived} páginas`;
|
||||
return;
|
||||
}
|
||||
// Se muestran EN ORDEN y sin porcentaje: el puntaje es un coseno y leerlo como
|
||||
// «85 % de acierto» sería inventarle un significado que no tiene.
|
||||
esperando.textContent = `de tus ${r.archived} páginas, en orden de parecido:`;
|
||||
for (const c of m.slice(0, 8)) {
|
||||
const d = document.createElement("div");
|
||||
d.className = "fuente";
|
||||
const a = document.createElement("a");
|
||||
a.href = c.page.url;
|
||||
a.textContent = c.page.title || c.page.url;
|
||||
a.title = c.page.url;
|
||||
d.appendChild(a);
|
||||
esperando.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("forma").addEventListener("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
const caja = document.getElementById("texto");
|
||||
@@ -46,5 +84,17 @@ document.getElementById("forma").addEventListener("submit", (ev) => {
|
||||
preguntar(texto);
|
||||
});
|
||||
|
||||
const q = new URLSearchParams(location.search).get("q");
|
||||
document.getElementById("archivo").addEventListener("click", () => {
|
||||
const caja = document.getElementById("texto");
|
||||
const texto = caja.value;
|
||||
caja.value = "";
|
||||
buscarEnMisPaginas(texto);
|
||||
});
|
||||
|
||||
// `?q=` pregunta al modelo; `?p=` busca en las páginas. Las dos son la puerta por la que
|
||||
// entran las preguntas que no se tipean acá (y las que usan los guardianes).
|
||||
const params = new URLSearchParams(location.search);
|
||||
const q = params.get("q");
|
||||
if (q) preguntar(q);
|
||||
const pag = params.get("p");
|
||||
if (pag) buscarEnMisPaginas(pag);
|
||||
|
||||
@@ -65,7 +65,11 @@ compile = "true"
|
||||
install = '''
|
||||
set -e
|
||||
mkdir -p /out/usr/share/takana/ia
|
||||
cp Qwen3-Embedding-0.6B-Q8_0.gguf /out/usr/share/takana/ia/embeddings.gguf
|
||||
# ⚠ El fichero DENTRO del tar se llama `qwen3-emb-q8.gguf` y no como en upstream: el tar lo
|
||||
# armamos nosotros y quedó con el nombre corto con el que se bajó. Se deja así —renombrarlo
|
||||
# significaría reempaquetar y volver a subir 610 MB— pero queda dicho acá, porque el bloque de
|
||||
# procedencia de arriba sí nombra el fichero de upstream y las dos cosas tienen que cuadrar.
|
||||
cp qwen3-emb-q8.gguf /out/usr/share/takana/ia/embeddings.gguf
|
||||
M=/out/usr/share/takana/ia/embeddings.gguf
|
||||
|
||||
# ── GUARDIÁN 1: QUE SEA UN GGUF ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[source]
|
||||
repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
|
||||
commit = "8415e5d12a879026a7518fb2f626bce1bcdac00b"
|
||||
commit = "43f9e09f476f159635c5b9ea209aeb854b92606c"
|
||||
# ⚠ SIN ESTO EL BUILD MUERE EN UN SITIO QUE NO NOMBRA LA CAUSA — medido acá el 2026-09-10.
|
||||
# tawasuyu COMMITEA su propio `vendor/`: adentro tiene un `smithay` parcheado a mano (tearing/flip
|
||||
# async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`.
|
||||
|
||||
@@ -87,15 +87,26 @@ class Servidor(http.server.SimpleHTTPRequestHandler):
|
||||
ruta = self.path.split("?")[0]
|
||||
if ruta in PAGINAS:
|
||||
titulo, texto = PAGINAS[ruta]
|
||||
# Cada página lleva a la siguiente: tres cargas de PRIMER NIVEL con un solo arranque.
|
||||
orden = list(PAGINAS)
|
||||
i = orden.index(ruta)
|
||||
siguiente = orden[i + 1] if i + 1 < len(orden) else "/fin.html"
|
||||
datos = (f'<!doctype html><html><head><meta charset="utf-8"><title>{titulo}</title>'
|
||||
f'<meta http-equiv="refresh" content="3;url={siguiente}">'
|
||||
f"</head><body><h1>{titulo}</h1><p>{texto}</p></body></html>").encode()
|
||||
elif ruta == "/ir.html":
|
||||
# Una página que pasea por las tres: el guión de contenido archiva cada una al cargar.
|
||||
saltos = "".join(f'<iframe src="{r}" width="10" height="10"></iframe>' for r in PAGINAS)
|
||||
datos = (f'<!doctype html><html><head><meta charset="utf-8"></head><body>{saltos}'
|
||||
"</body></html>").encode()
|
||||
# ⚠ La primera versión de esto metía las tres páginas en `<iframe>` y NO archivaba
|
||||
# ninguna: el archivo ignora a propósito lo que no es el marco principal (un iframe de
|
||||
# publicidad no es una página que alguien leyó). Ahora se ENCADENAN como navegación de
|
||||
# verdad, que es lo que hace una persona.
|
||||
datos = (b'<!doctype html><html><head><meta charset="utf-8">'
|
||||
b'<meta http-equiv="refresh" content="1;url=/gato.html">'
|
||||
# Sin texto visible: el archivo descarta lo que no tiene nada que buscar, así
|
||||
# que esta página de tránsito no ensucia la evidencia con una entrada sin título.
|
||||
b"</head><body></body></html>")
|
||||
else:
|
||||
datos = b"<html><body>nada</body></html>"
|
||||
# Igual que `/ir.html`: sin texto visible, para que el final de la cadena no se archive.
|
||||
datos = b"<html><body></body></html>"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(datos)))
|
||||
@@ -192,6 +203,8 @@ def main():
|
||||
correr(capas if not negativo else [atuq, costura, llama], salida, guion)
|
||||
|
||||
log = open(os.path.join(salida, "consola.log"), errors="replace").read()
|
||||
base = next((l[len("IA BASE "):].strip() for l in log.splitlines()
|
||||
if l.startswith("IA BASE ")), None)
|
||||
lineas = [l[len("ARCHIVO "):] for l in log.splitlines() if l.startswith("ARCHIVO ")]
|
||||
archivadas = [l for l in lineas if l.startswith("ARCHIVADA ")]
|
||||
for l in archivadas:
|
||||
@@ -230,9 +243,40 @@ def main():
|
||||
fatal(f"«{q}» puso primera a {m[0]['page']['url']} y esperaba {esperada}"
|
||||
" — el orden no sigue al SIGNIFICADO")
|
||||
|
||||
# ── Y LA BARRA LATERAL, que es por donde lo usa una persona ──────────────────────
|
||||
# Lo de arriba mide el host; esto mide que la extensión llegue hasta él y muestre el
|
||||
# orden. Sin esta mitad, la función existiría y no habría cómo usarla.
|
||||
if not base:
|
||||
fatal("la extensión de IA no dijo su URL base: no puedo abrir el panel")
|
||||
q, esperada = PREGUNTAS[0]
|
||||
panel = f"{base}panel.html?p={q.replace(' ', '%20').replace('¿', '%C2%BF')}"
|
||||
guion2 = f"""
|
||||
mkdir -p /root /salida/hogar
|
||||
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
|
||||
export HOME=/salida/hogar
|
||||
timeout 90 /usr/bin/atuq --profile /salida/perfil --no-remote --headless '{panel}' \
|
||||
> /salida/panel.log 2>&1
|
||||
exit 0
|
||||
"""
|
||||
correr(capas, salida, guion2)
|
||||
plog = open(os.path.join(salida, "panel.log"), errors="replace").read()
|
||||
coincidencias = [l[len("IA COINCIDENCIA "):] for l in plog.splitlines()
|
||||
if l.startswith("IA COINCIDENCIA ")]
|
||||
print("\n por la barra lateral:")
|
||||
for c in coincidencias[:4]:
|
||||
print(" " + c)
|
||||
if not coincidencias:
|
||||
destino = os.path.join(ROOT, "work", "atuq-panel-sem.log")
|
||||
os.makedirs(os.path.dirname(destino), exist_ok=True)
|
||||
shutil.copy(os.path.join(salida, "panel.log"), destino)
|
||||
fatal(f"el panel no mostró ninguna coincidencia — log en {destino}")
|
||||
if esperada not in coincidencias[0]:
|
||||
fatal(f"el panel puso primera a «{coincidencias[0]}» y esperaba {esperada}")
|
||||
|
||||
print("\n✓ el archivo contesta por PARECIDO: cada pregunta pone primera a su página")
|
||||
print("✓ y son dos preguntas con ganadores distintos — un ranking constante no pasaría")
|
||||
print("✓ todo sin navegador: el archivo quedó en disco y se consulta por el cable")
|
||||
print("✓ y por la barra lateral también: la extensión llega al host y muestra el orden")
|
||||
return 0
|
||||
finally:
|
||||
servidor.shutdown()
|
||||
|
||||
Reference in New Issue
Block a user