mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(ui): add skills system infrastructure to Lighthouse AI (#10322)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
committed by
GitHub
parent
1da10611e7
commit
75c4f11475
@@ -6,6 +6,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Add skill system to Lighthouse AI [(#10322)](https://github.com/prowler-cloud/prowler/pull/10322)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Google Workspace provider support [(#10333)](https://github.com/prowler-cloud/prowler/pull/10333)
|
||||
- Image (Container Registry) provider support in UI: badge icon, credentials form, and provider-type filtering [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167)
|
||||
- Organization and organizational unit row actions (Edit Name, Update Credentials, Test Connections, Delete) in providers table dropdown [(#10317)](https://github.com/prowler-cloud/prowler/pull/10317)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MESSAGE_ROLES,
|
||||
MESSAGE_STATUS,
|
||||
META_TOOLS,
|
||||
SKILL_PREFIX,
|
||||
} from "@/lib/lighthouse/constants";
|
||||
import type { ChainOfThoughtData, Message } from "@/lib/lighthouse/types";
|
||||
|
||||
@@ -70,17 +71,28 @@ export function getChainOfThoughtStepLabel(
|
||||
return `Executing ${tool}`;
|
||||
}
|
||||
|
||||
if (metaTool === META_TOOLS.LOAD_SKILL && tool) {
|
||||
const skillId = tool.startsWith(SKILL_PREFIX)
|
||||
? tool.slice(SKILL_PREFIX.length)
|
||||
: tool;
|
||||
return `Loading ${skillId} skill`;
|
||||
}
|
||||
|
||||
return tool || "Completed";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a meta-tool is a wrapper tool (describe_tool or execute_tool)
|
||||
* Determines if a tool name is a meta-tool (describe_tool, execute_tool, or load_skill)
|
||||
*
|
||||
* @param metaTool - The meta-tool name to check
|
||||
* @returns True if it's a meta-tool, false otherwise
|
||||
*/
|
||||
export function isMetaTool(metaTool: string): boolean {
|
||||
return metaTool === META_TOOLS.DESCRIBE || metaTool === META_TOOLS.EXECUTE;
|
||||
return (
|
||||
metaTool === META_TOOLS.DESCRIBE ||
|
||||
metaTool === META_TOOLS.EXECUTE ||
|
||||
metaTool === META_TOOLS.LOAD_SKILL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ERROR_PREFIX,
|
||||
LIGHTHOUSE_AGENT_TAG,
|
||||
META_TOOLS,
|
||||
SKILL_PREFIX,
|
||||
STREAM_MESSAGE_ID,
|
||||
} from "@/lib/lighthouse/constants";
|
||||
import type { ChainOfThoughtData, StreamEvent } from "@/lib/lighthouse/types";
|
||||
@@ -16,10 +17,35 @@ import type { ChainOfThoughtData, StreamEvent } from "@/lib/lighthouse/types";
|
||||
// Re-export for convenience
|
||||
export { CHAIN_OF_THOUGHT_ACTIONS, ERROR_PREFIX, STREAM_MESSAGE_ID };
|
||||
|
||||
/**
|
||||
* Safely parses the JSON string nested inside a meta-tool's input wrapper.
|
||||
* In tool stream events, meta-tools receive their arguments as `{ input: "<JSON string>" }`.
|
||||
* Note: In chat_model_end events, args are pre-parsed by LangChain (see handleChatModelEndEvent).
|
||||
*
|
||||
* @returns The parsed object, or null if parsing fails
|
||||
*/
|
||||
function parseMetaToolInput(
|
||||
toolInput: unknown,
|
||||
): Record<string, unknown> | null {
|
||||
try {
|
||||
if (
|
||||
toolInput &&
|
||||
typeof toolInput === "object" &&
|
||||
"input" in toolInput &&
|
||||
typeof toolInput.input === "string"
|
||||
) {
|
||||
return JSON.parse(toolInput.input) as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
// Failed to parse
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the actual tool name from meta-tool input.
|
||||
*
|
||||
* Meta-tools (describe_tool, execute_tool) wrap actual tool calls.
|
||||
* Meta-tools (describe_tool, execute_tool, load_skill) wrap actual tool calls.
|
||||
* This function parses the input to extract the real tool name.
|
||||
*
|
||||
* @param metaToolName - The name of the meta-tool or actual tool
|
||||
@@ -30,26 +56,19 @@ export function extractActualToolName(
|
||||
metaToolName: string,
|
||||
toolInput: unknown,
|
||||
): string | null {
|
||||
// Check if this is a meta-tool
|
||||
if (
|
||||
metaToolName === META_TOOLS.DESCRIBE ||
|
||||
metaToolName === META_TOOLS.EXECUTE
|
||||
) {
|
||||
// Meta-tool: Parse the JSON string in input.input
|
||||
try {
|
||||
if (
|
||||
toolInput &&
|
||||
typeof toolInput === "object" &&
|
||||
"input" in toolInput &&
|
||||
typeof toolInput.input === "string"
|
||||
) {
|
||||
const parsedInput = JSON.parse(toolInput.input);
|
||||
return parsedInput.toolName || null;
|
||||
}
|
||||
} catch {
|
||||
// Failed to parse, return null
|
||||
return null;
|
||||
}
|
||||
const parsed = parseMetaToolInput(toolInput);
|
||||
return (parsed?.toolName as string) || null;
|
||||
}
|
||||
|
||||
if (metaToolName === META_TOOLS.LOAD_SKILL) {
|
||||
const parsed = parseMetaToolInput(toolInput);
|
||||
return parsed?.skillId
|
||||
? `${SKILL_PREFIX}${parsed.skillId as string}`
|
||||
: null;
|
||||
}
|
||||
|
||||
// Actual tool execution: use the name directly
|
||||
@@ -172,11 +191,18 @@ export function handleChatModelEndEvent(
|
||||
const metaToolName = toolCall.name;
|
||||
const toolArgs = toolCall.args;
|
||||
|
||||
// Extract actual tool name from toolArgs.toolName (camelCase)
|
||||
const actualToolName =
|
||||
toolArgs && typeof toolArgs === "object" && "toolName" in toolArgs
|
||||
? (toolArgs.toolName as string)
|
||||
: null;
|
||||
// Extract actual tool name from toolArgs
|
||||
let actualToolName: string | null = null;
|
||||
if (toolArgs && typeof toolArgs === "object") {
|
||||
if ("toolName" in toolArgs) {
|
||||
actualToolName = toolArgs.toolName as string;
|
||||
} else if (
|
||||
metaToolName === META_TOOLS.LOAD_SKILL &&
|
||||
"skillId" in toolArgs
|
||||
) {
|
||||
actualToolName = `${SKILL_PREFIX}${toolArgs.skillId as string}`;
|
||||
}
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
createChainOfThoughtEvent({
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export const META_TOOLS = {
|
||||
DESCRIBE: "describe_tool",
|
||||
EXECUTE: "execute_tool",
|
||||
LOAD_SKILL: "load_skill",
|
||||
} as const;
|
||||
|
||||
export type MetaTool = (typeof META_TOOLS)[keyof typeof META_TOOLS];
|
||||
@@ -68,5 +69,7 @@ export const STREAM_MESSAGE_ID = "msg-1";
|
||||
|
||||
export const ERROR_PREFIX = "[LIGHTHOUSE_ANALYST_ERROR]:";
|
||||
|
||||
export const SKILL_PREFIX = "skill:";
|
||||
|
||||
export const TOOLS_UNAVAILABLE_MESSAGE =
|
||||
"\nProwler tools are unavailable. You cannot access cloud accounts or security scan data. If asked about security status or scan results, inform the user that this data is currently inaccessible.\n";
|
||||
|
||||
8
ui/lib/lighthouse/skills/index.ts
Normal file
8
ui/lib/lighthouse/skills/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Re-export registry functions and types
|
||||
export {
|
||||
getAllSkillMetadata,
|
||||
getRegisteredSkillIds,
|
||||
getSkillById,
|
||||
registerSkill,
|
||||
} from "./registry";
|
||||
export type { SkillDefinition, SkillMetadata } from "./types";
|
||||
21
ui/lib/lighthouse/skills/registry.ts
Normal file
21
ui/lib/lighthouse/skills/registry.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import "server-only";
|
||||
|
||||
import type { SkillDefinition, SkillMetadata } from "./types";
|
||||
|
||||
const skillRegistry = new Map<string, SkillDefinition>();
|
||||
|
||||
export function registerSkill(skill: SkillDefinition): void {
|
||||
skillRegistry.set(skill.metadata.id, skill);
|
||||
}
|
||||
|
||||
export function getAllSkillMetadata(): SkillMetadata[] {
|
||||
return Array.from(skillRegistry.values()).map((skill) => skill.metadata);
|
||||
}
|
||||
|
||||
export function getSkillById(id: string): SkillDefinition | undefined {
|
||||
return skillRegistry.get(id);
|
||||
}
|
||||
|
||||
export function getRegisteredSkillIds(): string[] {
|
||||
return Array.from(skillRegistry.keys());
|
||||
}
|
||||
10
ui/lib/lighthouse/skills/types.ts
Normal file
10
ui/lib/lighthouse/skills/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface SkillMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SkillDefinition {
|
||||
metadata: SkillMetadata;
|
||||
instructions: string;
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* {{TOOL_LISTING}} placeholder will be replaced with dynamically generated tool list
|
||||
*/
|
||||
import type { SkillMetadata } from "@/lib/lighthouse/skills/types";
|
||||
|
||||
export const LIGHTHOUSE_SYSTEM_PROMPT_TEMPLATE = `
|
||||
## Introduction
|
||||
|
||||
@@ -45,7 +47,7 @@ You have access to tools from multiple sources:
|
||||
|
||||
## Tool Usage
|
||||
|
||||
You have access to TWO meta-tools to interact with the available tools:
|
||||
You have access to THREE meta-tools to interact with the available tools and skills:
|
||||
|
||||
1. **describe_tool** - Get detailed schema for a specific tool
|
||||
- Use exact tool name from the list above
|
||||
@@ -59,6 +61,13 @@ You have access to TWO meta-tools to interact with the available tools:
|
||||
- Example: execute_tool({ "toolName": "prowler_hub_list_providers", "toolInput": {} })
|
||||
- Example: execute_tool({ "toolName": "prowler_app_search_security_findings", "toolInput": { "severity": ["critical", "high"], "status": ["FAIL"] } })
|
||||
|
||||
3. **load_skill** - Load specialized instructions for a complex task
|
||||
- Use when you identify a matching skill from the skill catalog below
|
||||
- Returns detailed workflows, schema knowledge, and examples
|
||||
- Example: load_skill({ "skillId": "<skill-id-from-catalog-below>" })
|
||||
|
||||
{{SKILL_CATALOG}}
|
||||
|
||||
## General Instructions
|
||||
|
||||
- **DON'T ASSUME**. Base your answers on the system prompt or tool outputs before responding to the user.
|
||||
@@ -229,6 +238,26 @@ When providing proactive recommendations to secure users' cloud accounts, follow
|
||||
- Prowler Documentation: https://docs.prowler.com/
|
||||
`;
|
||||
|
||||
/**
|
||||
* Generates the skill catalog section for the system prompt.
|
||||
* Lists all registered skills with their metadata so the LLM can match user requests.
|
||||
*/
|
||||
export function generateSkillCatalog(skills: SkillMetadata[]): string {
|
||||
if (skills.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let catalog = "## Skill Catalog\n\n";
|
||||
catalog +=
|
||||
"When a user request matches a skill below, use load_skill to get detailed instructions before proceeding.\n\n";
|
||||
|
||||
for (const skill of skills) {
|
||||
catalog += `- **${skill.id}**: ${skill.name} - ${skill.description}\n`;
|
||||
}
|
||||
|
||||
return catalog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the user-provided data section with security boundary
|
||||
*/
|
||||
|
||||
82
ui/lib/lighthouse/tools/load-skill.ts
Normal file
82
ui/lib/lighthouse/tools/load-skill.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import "server-only";
|
||||
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { addBreadcrumb } from "@sentry/nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
getRegisteredSkillIds,
|
||||
getSkillById,
|
||||
} from "@/lib/lighthouse/skills/index";
|
||||
|
||||
interface SkillLoadedResult {
|
||||
found: true;
|
||||
skillId: string;
|
||||
name: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface SkillNotFoundResult {
|
||||
found: false;
|
||||
skillId: string;
|
||||
message: string;
|
||||
availableSkills: string[];
|
||||
}
|
||||
|
||||
type LoadSkillResult = SkillLoadedResult | SkillNotFoundResult;
|
||||
|
||||
export const loadSkill = tool(
|
||||
async ({ skillId }: { skillId: string }): Promise<LoadSkillResult> => {
|
||||
addBreadcrumb({
|
||||
category: "skill",
|
||||
message: `load_skill called for: ${skillId}`,
|
||||
level: "info",
|
||||
data: { skillId },
|
||||
});
|
||||
|
||||
const skill = getSkillById(skillId);
|
||||
|
||||
if (!skill) {
|
||||
const availableSkills = getRegisteredSkillIds();
|
||||
|
||||
addBreadcrumb({
|
||||
category: "skill",
|
||||
message: `Skill not found: ${skillId}`,
|
||||
level: "warning",
|
||||
data: { skillId, availableSkills },
|
||||
});
|
||||
|
||||
return {
|
||||
found: false,
|
||||
skillId,
|
||||
message: `Skill '${skillId}' not found.`,
|
||||
availableSkills,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
skillId: skill.metadata.id,
|
||||
name: skill.metadata.name,
|
||||
instructions: skill.instructions,
|
||||
};
|
||||
},
|
||||
{
|
||||
name: "load_skill",
|
||||
description: `Load detailed instructions for a specialized skill.
|
||||
|
||||
Skills provide domain-specific guidance, workflows, and schema knowledge for complex tasks.
|
||||
Use this when you identify a relevant skill from the skill catalog in your system prompt.
|
||||
|
||||
Returns:
|
||||
- Skill metadata (id, name)
|
||||
- Full skill instructions with workflows and examples`,
|
||||
schema: z.object({
|
||||
skillId: z
|
||||
.string()
|
||||
.describe(
|
||||
"The ID of the skill to load (from the skill catalog in your system prompt)",
|
||||
),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
initializeMCPClient,
|
||||
isMCPAvailable,
|
||||
} from "@/lib/lighthouse/mcp-client";
|
||||
import { getAllSkillMetadata } from "@/lib/lighthouse/skills/index";
|
||||
import {
|
||||
generateSkillCatalog,
|
||||
generateUserDataSection,
|
||||
LIGHTHOUSE_SYSTEM_PROMPT_TEMPLATE,
|
||||
} from "@/lib/lighthouse/system-prompt";
|
||||
import { loadSkill } from "@/lib/lighthouse/tools/load-skill";
|
||||
import { describeTool, executeTool } from "@/lib/lighthouse/tools/meta-tool";
|
||||
import { getModelParams } from "@/lib/lighthouse/utils";
|
||||
|
||||
@@ -136,6 +139,10 @@ export async function initLighthouseWorkflow(runtimeConfig?: RuntimeConfig) {
|
||||
toolListing,
|
||||
);
|
||||
|
||||
// Generate and inject skill catalog
|
||||
const skillCatalog = generateSkillCatalog(getAllSkillMetadata());
|
||||
systemPrompt = systemPrompt.replace("{{SKILL_CATALOG}}", skillCatalog);
|
||||
|
||||
// Add user-provided data section if available
|
||||
const userDataSection = generateUserDataSection(
|
||||
runtimeConfig?.businessContext,
|
||||
@@ -177,7 +184,7 @@ export async function initLighthouseWorkflow(runtimeConfig?: RuntimeConfig) {
|
||||
|
||||
const agent = createAgent({
|
||||
model: llm,
|
||||
tools: [describeTool, executeTool],
|
||||
tools: [describeTool, executeTool, loadSkill],
|
||||
systemPrompt,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user