feat(0035e): manifest auto_group_threshold override + propagacion a Python

Manifest YAML puede declarar 'auto_group_threshold: <int>' a nivel
top-level. enrichers.cpp lo parsea y lo guarda en EnricherSpec.
jobs.cpp lo inyecta como campo opcional 'auto_group_threshold' en el
JSON stdin del subprocess. Los enrichers Python que crean Groups
(web_search, split_words, split_sentences, extract_iocs_text) leen el
campo y, si viene > 0, lo usan en lugar de su DEFAULT_GROUP_THRESHOLD.
Helper _coerce_threshold tolera int / str / None / 0 cayendo al default.
This commit is contained in:
2026-05-04 14:20:52 +02:00
parent 65a14749f3
commit 52495af779
7 changed files with 92 additions and 10 deletions
+9
View File
@@ -180,6 +180,15 @@ bool parse_manifest(const std::string& path, EnricherSpec* out) {
// Si fuese inline (`params: [{...}]`) — formato no usado en // Si fuese inline (`params: [{...}]`) — formato no usado en
// los manifests actuales, lo ignoramos. // los manifests actuales, lo ignoramos.
} }
else if (key == "auto_group_threshold") {
// Issue 0035e: override del threshold de auto-grouping. Si el
// valor no es un entero parseable, se ignora (queda en 0 =
// usar default interno del enricher).
try {
int v = std::stoi(strip_quotes(val));
if (v > 0) out->auto_group_threshold = v;
} catch (...) { /* ignore */ }
}
else if (key == "emits" && val.empty()) in_skip_block = true; else if (key == "emits" && val.empty()) in_skip_block = true;
else if (key == "relations" && val.empty()) in_skip_block = true; else if (key == "relations" && val.empty()) in_skip_block = true;
else if (key == "uses_functions" && val.empty()) in_skip_block = true; else if (key == "uses_functions" && val.empty()) in_skip_block = true;
+8
View File
@@ -48,6 +48,14 @@ struct EnricherSpec {
// Parametros editables por el usuario antes de lanzar el job. // Parametros editables por el usuario antes de lanzar el job.
std::vector<EnricherParam> params; std::vector<EnricherParam> params;
// Threshold opcional de auto-grouping (issue 0035e). Si > 0, el
// enricher debe respetarlo al decidir cuando crear un Group con sus
// resultados (vs dejarlos sueltos). Cuando es 0 / no declarado, el
// enricher usa su default interno (DEFAULT_GROUP_THRESHOLD = 50).
// Se propaga al runtime Python via campo `auto_group_threshold` del
// JSON de stdin que jobs.cpp construye.
int auto_group_threshold = 0;
// True si lang != "" y no se pudo resolver el ejecutable // True si lang != "" y no se pudo resolver el ejecutable
// correspondiente (ej: enricher Go sin compilar). El loader deja // correspondiente (ej: enricher Go sin compilar). El loader deja
// el spec en el registro pero marcado como deshabilitado para // el spec en el registro pero marcado como deshabilitado para
+14 -1
View File
@@ -48,6 +48,17 @@ DEFAULT_GROUP_THRESHOLD = 50
GROUP_PREVIEW_K = 10 GROUP_PREVIEW_K = 10
def _coerce_threshold(raw, default: int) -> int:
"""Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e)."""
if raw is None or raw == "":
return default
try:
v = int(raw)
except (TypeError, ValueError):
return default
return v if v > 0 else default
def progress(p: float, stage: str = "") -> None: def progress(p: float, stage: str = "") -> None:
sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n") sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n")
sys.stderr.flush() sys.stderr.flush()
@@ -206,7 +217,9 @@ def main() -> int:
try: try:
has_group_col = has_group_id_column(conn) has_group_col = has_group_id_column(conn)
n_total = len(unique) n_total = len(unique)
threshold = DEFAULT_GROUP_THRESHOLD # Issue 0035e: respeta override del manifest si viene en ctx.
threshold = _coerce_threshold(ctx.get("auto_group_threshold"),
DEFAULT_GROUP_THRESHOLD)
if n_total >= threshold and has_group_col: if n_total >= threshold and has_group_col:
# Group heterogeneo (provisional, ver docstring). # Group heterogeneo (provisional, ver docstring).
+14 -1
View File
@@ -38,6 +38,17 @@ from datetime import datetime, timezone
DEFAULT_GROUP_THRESHOLD = 50 DEFAULT_GROUP_THRESHOLD = 50
GROUP_PREVIEW_K = 10 GROUP_PREVIEW_K = 10
def _coerce_threshold(raw, default: int) -> int:
"""Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e)."""
if raw is None or raw == "":
return default
try:
v = int(raw)
except (TypeError, ValueError):
return default
return v if v > 0 else default
# Split por delimitador de oracion (.!?) seguido de whitespace seguido de # Split por delimitador de oracion (.!?) seguido de whitespace seguido de
# inicial de oracion en mayusculas (incluye acentos espanoles). Robusto # inicial de oracion en mayusculas (incluye acentos espanoles). Robusto
# para texto en espanol e ingles. Casos limite (abreviaturas como "Sr.", # para texto en espanol e ingles. Casos limite (abreviaturas como "Sr.",
@@ -273,7 +284,9 @@ def main() -> int:
try: try:
has_group_col = has_group_id_column(conn) has_group_col = has_group_id_column(conn)
n_total = len(sentences) n_total = len(sentences)
threshold = DEFAULT_GROUP_THRESHOLD # Issue 0035e: respeta override del manifest si viene en ctx.
threshold = _coerce_threshold(ctx.get("auto_group_threshold"),
DEFAULT_GROUP_THRESHOLD)
if n_total >= threshold and has_group_col: if n_total >= threshold and has_group_col:
group_id = insert_group_entity( group_id = insert_group_entity(
+14 -1
View File
@@ -39,6 +39,17 @@ from datetime import datetime, timezone
DEFAULT_GROUP_THRESHOLD = 50 DEFAULT_GROUP_THRESHOLD = 50
GROUP_PREVIEW_K = 10 GROUP_PREVIEW_K = 10
def _coerce_threshold(raw, default: int) -> int:
"""Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e)."""
if raw is None or raw == "":
return default
try:
v = int(raw)
except (TypeError, ValueError):
return default
return v if v > 0 else default
# Tokenizer: secuencias de letras (incluye acentos espanyoles + apostrofo # Tokenizer: secuencias de letras (incluye acentos espanyoles + apostrofo
# interno tipo "don't"). Mas robusto que split por espacios para texto # interno tipo "don't"). Mas robusto que split por espacios para texto
# real con puntuacion adyacente. Numeros se ignoran — pensado para # real con puntuacion adyacente. Numeros se ignoran — pensado para
@@ -264,7 +275,9 @@ def main() -> int:
try: try:
has_group_col = has_group_id_column(conn) has_group_col = has_group_id_column(conn)
n_total = len(words) n_total = len(words)
threshold = DEFAULT_GROUP_THRESHOLD # Issue 0035e: respeta override del manifest si viene en ctx.
threshold = _coerce_threshold(ctx.get("auto_group_threshold"),
DEFAULT_GROUP_THRESHOLD)
if n_total >= threshold and has_group_col: if n_total >= threshold and has_group_col:
group_id = insert_group_entity( group_id = insert_group_entity(
+21 -3
View File
@@ -50,6 +50,22 @@ DEFAULT_GROUP_THRESHOLD = 50
GROUP_PREVIEW_K = 10 GROUP_PREVIEW_K = 10
def _coerce_threshold(raw, default: int) -> int:
"""Acepta int / str numerico / None, devuelve >0 o el default.
Issue 0035e: el manifest puede declarar `auto_group_threshold: <int>`
y jobs.cpp lo propaga al subprocess. Cualquier otro valor (None,
"", 0, no parseable) cae al default global.
"""
if raw is None or raw == "":
return default
try:
v = int(raw)
except (TypeError, ValueError):
return default
return v if v > 0 else default
def progress(p: float, stage: str = "") -> None: def progress(p: float, stage: str = "") -> None:
sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n") sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n")
sys.stderr.flush() sys.stderr.flush()
@@ -619,9 +635,11 @@ def main() -> int:
try: try:
has_group_col = has_group_id_column(conn) has_group_col = has_group_id_column(conn)
n_total = len(results) n_total = len(results)
# Threshold: por ahora hardcoded; la lectura del manifest # Threshold: el manifest puede declarar `auto_group_threshold` y
# vendra en 0035e (settings UI / overrides por enricher). # jobs.cpp lo propaga via stdin (issue 0035e). Si no viene, se
threshold = DEFAULT_GROUP_THRESHOLD # usa el default interno del enricher.
threshold = _coerce_threshold(ctx.get("auto_group_threshold"),
DEFAULT_GROUP_THRESHOLD)
if n_total >= threshold and has_group_col: if n_total >= threshold and has_group_col:
# Modo Twitter/Reddit: K sueltos + Group con N-K hijos. # Modo Twitter/Reddit: K sueltos + Group con N-K hijos.
+12 -4
View File
@@ -391,7 +391,8 @@ std::string build_stdin_json(const std::string& job_id,
const std::string& ops_db, const std::string& ops_db,
const std::string& app_dir, const std::string& app_dir,
const std::string& registry_root, const std::string& registry_root,
const std::string& lang) const std::string& lang,
int auto_group_threshold = 0)
{ {
std::string node_type, node_name, node_metadata = "{}"; std::string node_type, node_name, node_metadata = "{}";
if (!node_id.empty()) { if (!node_id.empty()) {
@@ -457,8 +458,14 @@ std::string build_stdin_json(const std::string& job_id,
<< "\"ops_db_path\":\"" << json_escape(ops_db_out) << "\"," << "\"ops_db_path\":\"" << json_escape(ops_db_out) << "\","
<< "\"app_dir\":\"" << json_escape(app_dir_out) << "\"," << "\"app_dir\":\"" << json_escape(app_dir_out) << "\","
<< "\"cache_dir\":\"" << json_escape(cache_dir) << "\"," << "\"cache_dir\":\"" << json_escape(cache_dir) << "\","
<< "\"registry_root\":\"" << json_escape(root_out) << "\"" << "\"registry_root\":\"" << json_escape(root_out) << "\"";
<< '}'; // Issue 0035e: solo emitimos el campo si el manifest declara override.
// Asi las pruebas que NO setean el campo siguen viendo defaults estables
// y los enrichers Python solo lo leen cuando viene declarado.
if (auto_group_threshold > 0) {
o << ",\"auto_group_threshold\":" << auto_group_threshold;
}
o << '}';
return o.str(); return o.str();
} }
@@ -1050,7 +1057,8 @@ void worker_loop() {
} }
std::string stdin_payload = build_stdin_json( std::string stdin_payload = build_stdin_json(
ctx.id, ctx.enricher_id, ctx.node_id, ctx.params_json, ctx.id, ctx.enricher_id, ctx.node_id, ctx.params_json,
ops_db, g_state->app_dir, g_state->registry_root, lang); ops_db, g_state->app_dir, g_state->registry_root, lang,
spec->auto_group_threshold);
ProcResult res = run_subprocess(job_id, run_path, lang, ProcResult res = run_subprocess(job_id, run_path, lang,
stdin_payload, ctrl); stdin_payload, ctrl);