From f6cd35d2d38f3d6e86315ba8c5f2aea9993f466b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 10 May 2026 12:11:45 -0500 Subject: [PATCH] feat(insights): add 'AI' source filter for LLM-enriched insights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "ai" pill to the source-filter strip on /insights and a small blue "AI" badge on every card whose `llm_enriched_at` is set. Selecting the pill returns every LLM-enriched insight regardless of underlying source (preseem / snmp / gaiia / system). `source: "ai"` is a virtual filter — `list_insights/2` translates it to `WHERE llm_enriched_at IS NOT NULL` rather than equality on the source column, so existing real source semantics stay intact. --- lib/towerops/preseem/insights.ex | 11 ++++- .../live/insights_live/index.html.heex | 9 +++- test/towerops/preseem/insights_test.exs | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/lib/towerops/preseem/insights.ex b/lib/towerops/preseem/insights.ex index 782f3d78..00ec1468 100644 --- a/lib/towerops/preseem/insights.ex +++ b/lib/towerops/preseem/insights.ex @@ -29,7 +29,16 @@ defmodule Towerops.Preseem.Insights do query = if type, do: where(query, type: ^type), else: query query = if urgency, do: where(query, urgency: ^urgency), else: query - query = if source, do: where(query, source: ^source), else: query + + # `source: "ai"` is a virtual filter — surfaces every insight that has + # been LLM-enriched, regardless of which underlying integration produced + # it. Real source values (preseem/snmp/gaiia/system) match the column. + query = + case source do + nil -> query + "ai" -> where(query, [i], not is_nil(i.llm_enriched_at)) + s -> where(query, source: ^s) + end query |> preload([:preseem_access_point, :device]) diff --git a/lib/towerops_web/live/insights_live/index.html.heex b/lib/towerops_web/live/insights_live/index.html.heex index 56330be7..0292cef9 100644 --- a/lib/towerops_web/live/insights_live/index.html.heex +++ b/lib/towerops_web/live/insights_live/index.html.heex @@ -69,7 +69,7 @@ {t("All")} <.link - :for={source <- ~w(preseem gaiia snmp system)} + :for={source <- ~w(preseem gaiia snmp system ai)} id={"filter-source-#{source}"} patch={ ~p"/insights?#{build_filter_params(%{status: @filter_status, urgency: @filter_urgency}, %{source: source})}" @@ -258,6 +258,13 @@ {insight.source} + <%!-- AI badge — present on any LLM-enriched insight --%> + <%= if insight.llm_enriched_at do %> + + AI + + <% end %> + <%!-- Type badge --%> {String.replace(insight.type, "_", " ")} diff --git a/test/towerops/preseem/insights_test.exs b/test/towerops/preseem/insights_test.exs index 3f9f6577..1ac6d8e0 100644 --- a/test/towerops/preseem/insights_test.exs +++ b/test/towerops/preseem/insights_test.exs @@ -354,6 +354,55 @@ defmodule Towerops.Preseem.InsightsTest do assert hd(snmp_insights).title == "SNMP insight" end + test "source: \"ai\" returns LLM-enriched insights regardless of underlying source", + %{org: org, access_point: ap} do + # Two unenriched insights from two different sources + {:ok, _preseem_unenriched} = + insert_insight(org, ap, %{ + type: "qoe_degradation", + urgency: "warning", + title: "Preseem (no LLM)" + }) + + {:ok, _snmp_unenriched} = + insert_insight_with_source(org, %{ + type: "snmp_cpu_high", + urgency: "warning", + title: "SNMP (no LLM)", + source: "snmp" + }) + + # Two enriched insights from two different sources — these should match. + {:ok, _preseem_enriched} = + insert_insight(org, ap, %{ + type: "qoe_degradation", + urgency: "warning", + title: "Preseem (enriched)", + llm_summary: "summary", + llm_model: "deepseek-v4-pro", + llm_enriched_at: Towerops.Time.now() + }) + + {:ok, _snmp_enriched} = + insert_insight_with_source(org, %{ + type: "snmp_cpu_high", + urgency: "warning", + title: "SNMP (enriched)", + source: "snmp", + llm_summary: "summary", + llm_model: "deepseek-v4-pro", + llm_enriched_at: Towerops.Time.now() + }) + + ai_insights = Insights.list_insights(org.id, source: "ai") + titles = Enum.map(ai_insights, & &1.title) + + assert "Preseem (enriched)" in titles + assert "SNMP (enriched)" in titles + refute "Preseem (no LLM)" in titles + refute "SNMP (no LLM)" in titles + end + test "returns all sources when no source filter", %{org: org, access_point: ap} do insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Preseem"})