diff --git a/src/fgai/config.py b/src/fgai/config.py index 132c2a2..703eec5 100644 --- a/src/fgai/config.py +++ b/src/fgai/config.py @@ -28,7 +28,7 @@ DEFAULT_CONFIG: dict[str, object] = { "llm_model": "", "profile_advisor_enabled": False, "profile_advisor_model": "qwen3:8b", - "profile_advisor_timeout": 120, + "profile_advisor_timeout": 240, "threat_intel_enabled": False, "threat_intel_provider": "auto", "abuseipdb_api_key": "", diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index b16003b..7e43c90 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -988,7 +988,7 @@ async function applySuggestedProfile(streamId) { llm_model: config.llm_model || '', profile_advisor_enabled: Boolean(config.profile_advisor_enabled), profile_advisor_model: config.profile_advisor_model || 'qwen3:8b', - profile_advisor_timeout: config.profile_advisor_timeout || 120, + profile_advisor_timeout: config.profile_advisor_timeout || 240, threat_intel_enabled: Boolean(config.threat_intel_enabled), threat_intel_provider: config.threat_intel_provider || 'auto', threat_intel_daily_limit: config.threat_intel_daily_limit || 100, diff --git a/src/fgai/llm.py b/src/fgai/llm.py index cad420f..5e35338 100644 --- a/src/fgai/llm.py +++ b/src/fgai/llm.py @@ -129,7 +129,7 @@ def _compact_profile_suggestion(item: dict[str, object]) -> dict[str, object]: def ollama_profile_advice(suggestions: list[dict[str, object]], model: str | None = None, timeout: int | None = None) -> list[dict[str, object]]: host = os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/") selected_model = model or os.getenv("FGAI_PROFILE_ADVISOR_MODEL", "qwen3:8b") - selected_timeout = timeout or int(os.getenv("FGAI_PROFILE_ADVISOR_TIMEOUT", "120")) + selected_timeout = timeout or int(os.getenv("FGAI_PROFILE_ADVISOR_TIMEOUT", "240")) compact = [_compact_profile_suggestion(item) for item in suggestions[:10]] body = json.dumps( { diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index 318512b..5b67640 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -219,6 +219,7 @@ def build_status( history_path: str | None = None, incident_path: str | None = None, status_cache_path: str | None = None, + run_profile_advisor: bool = True, ) -> dict[str, object]: config_store = ConfigStore(config_path) if config_path else None config_exists = bool(config_store and config_store.path.exists()) @@ -420,14 +421,16 @@ def build_status( 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"} 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: + if runtime_values.get("profile_advisor_enabled") and not run_profile_advisor: + profile_advisor_status = {"enabled": True, "status": "pending" if advisor_candidates else "skipped_no_profile_changes", "candidates": len(advisor_candidates)} + elif 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: advice = ollama_profile_advice( advisor_candidates[:10], 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", 240) or 240), ) profile_suggestions = apply_profile_advice(profile_suggestions, advice) profile_advisor_status = {"enabled": True, "status": "ok" if advice else "empty", "profiles_returned": len(advice), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")} @@ -605,6 +608,42 @@ def add_llm_assessment(status: dict[str, object], *, previous: str | None = None } +def add_profile_advisor(status: dict[str, object], runtime_values: dict[str, object]) -> None: + profile_suggestions = status.get("profile_suggestions", []) + if not isinstance(profile_suggestions, list): + return + stream_profiles = parse_profiles(runtime_values.get("graylog_stream_profiles", [])) + advisor_candidates = [item for item in profile_suggestions if isinstance(item, dict) and _needs_profile_advisor(item, stream_profiles)] + capabilities = status.setdefault("capabilities", {}) + if not isinstance(capabilities, dict): + return + if not runtime_values.get("profile_advisor_enabled"): + capabilities["profile_advisor"] = {"enabled": False, "status": "disabled"} + return + if not profile_suggestions or not advisor_candidates: + capabilities["profile_advisor"] = {"enabled": True, "status": "skipped_no_profile_changes", "reason": "No missing profiles or newly discovered profile fields need advisor review."} + return + try: + advice = ollama_profile_advice( + advisor_candidates[:10], + model=str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b"), + timeout=int(runtime_values.get("profile_advisor_timeout", 240) or 240), + ) + status["profile_suggestions"] = apply_profile_advice(profile_suggestions, advice) + capabilities["profile_advisor"] = {"enabled": True, "status": "ok" if advice else "empty", "profiles_returned": len(advice), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")} + except Exception as exc: + capabilities["profile_advisor"] = { + "enabled": True, + "status": "heuristic_fallback", + "error": str(exc), + "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b"), + "candidates": len(advisor_candidates), + } + for suggestion in profile_suggestions: + if isinstance(suggestion, dict): + suggestion.setdefault("profile_advisor", {"status": "heuristic", "error": str(exc)}) + + def write_status(status: dict[str, object], output: str) -> None: output_path = Path(output) output_path.parent.mkdir(parents=True, exist_ok=True) @@ -703,6 +742,7 @@ def monitor_loop( log_path, policy_path=policy_path, anomaly_limit=anomaly_limit, baseline_path=baseline_path, config_path=config_path, history_path=history_path, status_cache_path=status_cache_path, + run_profile_advisor=False, ) except Exception as exc: error_status = {"status": "error", "error": f"monitor_error: {exc}", "call_timeout_seconds": mcp_call_timeout, "poll_timeout_seconds": mcp_poll_timeout} @@ -722,8 +762,15 @@ def monitor_loop( cached = cached_status_with_error(status_cache_path, mcp) if cached: status = cached + status["llm_assessment"] = {"enabled": bool(effective_llm), "status": "cached" if last_llm_text else ("pending" if effective_llm else "disabled"), "generated_at": last_llm_at, "text": last_llm_text or ""} if effective_llm: + if status_cache_path and isinstance(mcp, dict) and mcp.get("status") not in {"error", "refreshing"}: + StatusSnapshotStore(status_cache_path).save("last_good", status) + write_status(status, output) now = int(time.time()) + if runtime.get("profile_advisor_enabled"): + add_profile_advisor(status, runtime) + write_status(status, output) if now - last_llm_at >= llm_interval: add_llm_assessment(status, previous=last_llm_text, model=effective_model or None, timeout=llm_timeout) assessment = status.get("llm_assessment", {}) @@ -739,6 +786,11 @@ def monitor_loop( } else: status["llm_assessment"] = {"enabled": False, "status": "disabled", "text": ""} + if runtime.get("profile_advisor_enabled"): + if status_cache_path and isinstance(mcp, dict) and mcp.get("status") not in {"error", "refreshing"}: + StatusSnapshotStore(status_cache_path).save("last_good", status) + write_status(status, output) + add_profile_advisor(status, runtime) if status_cache_path and isinstance(mcp, dict) and mcp.get("status") not in {"error", "refreshing"}: StatusSnapshotStore(status_cache_path).save("last_good", status) write_status(status, output)