diff --git a/ui/app/api/lighthouse/nudge/route.ts b/ui/app/api/lighthouse/nudge/route.ts new file mode 100644 index 0000000000..1b39a46dcf --- /dev/null +++ b/ui/app/api/lighthouse/nudge/route.ts @@ -0,0 +1,35 @@ +import { getCurrentUserId } from "@/lib/lighthouse/cache"; +import { fetchNudges, nudgeCache } from "@/lib/lighthouse/nudge"; + +export async function GET() { + try { + const userId = await getCurrentUserId(); + + // Check if we have cached nudges + if (nudgeCache[userId]?.nudges) { + // Return cached nudges + return Response.json(nudgeCache[userId].nudges); + } + + // If we're already fetching nudges, return default nudges + if (nudgeCache[userId]?.isFetching) { + return Response.json({ nudges: [] }); + } + + // Initialize cache entry and start fetching + nudgeCache[userId] = { + nudges: null, + timestamp: Date.now(), + isFetching: true, + }; + + // Start fetching nudges asynchronously + fetchNudges(userId).catch(console.error); + + // Return default nudges while fetching + return Response.json({ nudges: [] }); + } catch (error) { + console.error("Error in GET request:", error); + return Response.json({ error: "An error occurred" }, { status: 500 }); + } +} diff --git a/ui/lib/lighthouse/nudge.ts b/ui/lib/lighthouse/nudge.ts new file mode 100644 index 0000000000..6e2e3e86cd --- /dev/null +++ b/ui/lib/lighthouse/nudge.ts @@ -0,0 +1,199 @@ +import { PromptTemplate } from "@langchain/core/prompts"; +import { ChatOpenAI } from "@langchain/openai"; + +import { getAIKey, getLighthouseConfig } from "@/actions/lighthouse/lighthouse"; +import { + getFindingsByService, + getFindingsByStatus, + getProvidersOverview, +} from "@/actions/overview/overview"; +import { getCachedDataSection } from "@/lib/lighthouse/cache"; + +// In-memory cache for nudges +type NudgeCache = { + [userId: string]: { + nudges: any; + timestamp: number; + isFetching: boolean; + fetchPromise?: Promise; + }; +}; + +export const nudgeCache: NudgeCache = {}; + +// Default nudges when API key is not configured +const defaultNudges = { + nudges: [ + { + nudge: "Activate Lighthouse for AI-powered cloud security!", + llm_query: "", + }, + { + nudge: "Resolve cloud security issues effortlessly with Lighthouse", + llm_query: "", + }, + { + nudge: "Fix cloud security issues the smart way with Lighthouse", + llm_query: "", + }, + ], +}; + +// Function to fetch nudges asynchronously +export async function fetchNudges(userId: string) { + try { + // If there's already a fetch in progress, wait for it + if (nudgeCache[userId]?.fetchPromise) { + await nudgeCache[userId].fetchPromise; + return; + } + + // Get AI configuration to access business context + const aiConfig = await getLighthouseConfig(); + const modelConfig = aiConfig?.data?.attributes?.model_config; + const apiKey = await getAIKey(); + + // If no API key is configured, return default nudges + if (!apiKey) { + nudgeCache[userId] = { + nudges: defaultNudges, + timestamp: Date.now(), + isFetching: false, + }; + return; + } + + // Get cached data + const cachedData = await getCachedDataSection(); + + // Initialize the chat model with backend config + const model = new ChatOpenAI({ + modelName: modelConfig?.model || "gpt-4o", + temperature: modelConfig?.temperature || 0, + maxTokens: modelConfig?.max_tokens || 4000, + apiKey: apiKey, + }); + + // Create the prompt template + // Use double curly braces for the JSON output - https://github.com/langchain-ai/langchain/issues/1660#issuecomment-1469320129 + const prompt = PromptTemplate.fromTemplate(` +You are a UX assistant for a cloud security dashboard. Your task is to generate 3 short, accurate, and helpful one-liners based on the provided JSON security data. These alerts will be shown in the top right corner of the dashboard as clickable pop-ups. + +Each alert should: +- Be grounded in the provided JSON data (findings, compliance scores, posture drift, etc.). +- Rotate focus across findings, score trends, and region/account-level security posture. +- Mention how the AI assistant ("Lighthouse") can help the user fix these issues. +- Be informative and calm — avoid dramatic or fear-based language. +- Be under 15 words. +- Output must only contain the JSON output. No other text or formatting. It should NOT contain backticks or markdown formatting. +- If no data is available, return 3 different nudges where each nudge tells to connect user's cloud accounts to Prowler and use Lighthouse to fix security issues in them. Keep LLM query empty. +- You should never give a non JSON output or a JSON output that doesn't match the expected format. + +Additionally, for each alert, generate an LLM query that Lighthouse can use to assist the user in fixing the specific issue. + +Output format (JSON): +""" +{{ + "nudges": [ + {{ + "nudge": "", + "llm_query": "", + }}, + {{ + "nudge": "", + "llm_query": "", + }}, + {{ + "nudge": "", + "llm_query": "", + }}, + ] +}} +""" + +User Data: + +{cachedData} + +Provider Overview: + +{providerOverview} + +Findings Status Overview: + +{findingsStatusOverview} + +Findings Service Overview: + +{findingsServiceOverview} + +`); + + // Create a promise for this fetch operation + const fetchPromise = (async () => { + try { + // Get all overview data + const providerOverview = await getProvidersOverview({ + page: 1, + query: "", + sort: "", + filters: {}, + }); + + const findingsStatusOverview = await getFindingsByStatus({ + page: 1, + query: "", + sort: "", + filters: {}, + }); + + const findingsServiceOverview = await getFindingsByService({ + page: 1, + query: "", + sort: "", + filters: { "filter[inserted_at__gte]": "2025-01-01" }, + }); + + // Generate the response + const response = await model.invoke( + await prompt.format({ + cachedData, + providerOverview, + findingsStatusOverview, + findingsServiceOverview, + }), + ); + + // Parse the response to ensure it's valid JSON + const nudges = JSON.parse(response.content.toString()); + + // Store the response in cache + nudgeCache[userId] = { + nudges, + timestamp: Date.now(), + isFetching: false, + }; + } catch (error) { + console.error("Error fetching nudges:", error); + // Clear the fetching flag in case of error + if (nudgeCache[userId]) { + nudgeCache[userId].isFetching = false; + } + throw error; + } + })(); + + // Store the promise in the cache + nudgeCache[userId] = { + ...nudgeCache[userId], + isFetching: true, + fetchPromise, + }; + + // Wait for the fetch to complete + await fetchPromise; + } catch (error) { + console.error("Error in fetchNudges:", error); + throw error; + } +}