From b17149c5283785a72e6e87858ab0178278c3d078 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 23 May 2026 14:36:20 +0000 Subject: [PATCH] gioser-web: add docs/ dir with frontmatter for Qdrant indexing - 4 md files (aire, fuego, tierra, agua) with YAML frontmatter - caminos mapped: logos, nomos, kay, uku - original md/ unchanged - add scripts/index-gioser-docs.py (adapted from gioserv) --- crates/apps/gioser-web/docs/agua.md | 41 ++++++ crates/apps/gioser-web/docs/aire.md | 37 +++++ crates/apps/gioser-web/docs/fuego.md | 36 +++++ crates/apps/gioser-web/docs/tierra.md | 37 +++++ scripts/index-gioser-docs.py | 189 ++++++++++++++++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 crates/apps/gioser-web/docs/agua.md create mode 100644 crates/apps/gioser-web/docs/aire.md create mode 100644 crates/apps/gioser-web/docs/fuego.md create mode 100644 crates/apps/gioser-web/docs/tierra.md create mode 100644 scripts/index-gioser-docs.py diff --git a/crates/apps/gioser-web/docs/agua.md b/crates/apps/gioser-web/docs/agua.md new file mode 100644 index 0000000..c7ce68e --- /dev/null +++ b/crates/apps/gioser-web/docs/agua.md @@ -0,0 +1,41 @@ +--- +title: Mística · Espiritualidad +camino: uku +tags: [mística, espiritualidad, meditación, contemplación] +--- + +> *La práctica como puente. El misterio como interlocutor.* + +Acá vive lo místico, lo espiritual, las prácticas que sostienen la +atención. No es decoración: es la otra mitad del trabajo. Sin esto, +el resto se vuelve ruido. + +## Prácticas + +Lo que sostiene día a día: + +- **Meditación.** Sentarse a observar lo que sucede, sin agarrarlo. +- **Lectura contemplativa.** Textos que se vuelven a leer hasta que + cambian. +- **Ceremonia.** Marcar inicios y cierres con gestos que pesan. +- **Naturaleza.** Estar en lugares donde uno no es el centro. +- **Silencio.** Día completo, una vez por mes mínimo. + +## Por qué mística + +Porque la racionalidad sola no alcanza para vivir. Y porque las +tradiciones llevan miles de años elaborando vocabulario para lo que +nos pasa cuando atendemos en serio: contemplación, ego, símbolo, +muerte, asombro. + +**Mística aplicada** = no quedarse en el libro. Pasar por el cuerpo, +por la relación, por la vida cotidiana. + +## Lo que leo + +Andino, budista, cristiano-contemplativo, hindú, sufí. Sin +exclusividad: cada tradición resuelve algunas cosas mejor que otras. + +## Próximamente + +*Acá se va a ir armando una bitácora de lecturas, prácticas y notas.* diff --git a/crates/apps/gioser-web/docs/aire.md b/crates/apps/gioser-web/docs/aire.md new file mode 100644 index 0000000..6ed6de1 --- /dev/null +++ b/crates/apps/gioser-web/docs/aire.md @@ -0,0 +1,37 @@ +--- +title: Software · Tecnología +camino: logos +tags: [software, tecnología, open-source, rust, python] +--- + +> *Lo público. Lo que se mantiene abierto.* + +Acá viven los proyectos de **software libre**, herramientas, librerías +y exploraciones técnicas que voy publicando. La premisa es mantener +el código abierto, documentado y útil más allá del autor. + +## Qué vas a encontrar acá + +- Repos públicos de cosas que escribo (Rust, Python, embedded, web). +- Notas técnicas sobre arquitectura, sistemas distribuidos, runtimes. +- Ensayos sobre **IA aplicada** — sin hype, con ejemplos concretos. +- Bitácoras de exploración: lo que probé, lo que descarté, lo que sigo + usando. + +## Por qué open source + +Porque el conocimiento técnico se multiplica cuando circula. Y porque +mucho de lo que uso a diario me lo regaló alguien que decidió compartir. +La reciprocidad importa. + +## Stack actual + +- **Rust** para lo que necesita ser rápido, seguro y portable. +- **Python** para análisis, ML y prototipos rápidos. +- **Linux** (Artix/Arch) como sistema operativo de trabajo. +- **gitea** + **nix** para infraestructura personal. + +## Próximamente + +*Voy a ir enlazando proyectos específicos acá: tools, runtimes, +experimentos. Por ahora, este placeholder vive en `docs/aire.md`.* diff --git a/crates/apps/gioser-web/docs/fuego.md b/crates/apps/gioser-web/docs/fuego.md new file mode 100644 index 0000000..cda7c3e --- /dev/null +++ b/crates/apps/gioser-web/docs/fuego.md @@ -0,0 +1,36 @@ +--- +title: Quién Soy · Bitácora +camino: nomos +tags: [identidad, bitácora, crónica, personal] +--- + +> *La identidad como verbo. La crónica como práctica.* + +Acá vive lo personal: quién soy, qué hago, qué leo, qué pienso. Una +bitácora honesta, no curada para impresionar. Si vas a leer esto, +asumí que es borrador. + +## Quién soy + +**Sergio**. Programador, lector, padre, alguien que practica +mantenerse despierto. Vivo entre código, café, montañas y libros. + +Las cosas que más me importan no son las que mejor cuento todavía. +Por eso escribo: para precisar lo que sé y lo que no. + +## Bitácora + +Notas más o menos diarias sobre lo que voy pensando, viviendo, +fallando. Sin algoritmo de engagement, sin métricas. Sólo crónica. + +Las entradas se ordenan por fecha. Las más viejas a veces dicen cosas +que ya no pienso así — las dejo igual. + +## Por qué publicarlo + +Porque escribir en público obliga a precisar. Y porque a veces lo que +uno escribe para sí mismo le sirve a otra persona que no conoce. + +## Próximamente + +*Acá se va a ir armando una bitácora con entradas fechadas.* diff --git a/crates/apps/gioser-web/docs/tierra.md b/crates/apps/gioser-web/docs/tierra.md new file mode 100644 index 0000000..42f9a39 --- /dev/null +++ b/crates/apps/gioser-web/docs/tierra.md @@ -0,0 +1,37 @@ +--- +title: Manifiesto · Invariantes +camino: kay +tags: [manifiesto, principios, invariantes, ética] +--- + +> *Lo que no cambia. La piedra de toque.* + +Acá vive el manifiesto de GioSer: las **invariantes** que sostienen +todo lo demás. Lo que no negocio, lo que define la forma del trabajo +antes que cualquier proyecto particular. + +## Invariantes + +Cosas que considero **no-negociables** en cómo hago el trabajo: + +- **Código abierto por defecto.** Si tiene sentido, se publica. +- **Honestidad por encima de marketing.** No prometo lo que no puedo + cumplir, ni vendo lo que no probé. +- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no + opuesto al trabajo. Sin cuerpo no hay nada. +- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía + no existe la idea. +- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes + duran, las modas no. +- **Una sola voz.** Lo que digo en privado coincide con lo que publico. + +## Por qué un manifiesto + +Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de +principios reduce la energía gastada en cada elección — y deja en +claro cuándo estoy contradiciéndome. + +## Revisión + +Este manifiesto se revisa una vez al año, no antes. Si una invariante +deja de aplicarse, se quita con una explicación pública. diff --git a/scripts/index-gioser-docs.py b/scripts/index-gioser-docs.py new file mode 100644 index 0000000..a59e10a --- /dev/null +++ b/scripts/index-gioser-docs.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Indexador de docs/ de gioser-web → Qdrant. + +Recorre crates/apps/gioser-web/docs/, parsea YAML frontmatter, +trocea cada documento en fragmentos de párrafo, pide embeddings al +servicio agnóstico y hace upsert a Qdrant. + +Uso: + python scripts/index-gioser-docs.py # usa defaults + python scripts/index-gioser-docs.py --rebuild # recrea colección + python scripts/index-gioser-docs.py --docs ./docs --rebuild # docs custom +""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import re +import sys +import uuid +from dataclasses import dataclass +from pathlib import Path + +import httpx +import yaml +from qdrant_client import QdrantClient +from qdrant_client.http import models as qm + + +DEFAULT_DOCS = Path(__file__).resolve().parent.parent / "crates/apps/gioser-web/docs" +DEFAULT_QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333") +DEFAULT_EMBED_URL = os.getenv("EMBEDDINGS_URL", "http://localhost:8001") +DEFAULT_COLLECTION = os.getenv("QDRANT_COLLECTION", "gioser") + +VALID_CAMINOS = {"logos", "uku", "kay", "nomos", "aire", "fuego", "tierra", "agua"} +FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?\n)---\s*\n(.*)$", re.DOTALL) + + +@dataclass +class Chunk: + doc_id: str + chunk_index: int + text: str + title: str + camino: str + tags: list[str] + + +def parse_md(path: Path) -> tuple[dict, str]: + raw = path.read_text(encoding="utf-8") + m = FRONTMATTER_RE.match(raw) + if m: + meta = yaml.safe_load(m.group(1)) or {} + body = m.group(2) + else: + meta = {} + body = raw + return meta, body + + +def chunk_body(body: str, min_chars: int = 200, max_chars: int = 900) -> list[str]: + """Fragmenta por párrafos respetando un mínimo y un máximo.""" + paragraphs = [p.strip() for p in re.split(r"\n\s*\n", body) if p.strip()] + chunks: list[str] = [] + buf = "" + for p in paragraphs: + candidate = f"{buf}\n\n{p}".strip() if buf else p + if len(candidate) >= max_chars: + if buf: + chunks.append(buf) + buf = p + else: + buf = candidate + if len(buf) >= min_chars: + chunks.append(buf) + buf = "" + if buf: + if chunks and len(buf) < min_chars: + chunks[-1] = f"{chunks[-1]}\n\n{buf}" + else: + chunks.append(buf) + return chunks + + +def discover_chunks(docs_dir: Path) -> list[Chunk]: + out: list[Chunk] = [] + for path in sorted(docs_dir.rglob("*.md")): + meta, body = parse_md(path) + camino = (meta.get("camino") or path.stem).lower() + if camino not in VALID_CAMINOS: + print(f" ⚠ saltando {path}: camino '{camino}' inválido", file=sys.stderr) + continue + title = meta.get("title") or path.stem.replace("-", " ").title() + tags = list(meta.get("tags") or []) + doc_id = meta.get("id") or hashlib.sha1(str(path).encode()).hexdigest()[:12] + for i, chunk in enumerate(chunk_body(body)): + out.append( + Chunk( + doc_id=doc_id, + chunk_index=i, + text=chunk, + title=title if i == 0 else f"{title} · §{i + 1}", + camino=camino, + tags=tags, + ) + ) + return out + + +def embed_batches(http: httpx.Client, embed_url: str, texts: list[str], batch: int = 32) -> list[list[float]]: + out: list[list[float]] = [] + for i in range(0, len(texts), batch): + chunk = texts[i : i + batch] + r = http.post( + f"{embed_url}/embed", + json={"texts": chunk, "kind": "passage", "normalize": True}, + timeout=120.0, + ) + r.raise_for_status() + out.extend(r.json()["vectors"]) + return out + + +def ensure_collection(qdrant: QdrantClient, name: str, dim: int, rebuild: bool): + existing = {c.name for c in qdrant.get_collections().collections} + if name in existing and rebuild: + qdrant.delete_collection(name) + existing.discard(name) + if name not in existing: + qdrant.create_collection( + collection_name=name, + vectors_config=qm.VectorParams(size=dim, distance=qm.Distance.COSINE), + ) + + +def main(): + ap = argparse.ArgumentParser(description="Indexa docs/ de gioser-web en Qdrant") + ap.add_argument("--docs", default=str(DEFAULT_DOCS)) + ap.add_argument("--qdrant", default=DEFAULT_QDRANT_URL) + ap.add_argument("--embed", default=DEFAULT_EMBED_URL) + ap.add_argument("--collection", default=DEFAULT_COLLECTION) + ap.add_argument("--rebuild", action="store_true", help="borra y recrea la colección") + args = ap.parse_args() + + docs_dir = Path(args.docs) + if not docs_dir.is_dir(): + sys.exit(f"docs no existe: {docs_dir}") + + chunks = discover_chunks(docs_dir) + if not chunks: + sys.exit("no se encontraron docs para indexar") + print(f"→ {len(chunks)} fragmentos descubiertos") + + with httpx.Client() as http: + health = http.get(f"{args.embed}/health", timeout=10.0).json() + dim = int(health["dim"]) + print(f"→ embeddings: {health['model']} (dim={dim})") + + qdrant = QdrantClient(url=args.qdrant) + ensure_collection(qdrant, args.collection, dim, rebuild=args.rebuild) + + vectors = embed_batches(http, args.embed, [c.text for c in chunks]) + + points = [ + qm.PointStruct( + id=str(uuid.uuid5(uuid.NAMESPACE_URL, f"{c.doc_id}:{c.chunk_index}")), + vector=v, + payload={ + "doc_id": c.doc_id, + "chunk_index": c.chunk_index, + "title": c.title, + "text": c.text, + "camino": c.camino, + "tags": c.tags, + "source": "gioser-web", + }, + ) + for c, v in zip(chunks, vectors) + ] + qdrant.upsert(collection_name=args.collection, points=points) + print(f"✓ {len(points)} puntos en colección '{args.collection}'") + + if args.rebuild: + print(" ✔ colección recreada") + + +if __name__ == "__main__": + main()