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:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user