Skip to content

Commit d90363a

Browse files
committed
feat(agent):to support ml_xxx tools in agent
1: to refactor impl of ml_xxx 2: to support new tools, ml_xxxx, in agent. Issues:#791
1 parent fedc00d commit d90363a

31 files changed

Lines changed: 1838 additions & 125 deletions

ml/auto_ml.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "ml_forecasting.h"
4646
#include "ml_recommendation.h"
4747
#include "ml_regression.h"
48+
#include "ml_topic_modeling.h"
4849
#include "ml_utils.h"
4950

5051
namespace ShannonBase {
@@ -123,6 +124,18 @@ void Auto_ML::build_task(std::string_view task_str) {
123124
down_cast<ML_forecasting *>(m_ml_task.get())->set_options(m_options);
124125
down_cast<ML_forecasting *>(m_ml_task.get())->set_handle_name(m_handler);
125126
break;
127+
case ML_TASK_TYPE_T::LOG_ANOMALY_DETECTION:
128+
if (m_ml_task == nullptr || (m_ml_task->type() != ML_TASK_TYPE_T::ANOMALY_DETECTION &&
129+
m_ml_task->type() != ML_TASK_TYPE_T::LOG_ANOMALY_DETECTION))
130+
m_ml_task = std::make_unique<ML_anomaly_detection>();
131+
132+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_schema(m_schema_name);
133+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_table(m_table_name);
134+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_target(m_target_name);
135+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_options(m_options);
136+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_handle_name(m_handler);
137+
down_cast<ML_anomaly_detection *>(m_ml_task.get())->set_is_logad(true);
138+
break;
126139
case ML_TASK_TYPE_T::ANOMALY_DETECTION:
127140
if (m_ml_task == nullptr || m_ml_task->type() != ML_TASK_TYPE_T::ANOMALY_DETECTION)
128141
m_ml_task = std::make_unique<ML_anomaly_detection>();
@@ -143,6 +156,16 @@ void Auto_ML::build_task(std::string_view task_str) {
143156
down_cast<ML_recommendation *>(m_ml_task.get())->set_options(m_options);
144157
down_cast<ML_recommendation *>(m_ml_task.get())->set_handle_name(m_handler);
145158
break;
159+
case ML_TASK_TYPE_T::TOPIC_MODELING:
160+
if (m_ml_task == nullptr || m_ml_task->type() != ML_TASK_TYPE_T::TOPIC_MODELING)
161+
m_ml_task = std::make_unique<ML_topic_modeling>();
162+
163+
down_cast<ML_topic_modeling *>(m_ml_task.get())->set_schema(m_schema_name);
164+
down_cast<ML_topic_modeling *>(m_ml_task.get())->set_table(m_table_name);
165+
down_cast<ML_topic_modeling *>(m_ml_task.get())->set_target(m_target_name);
166+
down_cast<ML_topic_modeling *>(m_ml_task.get())->set_options(m_options);
167+
down_cast<ML_topic_modeling *>(m_ml_task.get())->set_handle_name(m_handler);
168+
break;
146169
default:
147170
break;
148171
}

ml/ml_anomaly_detection.cpp

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "include/mysqld_error.h"
3737
#include "sql/current_thd.h"
3838
#include "sql/sql_class.h"
39+
#include "sql/sql_error.h"
3940

4041
#include "ml_utils.h"
4142
#include "storage/rapid_engine/include/rapid_config.h"
@@ -201,21 +202,42 @@ static double calc_neg_log_loss(size_t n, const std::vector<double> &pred, const
201202
} // namespace
202203

203204
int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper &model_metadata) {
204-
if (m_target_name.length()) {
205-
my_error(ER_ML_FAIL, MYF(0), "anomaly detection does not support a target column; set it to NULL");
206-
return HA_ERR_GENERIC;
207-
}
208-
205+
// For unsupervised (non-semi-supervised, non-logad), target must be NULL
206+
// Semi-supervised and log_anomaly_detection may have target
209207
OPTION_VALUE_T options;
210208
std::string keystr;
211209
if (!m_options.empty() && Utils::parse_json(m_options, options, keystr, 0)) return HA_ERR_GENERIC;
212210

211+
// Handle log_anomaly_detection options
212+
bool is_logad = m_is_logad;
213+
std::string additional_masking_regex, log_source_column, embedding_model, keyword_model;
214+
int window_size = 10, window_stride = 3;
215+
if (is_logad) {
216+
Utils::parse_logad_options(m_options, additional_masking_regex, window_size, window_stride, log_source_column,
217+
embedding_model, keyword_model);
218+
// Handle exclude_column_list for logad: exclude non-required columns (PKs etc.)
219+
if (options.find(ML_KEYWORDS::exclude_column_list) != options.end()) {
220+
// Already handled by parse_common_options, applied via read_data
221+
}
222+
}
223+
213224
double contamination = ML_anomaly_detection::default_contamination;
214225
if (options.find(ML_KEYWORDS::contamination) != options.end())
215226
contamination = std::stod(options[ML_KEYWORDS::contamination][0]);
227+
if (contamination <= 0 || contamination >= 0.5) contamination = ML_anomaly_detection::default_contamination;
216228

217-
std::vector<std::string> model_list;
229+
std::vector<std::string> model_list, exclude_model_list_str;
218230
if (options.find(ML_KEYWORDS::model_list) != options.end()) model_list = options[ML_KEYWORDS::model_list];
231+
if (options.find(ML_KEYWORDS::exclude_model_list) != options.end())
232+
exclude_model_list_str = options[ML_KEYWORDS::exclude_model_list];
233+
Utils::apply_exclude_model_list(model_list, exclude_model_list_str);
234+
235+
// Validate anomaly detection model_list: only PCA, GLOF, or GKNN
236+
if (!model_list.empty() && model_list.size() > 1) {
237+
my_error(ER_ML_FAIL, MYF(0), "anomaly_detection only supports a single model: PCA, GLOF, or GKNN");
238+
return HA_ERR_GENERIC;
239+
}
240+
if (model_list.empty()) model_list.push_back("GKNN");
219241

220242
bool semisupervised = false;
221243
int min_labels = 20, n_neighbors = 5;
@@ -231,10 +253,15 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
231253
}
232254
}
233255

