chore: auto-commit (6 archivos)
- 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>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user