feat(seo): fijar vocabulario de tags ES en el autofill
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Cierra el TODO de ALLOWED_TAGS: el allow-list ES son los 6 tags del núcleo vivo de zonadeexclusion (Admin API, 2026-07-03), espejo de la lista EN confirmada: uap, desclasificados, casos-militares, casos-clasicos, investigacion, casos-espana. Quedan fuera la cola de tags de 1 post que el modelo inventó antes de constreñir y el duplicado investigacion-2 (colisión de mayúsculas en Ghost). La regla anti-legacy 'never use "investigacion"' del prompt pasa a ser solo-EN: en ES ese slug es el tag canónico del allow-list. Tests de las funciones puras (_coerce, _system_prompt) por idioma. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b872d00efc
commit
19efc70539
+13
-4
@@ -37,14 +37,19 @@ SITE_BY_LANG = {
|
|||||||
|
|
||||||
# Tag allow-list — the model may ONLY pick from these; invented tags are dropped
|
# Tag allow-list — the model may ONLY pick from these; invented tags are dropped
|
||||||
# deterministically after the call (never trust the LLM to self-limit). EN is the
|
# deterministically after the call (never trust the LLM to self-limit). EN is the
|
||||||
# live vocabulary Jose confirmed; ES is not yet constrained, so we don't enforce
|
# live vocabulary Jose confirmed; ES is the live core taxonomy on zonadeexclusion
|
||||||
# it (model picks freely there until an ES vocab is pinned down).
|
# (fetched 2026-07-03), mirroring EN. Deliberately excluded from ES: the one-post
|
||||||
|
# long-tail tags the model invented before constraining, and "investigacion-2"
|
||||||
|
# (a Ghost case-collision duplicate of "investigacion").
|
||||||
ALLOWED_TAGS = {
|
ALLOWED_TAGS = {
|
||||||
"en": [
|
"en": [
|
||||||
"uap", "declassified", "military-cases", "classic-cases",
|
"uap", "declassified", "military-cases", "classic-cases",
|
||||||
"investigation", "spain-cases", "latin-america",
|
"investigation", "spain-cases", "latin-america",
|
||||||
],
|
],
|
||||||
# "es": [...] # TODO: pin the live ES tag vocabulary before constraining.
|
"es": [
|
||||||
|
"uap", "desclasificados", "casos-militares", "casos-clasicos",
|
||||||
|
"investigacion", "casos-espana",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Language-aware default tag when nothing from the allow-list fits / the model
|
# Language-aware default tag when nothing from the allow-list fits / the model
|
||||||
@@ -116,9 +121,13 @@ async def fetch_published_menu(lang: str) -> list[dict]:
|
|||||||
def _system_prompt(lang: str) -> str:
|
def _system_prompt(lang: str) -> str:
|
||||||
allow = ", ".join(ALLOWED_TAGS.get(lang, []))
|
allow = ", ".join(ALLOWED_TAGS.get(lang, []))
|
||||||
out_lang = "SPANISH" if lang == "es" else "ENGLISH"
|
out_lang = "SPANISH" if lang == "es" else "ENGLISH"
|
||||||
|
# The anti-legacy "never use investigacion" rule is EN-only: on the ES site
|
||||||
|
# "investigacion" IS the canonical tag (and the allow-list already excludes
|
||||||
|
# the "investigacion-2" duplicate).
|
||||||
|
legacy_clause = ", never use \"investigacion\"" if lang == "en" else ""
|
||||||
tag_clause = (
|
tag_clause = (
|
||||||
f"TAGS: choose 2-4 tags ONLY from this exact list (never invent a tag, "
|
f"TAGS: choose 2-4 tags ONLY from this exact list (never invent a tag, "
|
||||||
f"never translate it, never use \"investigacion\"): {allow}.\n"
|
f"never translate it{legacy_clause}): {allow}.\n"
|
||||||
if allow else
|
if allow else
|
||||||
"TAGS: 2-4 lowercase-hyphenated topical tags appropriate to the article.\n"
|
"TAGS: 2-4 lowercase-hyphenated topical tags appropriate to the article.\n"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from src.seo.autofill import ALLOWED_TAGS, DEFAULT_TAG, _coerce, _system_prompt
|
||||||
|
|
||||||
|
BASE = {
|
||||||
|
"meta_title": "t",
|
||||||
|
"meta_description": "d",
|
||||||
|
"custom_excerpt": "e",
|
||||||
|
"image_query": "q",
|
||||||
|
"image_context": "c",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_es_drops_invented_tags():
|
||||||
|
obj = dict(BASE, tags=["uap", "humanoides", "Desclasificados", "investigacion-2"])
|
||||||
|
out = _coerce(obj, "es")
|
||||||
|
assert out["tags"] == ["uap", "desclasificados"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_es_falls_back_to_default():
|
||||||
|
obj = dict(BASE, tags=["pentagono", "encuentros-cercanos"])
|
||||||
|
out = _coerce(obj, "es")
|
||||||
|
assert out["tags"] == [DEFAULT_TAG["es"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_en_still_constrained():
|
||||||
|
obj = dict(BASE, tags=["uap", "investigacion"])
|
||||||
|
out = _coerce(obj, "en")
|
||||||
|
assert out["tags"] == ["uap"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_system_prompt_lists_allowed_tags_per_lang():
|
||||||
|
es = _system_prompt("es")
|
||||||
|
en = _system_prompt("en")
|
||||||
|
for tag in ALLOWED_TAGS["es"]:
|
||||||
|
assert tag in es
|
||||||
|
# La regla anti-legacy 'never use "investigacion"' es solo para EN: en ES
|
||||||
|
# "investigacion" es el tag canónico del allow-list.
|
||||||
|
assert 'never use "investigacion"' in en
|
||||||
|
assert 'never use "investigacion"' not in es
|
||||||
|
assert "ONLY from this exact list" in es
|
||||||
Reference in New Issue
Block a user