234-
if (!semisupervised && !m_target_name.empty()) {
256+
// For log_anomaly_detection, target may be non-NULL (semi-supervised logs)
257+
if (!is_logad && !semisupervised && !m_target_name.empty()) {
235258
my_error(ER_ML_FAIL, MYF(0), "unsupervised anomaly_detection requires target_column_name = NULL");
236259
return HA_ERR_GENERIC;
237260
}
261+
// For unsupervised (including logad), target may be NULL
262+
if (!semisupervised && !is_logad && !m_target_name.empty()) {
263+
// This is the legacy unsupervised check already handled above
264+
}
238265
if (semisupervised && m_target_name.empty()) {
239266
my_error(ER_ML_FAIL, MYF(0), "semi-supervised anomaly_detection requires a non-NULL target_column_name");
240267
return HA_ERR_GENERIC;
@@ -256,14 +283,33 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
256283
return HA_ERR_GENERIC;
257284
}
258285

286+
// Validate table size (10 GB, 100M rows, 1017 columns)
287+
if (Utils::validate_table_size(source_table_ptr)) {
288+
Utils::close_table(source_table_ptr);
289+
return HA_ERR_GENERIC;
290+
}
291+
292+
// Validate target not text (for semi-supervised)
293+
if (!m_target_name.empty() && Utils::validate_target_not_text(source_table_ptr, m_target_name)) {
294+
Utils::close_table(source_table_ptr);
295+
return HA_ERR_GENERIC;
296+
}
297+
298+
// Parse exclude_column_list for read_data
299+
std::vector<std::string> include_cols, exclude_cols;
300+
if (options.find(ML_KEYWORDS::include_column_list) != options.end())
301+
include_cols = options[ML_KEYWORDS::include_column_list];
302+
if (options.find(ML_KEYWORDS::exclude_column_list) != options.end())
303+
exclude_cols = options[ML_KEYWORDS::exclude_column_list];
304+
259305
std::vector<double> train_data;
260306
std::vector<float> label_data;
261307
std::vector<std::string> features_name;
262308
int n_class{0};
263309
txt2numeric_map_t txt2num_dict;
264310
std::string target_name = semisupervised ? m_target_name : "";
265-
auto n_sample =
266-
Utils::read_data(source_table_ptr, train_data, features_name, target_name, label_data, n_class, txt2num_dict);
311+
auto n_sample = Utils::read_data(source_table_ptr, train_data, features_name, target_name, label_data, n_class,
312+
txt2num_dict, &include_cols, &exclude_cols);
267313
Utils::close_table(source_table_ptr);
268314

269315
if (n_sample == 0 || features_name.empty()) {
@@ -318,22 +364,65 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
318364
oss.clear();
319365
oss.str("");
320366
oss << m_sch_name << "." << m_table_name;
321-
std::string sch_tb_name(oss.str()), notes, opt_metrics;
367+
std::string sch_tb_name(oss.str()), notes_str, opt_metrics;
368+
369+
// Parse notes if present
370+
if (options.find(ML_KEYWORDS::notes) != options.end()) notes_str = options[ML_KEYWORDS::notes][0];
322371

323372
auto content_dom = Json_dom::parse(
324373
model_content.c_str(), model_content.length(), [](const char *, size_t) { assert(false); },
325374
[] { assert(false); });
326375
if (!content_dom.get()) return HA_ERR_GENERIC;
327376
model_object = Json_wrapper(std::move(content_dom));
328377

378+
// Build training_params JSON with logad options if applicable
379+
std::string training_params_str = mode_params;
380+
if (is_logad) {
381+
training_params_str += " logad_window_size=" + std::to_string(window_size) +
382+
" logad_window_stride=" + std::to_string(window_stride) +
383+
" logad_embedding_model=" + embedding_model + " logad_keyword_model=" + keyword_model;
384+
if (!log_source_column.empty()) training_params_str += " logad_source_column=" + log_source_column;
385+
}
386+
387+
// Determine model quality from training score
388+
std::string model_quality = MODEL_QUALITIES_MAP[MODEL_QUALITY_T::HIGH];
389+
double training_score = 0;
390+
// For unsupervised, use contamination-based heuristic
391+
if (training_score < -0.5) model_quality = MODEL_QUALITIES_MAP[MODEL_QUALITY_T::LOW];
392+
393+
// Build logad_options JSON for metadata
394+
Json_object *logad_meta = nullptr;
395+
if (is_logad) {
396+
logad_meta = new (std::nothrow) Json_object();
397+
if (logad_meta) {
398+
logad_meta->add_alias(ML_KEYWORDS::window_size, new (std::nothrow) Json_int(window_size));
399+
logad_meta->add_alias(ML_KEYWORDS::window_stride, new (std::nothrow) Json_int(window_stride));
400+
logad_meta->add_alias(ML_KEYWORDS::embedding_model, new (std::nothrow) Json_string(embedding_model));
401+
logad_meta->add_alias(ML_KEYWORDS::keyword_model, new (std::nothrow) Json_string(keyword_model));
402+
if (!log_source_column.empty())
403+
logad_meta->add_alias(ML_KEYWORDS::log_source_column, new (std::nothrow) Json_string(log_source_column));
404+
}
405+
}
406+
329407
auto meta_json = Utils::build_up_model_metadata(
330-
TASK_NAMES_MAP[type()], m_target_name, sch_tb_name, features_name, nullptr, notes,
331-
MODEL_FORMATS_MAP[MODEL_FORMAT_T::VER_1], MODEL_STATUS_MAP[MODEL_STATUS_T::READY],
332-
MODEL_QUALITIES_MAP[MODEL_QUALITY_T::HIGH], train_duration, TASK_NAMES_MAP[type()], 0, n_sample, n_feature,
333-
n_sample, n_feature, opt_metrics, features_name, contamination, &m_options, mode_params, nullptr, nullptr,
334-
nullptr, 1, txt2num_dict);
408+
TASK_NAMES_MAP[m_is_logad ? ML_TASK_TYPE_T::LOG_ANOMALY_DETECTION : ML_TASK_TYPE_T::ANOMALY_DETECTION],
409+
m_target_name, sch_tb_name, features_name, nullptr, notes_str, MODEL_FORMATS_MAP[MODEL_FORMAT_T::VER_2],
410+
MODEL_STATUS_MAP[MODEL_STATUS_T::READY], model_quality, train_duration,
411+
TASK_NAMES_MAP[m_is_logad ? ML_TASK_TYPE_T::LOG_ANOMALY_DETECTION : ML_TASK_TYPE_T::ANOMALY_DETECTION],
412+
training_score, n_sample, n_feature, n_sample, n_feature, opt_metrics, features_name, contamination, &m_options,
413+
training_params_str, nullptr, nullptr, logad_meta, 1, txt2num_dict);
335414

336415
model_metadata = Json_wrapper(meta_json);
416+
417+
// Model quality warning
418+
if (model_quality == MODEL_QUALITIES_MAP[MODEL_QUALITY_T::LOW]) {
419+
THD *thd = current_thd;
420+
if (thd) {
421+
push_warning_printf(thd, Sql_condition::SL_WARNING, ER_ML_FAIL,
422+
"Model has a low training score, expect low quality model explanations");
423+
}
424+
}
425+
337426
return 0;
338427
}
339428

