advisor runs to update new and existing profiles if new field occur
This commit is contained in:
@@ -79,6 +79,50 @@ def _public_runtime_config(config: dict[str, object]) -> dict[str, object]:
|
|||||||
return public
|
return public
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_field_set(profile: object | None) -> set[str]:
|
||||||
|
if profile is None:
|
||||||
|
return set()
|
||||||
|
fields = {
|
||||||
|
str(getattr(profile, "entity_field", "") or ""),
|
||||||
|
str(getattr(profile, "timestamp_field", "") or ""),
|
||||||
|
}
|
||||||
|
fields.update(str(field) for field in getattr(profile, "entity_fields", ()) if field)
|
||||||
|
fields.update(str(field) for field in getattr(profile, "categorical_fields", ()) if field)
|
||||||
|
fields.update(str(field) for field in getattr(profile, "numeric_fields", ()) if field)
|
||||||
|
for relation in getattr(profile, "relationship_fields", ()):
|
||||||
|
fields.add(str(getattr(relation, "left", "") or ""))
|
||||||
|
fields.add(str(getattr(relation, "right", "") or ""))
|
||||||
|
return {field for field in fields if field}
|
||||||
|
|
||||||
|
|
||||||
|
def _suggestion_field_set(suggestion: dict[str, object]) -> set[str]:
|
||||||
|
profile = suggestion.get("profile", {}) if isinstance(suggestion.get("profile"), dict) else {}
|
||||||
|
fields = {
|
||||||
|
str(profile.get("entity_field", "") or ""),
|
||||||
|
str(profile.get("timestamp_field", "") or ""),
|
||||||
|
}
|
||||||
|
for key in ("entity_fields", "categorical_fields", "numeric_fields"):
|
||||||
|
value = profile.get(key, [])
|
||||||
|
if isinstance(value, list):
|
||||||
|
fields.update(str(field) for field in value if field)
|
||||||
|
relationships = profile.get("relationship_fields", [])
|
||||||
|
if isinstance(relationships, list):
|
||||||
|
for relation in relationships:
|
||||||
|
if isinstance(relation, dict):
|
||||||
|
fields.add(str(relation.get("left", "") or ""))
|
||||||
|
fields.add(str(relation.get("right", "") or ""))
|
||||||
|
return {field for field in fields if field}
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_profile_advisor(suggestion: dict[str, object], stream_profiles: dict[str, object]) -> bool:
|
||||||
|
if not suggestion.get("profile_exists"):
|
||||||
|
return True
|
||||||
|
stream_id = str(suggestion.get("stream_id", ""))
|
||||||
|
suggested_fields = _suggestion_field_set(suggestion)
|
||||||
|
existing_fields = _profile_field_set(stream_profiles.get(stream_id))
|
||||||
|
return bool(suggested_fields - existing_fields)
|
||||||
|
|
||||||
|
|
||||||
def _stream_name(stream_id: str, stream_titles: dict[str, str], profile: object | None = None) -> str:
|
def _stream_name(stream_id: str, stream_titles: dict[str, str], profile: object | None = None) -> str:
|
||||||
return stream_titles.get(stream_id) or getattr(profile, "name", "") or stream_id
|
return stream_titles.get(stream_id) or getattr(profile, "name", "") or stream_id
|
||||||
|
|
||||||
@@ -375,10 +419,13 @@ def build_status(
|
|||||||
discovery_cache_events = discovery_store.synthetic_events()
|
discovery_cache_events = discovery_store.synthetic_events()
|
||||||
profile_suggestions = suggest_stream_profiles([*discovery_cache_events, *events], existing_profiles=stream_profiles)
|
profile_suggestions = suggest_stream_profiles([*discovery_cache_events, *events], existing_profiles=stream_profiles)
|
||||||
profile_advisor_status = {"enabled": bool(runtime_values.get("profile_advisor_enabled")), "status": "disabled"}
|
profile_advisor_status = {"enabled": bool(runtime_values.get("profile_advisor_enabled")), "status": "disabled"}
|
||||||
if runtime_values.get("profile_advisor_enabled") and profile_suggestions:
|
advisor_candidates = [item for item in profile_suggestions if _needs_profile_advisor(item, stream_profiles)]
|
||||||
|
if runtime_values.get("profile_advisor_enabled") and profile_suggestions and not advisor_candidates:
|
||||||
|
profile_advisor_status = {"enabled": True, "status": "skipped_no_profile_changes", "reason": "No missing profiles or newly discovered profile fields need advisor review."}
|
||||||
|
elif runtime_values.get("profile_advisor_enabled") and profile_suggestions:
|
||||||
try:
|
try:
|
||||||
advice = ollama_profile_advice(
|
advice = ollama_profile_advice(
|
||||||
profile_suggestions,
|
advisor_candidates[:10],
|
||||||
model=str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b"),
|
model=str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b"),
|
||||||
timeout=int(runtime_values.get("profile_advisor_timeout", 120) or 120),
|
timeout=int(runtime_values.get("profile_advisor_timeout", 120) or 120),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -201,6 +201,48 @@ class MonitorTests(unittest.TestCase):
|
|||||||
self.assertEqual(status["profile_suggestions"][0]["profile_advisor"]["status"], "heuristic")
|
self.assertEqual(status["profile_suggestions"][0]["profile_advisor"]["status"], "heuristic")
|
||||||
self.assertIn("timeout", status["profile_suggestions"][0]["profile_advisor"]["error"])
|
self.assertIn("timeout", status["profile_suggestions"][0]["profile_advisor"]["error"])
|
||||||
|
|
||||||
|
def test_profile_advisor_skips_when_all_streams_have_profiles(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
log_path = Path(tmp) / "events.log"
|
||||||
|
log_path.write_text("fgai_stream_id=windows fgai_stream=Windows username=alice\n", encoding="utf-8")
|
||||||
|
config_path = Path(tmp) / "config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"profile_advisor_enabled": True,
|
||||||
|
"graylog_stream_profiles": [{"stream_id": "windows", "name": "Windows profile", "entity_field": "username", "entity_fields": ["username"], "timestamp_field": "timestamp"}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("fgai.monitor.ollama_profile_advice") as advisor:
|
||||||
|
status = build_status(str(log_path), config_path=str(config_path), incident_path=str(Path(tmp) / "incidents.json"))
|
||||||
|
|
||||||
|
advisor.assert_not_called()
|
||||||
|
self.assertEqual(status["capabilities"]["profile_advisor"]["status"], "skipped_no_profile_changes")
|
||||||
|
|
||||||
|
def test_profile_advisor_runs_when_existing_profile_has_new_fields(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
log_path = Path(tmp) / "events.log"
|
||||||
|
log_path.write_text("fgai_stream_id=windows fgai_stream=Windows username=alice eventid=4625 action=failure\n", encoding="utf-8")
|
||||||
|
config_path = Path(tmp) / "config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"profile_advisor_enabled": True,
|
||||||
|
"graylog_stream_profiles": [{"stream_id": "windows", "name": "Windows profile", "entity_field": "username", "entity_fields": ["username"], "timestamp_field": "timestamp"}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("fgai.monitor.ollama_profile_advice", return_value=[]) as advisor:
|
||||||
|
status = build_status(str(log_path), config_path=str(config_path), incident_path=str(Path(tmp) / "incidents.json"))
|
||||||
|
|
||||||
|
advisor.assert_called_once()
|
||||||
|
self.assertEqual(status["capabilities"]["profile_advisor"]["status"], "empty")
|
||||||
|
|
||||||
def test_profile_suggestions_use_cached_discovered_fields(self):
|
def test_profile_suggestions_use_cached_discovered_fields(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
history_path = str(Path(tmp) / "history.sqlite3")
|
history_path = str(Path(tmp) / "history.sqlite3")
|
||||||
|
|||||||
Reference in New Issue
Block a user