f771c9b883
- CONVENTIONS.md - tools/dedup_persons.py - tools/extract_entities.py - tools/migrate_external_orgs.py - tools/normalize_person_frontmatter.py - tools/person_datapoints.py Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
6.1 KiB
Python
137 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Extrae entidades (personas y organizaciones) mencionadas en los titulos de un vault
|
|
Obsidian, via ask_llm (NER). Genera fichas-indice en osint que documentan cada entidad y las
|
|
notas donde aparece (referencia textual; los vaults son separados, no hay wikilink cross-vault).
|
|
|
|
Uso:
|
|
extract_entities.py <Vault> [--apply]
|
|
Sin --apply: solo imprime el preview (no escribe nada).
|
|
|
|
NO mueve ni modifica el vault de origen: solo lee titulos y, con --apply, crea fichas en osint.
|
|
No sobreescribe fichas de persona/organizacion ya existentes en osint (no pisa datos ricos).
|
|
"""
|
|
import sys, os, json, re
|
|
from collections import defaultdict
|
|
|
|
sys.path.insert(0, "/home/enmanuel/fn_registry/python/functions")
|
|
from core.ask_llm import ask_llm
|
|
from obsidian import list_obsidian_notes, slugify_obsidian_name, create_obsidian_note
|
|
|
|
OBS = "/home/enmanuel/Obsidian"
|
|
OSINT = f"{OBS}/osint"
|
|
BATCH = 60
|
|
|
|
|
|
def vault_titles(vault):
|
|
vp = f"{OBS}/{vault}"
|
|
out = []
|
|
for n in list_obsidian_notes(vp):
|
|
if "/.git/" in n or "/dist/" in n:
|
|
continue
|
|
out.append(os.path.basename(n)[:-3])
|
|
return sorted(set(out))
|
|
|
|
|
|
def ner_batch(titles, vault):
|
|
listado = "\n".join(f"{i}. {t}" for i, t in enumerate(titles))
|
|
prompt = (
|
|
f"Estos son titulos de notas de un vault Obsidian ('{vault}'). Extrae entidades reales:\n"
|
|
"- personas: nombres propios de individuos reales (colegas, clientes, ponentes, contactos).\n"
|
|
"- organizaciones: empresas/entidades reales con las que se trabaja (clientes, proveedores, partners, mutuas).\n"
|
|
"EXCLUYE estrictamente: herramientas y productos de software (BigQuery, Looker, Metabase, "
|
|
"Navision, Palantir, Dask, ElasticSearch, Excel, Google Cloud, Fivetran, OTRS, TPV, Beats, "
|
|
"Looker Studio, Visual Studio), conceptos tecnicos, lenguajes, y la propia empresa Aurgi.\n"
|
|
"Solo incluye una persona si su nombre propio aparece explicito. No inventes.\n"
|
|
'Devuelve SOLO JSON: {"personas": {"Nombre Apellido": [indices]}, "organizaciones": {"Nombre": [indices]}}.\n'
|
|
"Indices 0-based de los titulos donde aparece la entidad.\n\n" + listado
|
|
)
|
|
raw = ask_llm(prompt, model="claude-haiku-4-5-20251001", echo=False)
|
|
m = re.search(r'\{.*\}', raw, re.S)
|
|
if not m:
|
|
return {"personas": {}, "organizaciones": {}}
|
|
try:
|
|
return json.loads(m.group(0))
|
|
except Exception:
|
|
return {"personas": {}, "organizaciones": {}}
|
|
|
|
|
|
# Nombres que el NER suele clasificar mal como persona pero son organizaciones/sistemas.
|
|
NOT_PERSON = {"anjana"}
|
|
|
|
|
|
def extract(vault):
|
|
titles = vault_titles(vault)
|
|
personas, orgs = defaultdict(set), defaultdict(set)
|
|
for b in range(0, len(titles), BATCH):
|
|
chunk = titles[b:b + BATCH]
|
|
res = ner_batch(chunk, vault)
|
|
for nombre, idxs in (res.get("personas") or {}).items():
|
|
for i in idxs:
|
|
if isinstance(i, int) and 0 <= i < len(chunk):
|
|
personas[nombre.strip()].add(chunk[i])
|
|
for nombre, idxs in (res.get("organizaciones") or {}).items():
|
|
for i in idxs:
|
|
if isinstance(i, int) and 0 <= i < len(chunk):
|
|
orgs[nombre.strip()].add(chunk[i])
|
|
# reclasificar falsos-persona conocidos como organizacion
|
|
for n in list(personas):
|
|
if slugify_obsidian_name(n) in NOT_PERSON:
|
|
orgs[n] |= personas.pop(n)
|
|
return titles, personas, orgs
|
|
|
|
|
|
def make_fichas(vault, personas, orgs):
|
|
created, skipped = [], []
|
|
for nombre, notas in personas.items():
|
|
slug = slugify_obsidian_name(nombre)
|
|
if not slug or len(slug) < 3:
|
|
continue
|
|
if os.path.exists(f"{OSINT}/personas/{slug}.md"):
|
|
skipped.append(("persona", nombre)); continue
|
|
body = [f"Contacto detectado en el vault {vault} (red profesional).", "",
|
|
"## Aparece en", ""] + [f"- {t} _({vault})_" for t in sorted(notas)] + ["", "## Notas", ""]
|
|
create_obsidian_note(OSINT, f"personas/{slug}", body="\n".join(body),
|
|
frontmatter={"tipo": "persona", "nombre": nombre, "slug": slug,
|
|
"contexto": vault.lower(), "tags": ["persona", "osint", "contacto"],
|
|
"fuente": vault}, overwrite=False)
|
|
created.append(("persona", nombre))
|
|
for nombre, notas in orgs.items():
|
|
slug = slugify_obsidian_name(nombre)
|
|
if not slug or len(slug) < 2:
|
|
continue
|
|
if os.path.exists(f"{OSINT}/organizaciones/{slug}.md"):
|
|
skipped.append(("org", nombre)); continue
|
|
body = [f"Organizacion detectada en el vault {vault} (red profesional).", "",
|
|
"## Aparece en", ""] + [f"- {t} _({vault})_" for t in sorted(notas)] + ["", "## Notas", ""]
|
|
create_obsidian_note(OSINT, f"organizaciones/{slug}", body="\n".join(body),
|
|
frontmatter={"tipo": "organizacion", "nombre": nombre, "slug": slug,
|
|
"externa": True, "contexto": vault.lower(),
|
|
"tags": ["organizacion", "externa", "osint"], "fuente": vault},
|
|
overwrite=False)
|
|
created.append(("org", nombre))
|
|
return created, skipped
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("uso: extract_entities.py <Vault> [--apply]"); return
|
|
vault = sys.argv[1]
|
|
apply = "--apply" in sys.argv
|
|
titles, personas, orgs = extract(vault)
|
|
print(f"{vault}: {len(titles)} titulos | personas={len(personas)} | organizaciones={len(orgs)}\n")
|
|
print("PERSONAS:")
|
|
for n in sorted(personas, key=lambda x: -len(personas[x])):
|
|
print(f" {n} ({len(personas[n])} notas)")
|
|
print("\nORGANIZACIONES:")
|
|
for n in sorted(orgs, key=lambda x: -len(orgs[x])):
|
|
print(f" {n} ({len(orgs[n])} notas)")
|
|
if apply:
|
|
created, skipped = make_fichas(vault, personas, orgs)
|
|
print(f"\nfichas creadas: {len(created)} | omitidas (ya existian): {len(skipped)}")
|
|
if skipped:
|
|
print(" omitidas:", ", ".join(n for _, n in skipped[:15]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|