ml/ml_anomaly_detection.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ class ML_anomaly_detection : public ML_algorithm {
8484
};
8585

8686
static std::map<std::string, SCORE_METRIC_T> score_metrics;
87-
static constexpr float default_contamination = 0.1f;
87+
static constexpr float default_contamination = 0.01f;
88+
89+
void set_is_logad(bool is_logad) { m_is_logad = is_logad; }
90+
bool is_logad() const { return m_is_logad; }
8891

8992
private:
9093
// source data schema name.
@@ -98,6 +101,9 @@ class ML_anomaly_detection : public ML_algorithm {
98101
// model options JSON format.
99102
Json_wrapper m_options;
100103

104+
// flag indicating if this is a log anomaly detection task
105+
bool m_is_logad{false};
106+
101107
void *m_handler{nullptr};
102108
};
103109
} // namespace ML

ml/ml_classification.cpp

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,10 @@ MODEL_PREDICTION_EXP_T ML_classification::parse_option(Json_wrapper &options) {
181181

182182
int ML_classification::train(THD * /*thd*/, Json_wrapper &model_object, Json_wrapper &model_metadata) {
183183
std::vector<std::string> include_cols, exclude_cols, model_list, exclude_model_list;
184-
std::string optimization_metric;
184+
std::string optimization_metric, notes;
185185
Utils::parse_common_options(m_options, include_cols, exclude_cols, model_list, exclude_model_list,
186-
optimization_metric);
186+
optimization_metric, notes);
187+
Utils::apply_exclude_model_list(model_list, exclude_model_list);
187188

188189
auto share = ShannonBase::shannon_loaded_tables->get(m_sch_name.c_str(), m_table_name.c_str());
189190
if (!share) {
@@ -201,6 +202,18 @@ int ML_classification::train(THD * /*thd*/, Json_wrapper &model_object, Json_wra
201202
return HA_ERR_GENERIC;
202203
}
203204

205+
// Validate table size limits
206+
if (Utils::validate_table_size(source_table_ptr)) {
207+
Utils::close_table(source_table_ptr);
208+
return HA_ERR_GENERIC;
209+
}
210+
211+
// Validate target is not text type
212+
if (Utils::validate_target_not_text(source_table_ptr, m_target_name)) {
213+
Utils::close_table(source_table_ptr);
214+
return HA_ERR_GENERIC;
215+
}
216+
204217
std::vector<double> train_data;
205218
std::vector<float> label_data;
206219
std::vector<std::string> features_name, target_names;
@@ -256,20 +269,24 @@ int ML_classification::train(THD * /*thd*/, Json_wrapper &model_object, Json_wra
256269
oss.clear();
257270
oss.str("");
258271
oss << m_sch_name << "." << m_table_name;
259-
std::string sch_tb_name(oss.str()), notes, opt_metrics;
272+
std::string sch_tb_name(oss.str()), opt_metrics;
260273

261274
auto content_dom = Json_dom::parse(
262275
model_content.c_str(), model_content.length(), [](const char *, size_t) { assert(false); },
263276
[] { assert(false); });
264277
if (!content_dom.get()) return HA_ERR_GENERIC;
265278
model_object = Json_wrapper(std::move(content_dom));
266279

267-
auto meta_json =
268-
Utils::build_up_model_metadata(TASK_NAMES_MAP[type()], m_target_name, sch_tb_name, features_name, nullptr, notes,
269-
MODEL_FORMATS_MAP[MODEL_FORMAT_T::VER_1], MODEL_STATUS_MAP[MODEL_STATUS_T::READY],
270-
MODEL_QUALITIES_MAP[MODEL_QUALITY_T::HIGH], train_duration, TASK_NAMES_MAP[type()],
271-
0, n_sample, n_feature + 1, n_sample, n_feature, opt_metrics, features_name, 0,
272-
&m_options, mode_params, nullptr, nullptr, nullptr, 1, txt2num_dict);
280+
// Auto-run ML_EXPLAIN: compute permutation importance
281+
Json_object *model_explanation = Utils::compute_permutation_importance(model_content, train_data, features_name,
282+
n_sample, n_feature, label_data, type());
283+
284+
auto meta_json = Utils::build_up_model_metadata(
285+
TASK_NAMES_MAP[type()], m_target_name, sch_tb_name, features_name, model_explanation, notes,
286+
MODEL_FORMATS_MAP[MODEL_FORMAT_T::VER_2], MODEL_STATUS_MAP[MODEL_STATUS_T::READY],
287+
MODEL_QUALITIES_MAP[MODEL_QUALITY_T::HIGH], train_duration, TASK_NAMES_MAP[type()], 0, n_sample, n_feature,
288+
n_sample, n_feature, opt_metrics, features_name, 0, &m_options, mode_params, nullptr, nullptr, nullptr, 1,
289+
txt2num_dict);
273290

274291
model_metadata = Json_wrapper(meta_json);
275292
return 0;

0 commit comments

Comments
 (0)