From 14c9bd00c76ce117c3c6b038984bbd7847729d89 Mon Sep 17 00:00:00 2001 From: Amith M P Date: Sun, 26 Oct 2025 19:45:53 +0530 Subject: [PATCH 1/4] Chatbot init --- backend/app/agents/chatbot_agent.py | 69 ++++++++++++++++++ backend/app/api/chat.py | 39 ++++++++++ backend/main.py | 5 +- frontend/app/api/chat/route.ts | 29 ++++++++ frontend/app/layout.tsx | 2 + frontend/components/chat/Chat.tsx | 16 ++++ frontend/components/chat/ChatButton.tsx | 16 ++++ frontend/components/chat/ChatDialog.tsx | 97 +++++++++++++++++++++++++ frontend/components/index.ts | 3 +- frontend/lib/api/chat.ts | 17 +++++ frontend/lib/api/client.ts | 2 +- 11 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 backend/app/agents/chatbot_agent.py create mode 100644 backend/app/api/chat.py create mode 100644 frontend/app/api/chat/route.ts create mode 100644 frontend/components/chat/Chat.tsx create mode 100644 frontend/components/chat/ChatButton.tsx create mode 100644 frontend/components/chat/ChatDialog.tsx create mode 100644 frontend/lib/api/chat.ts diff --git a/backend/app/agents/chatbot_agent.py b/backend/app/agents/chatbot_agent.py new file mode 100644 index 0000000..ff24c47 --- /dev/null +++ b/backend/app/agents/chatbot_agent.py @@ -0,0 +1,69 @@ +from typing import Dict, Any, Optional +from .base import ProactivePulseAgent +from .orchestrator import OrchestratorAgent +import boto3 +import json +import logging + +logger = logging.getLogger(__name__) + +class ChatbotAgent(ProactivePulseAgent): + """Chatbot agent using AWS Bedrock for natural language interactions.""" + + def __init__(self, config): + super().__init__( + name="Chatbot Agent", + description="Natural language interface powered by AWS Bedrock" + ) + + self.config = config + self.orchestrator = OrchestratorAgent() + self.bedrock_client = boto3.client('bedrock-runtime') + + async def process_message(self, message: str, conversation_id: str = None) -> Dict[str, Any]: + try: + # Prepare the prompt for Bedrock + prompt = { + "prompt": message, + "max_tokens": 512, + "temperature": 0.7, + "model": self.config.aws_bedrock_model_text + } + + # Get response from Bedrock + response = self.bedrock_client.invoke_model( + modelId=self.config.aws_bedrock_model_text, + body=json.dumps(prompt) + ) + + # Parse response and determine if we need to call other agents + response_body = json.loads(response['body'].read()) + + # Check if we need to perform any specific actions + if "analyze" in message.lower(): + analysis_results = await self.orchestrator.run_analysis() + return { + "type": "analysis", + "message": response_body['completion'], + "data": analysis_results + } + + if "health" in message.lower(): + health_status = await self.orchestrator.get_health_status() + return { + "type": "health", + "message": response_body['completion'], + "data": health_status + } + + return { + "type": "chat", + "message": response_body['completion'] + } + + except Exception as e: + logger.error(f"ChatbotAgent error: {str(e)}") + return { + "type": "error", + "message": "I encountered an error processing your request." + } \ No newline at end of file diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py new file mode 100644 index 0000000..0cfa650 --- /dev/null +++ b/backend/app/api/chat.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from typing import Dict, Any, Optional +from ..agents.chatbot_agent import ChatbotAgent +from ..config import get_settings +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/chat", tags=["chat"]) + +class ChatMessage(BaseModel): + text: str + +class ChatResponse(BaseModel): + type: str + message: str + data: Optional[Dict[str, Any]] = None + +@router.post("/", response_model=ChatResponse) +async def process_chat( + message: ChatMessage, + settings = Depends(get_settings) +): + try: + # Create chatbot instance with settings + chatbot = ChatbotAgent(settings) + + # Process message + response = await chatbot.process_message(message.text) + return response + + except Exception as e: + logger.error(f"Chat processing error: {str(e)}") + return ChatResponse( + type="error", + message="An error occurred while processing your message", + data={"error": str(e)} + ) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 06fce55..ae99664 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,7 +16,7 @@ import sys from io import StringIO from app.config import get_settings -from app.api import insights, analysis, health +from app.api import chat, insights, analysis, health from app.utils.logging import setup_logging from app.agents.orchestrator import OrchestratorAgent from app.storage.factory import get_storage_service @@ -138,7 +138,7 @@ async def lifespan(app: FastAPI): # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=settings.allowed_origins, + allow_origins=["http://localhost:3000"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -173,6 +173,7 @@ async def global_exception_handler(request: Request, exc: Exception): # Include routers +app.include_router(chat.router) app.include_router(health.router, prefix="/health", tags=["health"]) app.include_router(insights.router, prefix="/insights", tags=["insights"]) app.include_router(analysis.router, prefix="/analysis", tags=["analysis"]) diff --git a/frontend/app/api/chat/route.ts b/frontend/app/api/chat/route.ts new file mode 100644 index 0000000..509aed1 --- /dev/null +++ b/frontend/app/api/chat/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server' + +export async function POST(req: Request) { + try { + const body = await req.json() + + const response = await fetch('http://localhost:8000/api/chat/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body) + }) + + const data = await response.json() + return NextResponse.json(data) + + } catch (error) { + console.error('Chat API error:', error) + return NextResponse.json( + { + type: 'error', + message: 'Failed to process chat message', + data: { error: error instanceof Error ? error.message : String(error) } + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 725c913..2facc0d 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -6,6 +6,7 @@ import { SidebarProvider } from '@/components/layout/MainLayout'; import { Toaster } from 'react-hot-toast'; import { SWRConfig } from 'swr'; import { REFRESH_INTERVAL } from '@/lib/utils/constants'; +import { Chat } from '@/components/chat/Chat'; const inter = Inter({ subsets: ['latin'] }); @@ -55,6 +56,7 @@ export default function RootLayout({ + ); diff --git a/frontend/components/chat/Chat.tsx b/frontend/components/chat/Chat.tsx new file mode 100644 index 0000000..d4610eb --- /dev/null +++ b/frontend/components/chat/Chat.tsx @@ -0,0 +1,16 @@ +'use client'; + +import React, { useState } from 'react'; +import { ChatButton } from './ChatButton'; +import { ChatDialog } from './ChatDialog'; + +export const Chat: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + {!isOpen && setIsOpen(true)} />} + {isOpen && setIsOpen(false)} />} + + ); +}; \ No newline at end of file diff --git a/frontend/components/chat/ChatButton.tsx b/frontend/components/chat/ChatButton.tsx new file mode 100644 index 0000000..1abb6ad --- /dev/null +++ b/frontend/components/chat/ChatButton.tsx @@ -0,0 +1,16 @@ +'use client'; + +import React from 'react'; +import { ChatBubbleLeftIcon } from '@heroicons/react/24/outline'; + +export const ChatButton: React.FC<{ onClick: () => void }> = ({ onClick }) => { + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/components/chat/ChatDialog.tsx b/frontend/components/chat/ChatDialog.tsx new file mode 100644 index 0000000..5acb129 --- /dev/null +++ b/frontend/components/chat/ChatDialog.tsx @@ -0,0 +1,97 @@ +'use client'; + +import React, { useState } from 'react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { Card } from '../shared/Card'; +import { Button } from '../shared/Button'; +import { LoadingSpinner } from '../shared/LoadingSpinner'; +import { sendChatMessage } from '../../lib/api/chat'; +import type { ChatResponse } from '../../lib/api/chat'; + +interface Message { + type: 'user' | 'bot'; + text: string; + data?: any; +} + +export const ChatDialog: React.FC<{ onClose: () => void }> = ({ onClose }) => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const sendMessage = async () => { + if (!input.trim() || isLoading) return; + + try { + setIsLoading(true); + setMessages(prev => [...prev, { type: 'user', text: input }]); + + const response = await sendChatMessage({ text: input }); + + setMessages(prev => [...prev, { + type: 'bot', + text: response.message, + data: response.data + }]); + + } catch (error) { + console.error('Chat error:', error); + setMessages(prev => [...prev, { + type: 'bot', + text: 'Sorry, I encountered an error processing your request.' + }]); + } finally { + setIsLoading(false); + setInput(''); + } + }; + + return ( +
+ +
+

ProactivePulse AI Assistant

+ +
+ +
+ {messages.map((msg, i) => ( +
+
+

{msg.text}

+ {msg.data && ( +
+                    {JSON.stringify(msg.data, null, 2)}
+                  
+ )} +
+
+ ))} +
+ +
+
+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && sendMessage()} + placeholder="Ask me anything..." + className="flex-1 p-2 border rounded" + disabled={isLoading} + /> + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/components/index.ts b/frontend/components/index.ts index f7a5f5b..62c3b8d 100644 --- a/frontend/components/index.ts +++ b/frontend/components/index.ts @@ -13,4 +13,5 @@ export { Dashboard } from './dashboard/Dashboard'; export { Button } from './shared/Button'; export { Badge } from './shared/Badge'; export { Card, CardHeader } from './shared/Card'; -export { LoadingSpinner, LoadingPage } from './shared/LoadingSpinner'; \ No newline at end of file +export { LoadingSpinner, LoadingPage } from './shared/LoadingSpinner'; +export { Chat } from './chat/Chat'; \ No newline at end of file diff --git a/frontend/lib/api/chat.ts b/frontend/lib/api/chat.ts new file mode 100644 index 0000000..5cb40a0 --- /dev/null +++ b/frontend/lib/api/chat.ts @@ -0,0 +1,17 @@ +import apiClient from './client'; + +export interface ChatMessage { + text: string; +} + +export interface ChatResponse { + type: string; + message: string; + data?: any; +} + +// Send chat message +export const sendChatMessage = async (message: ChatMessage): Promise => { + const response = await apiClient.post('/api/chat', message); + return response.data; +}; \ No newline at end of file diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index 48b61fe..c07f4ed 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -6,7 +6,7 @@ const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; // Create axios instance with default config const apiClient = axios.create({ baseURL: BASE_URL, - timeout: 10000, + timeout: 25000, headers: { 'Content-Type': 'application/json', }, From a0ba597a25c3fa6eeb5402d9d3982117648dc6f2 Mon Sep 17 00:00:00 2001 From: Amith M P Date: Sun, 26 Oct 2025 20:27:58 +0530 Subject: [PATCH 2/4] chatbot message template update --- backend/app/agents/chatbot_agent.py | 73 ++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/backend/app/agents/chatbot_agent.py b/backend/app/agents/chatbot_agent.py index ff24c47..9d8d434 100644 --- a/backend/app/agents/chatbot_agent.py +++ b/backend/app/agents/chatbot_agent.py @@ -1,15 +1,13 @@ -from typing import Dict, Any, Optional -from .base import ProactivePulseAgent -from .orchestrator import OrchestratorAgent import boto3 import json import logging +from typing import Dict, Any, Optional +from .base import ProactivePulseAgent +from .orchestrator import OrchestratorAgent logger = logging.getLogger(__name__) class ChatbotAgent(ProactivePulseAgent): - """Chatbot agent using AWS Bedrock for natural language interactions.""" - def __init__(self, config): super().__init__( name="Chatbot Agent", @@ -18,33 +16,61 @@ def __init__(self, config): self.config = config self.orchestrator = OrchestratorAgent() - self.bedrock_client = boto3.client('bedrock-runtime') + try: + # Create session using SSO profile + session = boto3.Session( + profile_name='proactive-pulse', # Using the SSO profile + region_name=config.aws_region + ) + + # Initialize Bedrock client with session + self.bedrock_client = session.client('bedrock-runtime') + logger.info(f"Successfully initialized Bedrock client using SSO profile in region {config.aws_region}") + + except Exception as e: + logger.error(f"Failed to initialize Bedrock client: {str(e)}") + raise + async def process_message(self, message: str, conversation_id: str = None) -> Dict[str, Any]: try: - # Prepare the prompt for Bedrock - prompt = { - "prompt": message, - "max_tokens": 512, - "temperature": 0.7, - "model": self.config.aws_bedrock_model_text + logger.info(f"Processing message using Bedrock model: {self.config.aws_bedrock_model_text}") + + # Prepare message payload + body = { + "messages": [ + { + "role": "user", + "content": [ + { + "text": message + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 512, + "temperature": 0.7, + "topP": 0.9 + } } - # Get response from Bedrock + # Invoke model response = self.bedrock_client.invoke_model( modelId=self.config.aws_bedrock_model_text, - body=json.dumps(prompt) + body=json.dumps(body) ) + + # Parse response + response_body = json.loads(response["body"].read()) + completion = response_body.get('messages', [])[0].get('content', [])[0].get('text', '') if response_body.get('messages') else '' - # Parse response and determine if we need to call other agents - response_body = json.loads(response['body'].read()) - - # Check if we need to perform any specific actions + # Check for specific intents if "analyze" in message.lower(): analysis_results = await self.orchestrator.run_analysis() return { "type": "analysis", - "message": response_body['completion'], + "message": completion, "data": analysis_results } @@ -52,18 +78,19 @@ async def process_message(self, message: str, conversation_id: str = None) -> Di health_status = await self.orchestrator.get_health_status() return { "type": "health", - "message": response_body['completion'], + "message": completion, "data": health_status } return { "type": "chat", - "message": response_body['completion'] + "message": completion } except Exception as e: - logger.error(f"ChatbotAgent error: {str(e)}") + logger.error(f"Error processing message: {e}", exc_info=True) return { "type": "error", - "message": "I encountered an error processing your request." + "message": "I encountered an error processing your request.", + "data": {"error": str(e)} } \ No newline at end of file From 99dab3f6a148c3e098d86333c3d1c7b552b5d9fb Mon Sep 17 00:00:00 2001 From: Amith M P Date: Sun, 26 Oct 2025 22:22:32 +0530 Subject: [PATCH 3/4] Chatbot markdown and list changes --- backend/app/agents/chatbot_agent.py | 230 +++- backend/app/agents/insight_fetch.py | 66 + frontend/components/chat/ChatDialog.tsx | 93 +- frontend/components/insights/InsightTable.tsx | 84 ++ frontend/package-lock.json | 1173 ++++++++++++++++- frontend/package.json | 1 + 6 files changed, 1596 insertions(+), 51 deletions(-) create mode 100644 backend/app/agents/insight_fetch.py create mode 100644 frontend/components/insights/InsightTable.tsx diff --git a/backend/app/agents/chatbot_agent.py b/backend/app/agents/chatbot_agent.py index 9d8d434..d3a05f6 100644 --- a/backend/app/agents/chatbot_agent.py +++ b/backend/app/agents/chatbot_agent.py @@ -1,9 +1,10 @@ import boto3 import json import logging -from typing import Dict, Any, Optional +from typing import Dict, Any, List from .base import ProactivePulseAgent from .orchestrator import OrchestratorAgent +from .insight_fetch import InsightFetchAgent logger = logging.getLogger(__name__) @@ -16,81 +17,232 @@ def __init__(self, config): self.config = config self.orchestrator = OrchestratorAgent() + self.insight_fetch = InsightFetchAgent(config) try: - # Create session using SSO profile session = boto3.Session( - profile_name='proactive-pulse', # Using the SSO profile + profile_name='proactive-pulse', region_name=config.aws_region ) - - # Initialize Bedrock client with session self.bedrock_client = session.client('bedrock-runtime') - logger.info(f"Successfully initialized Bedrock client using SSO profile in region {config.aws_region}") - + logger.info(f"✅ Bedrock client initialized in region {config.aws_region}") except Exception as e: - logger.error(f"Failed to initialize Bedrock client: {str(e)}") + logger.error(f"❌ Failed to initialize Bedrock client: {str(e)}") raise + # ============================================================ + # Main conversational logic with Nova-Lite compliant message body + # ============================================================ async def process_message(self, message: str, conversation_id: str = None) -> Dict[str, Any]: try: logger.info(f"Processing message using Bedrock model: {self.config.aws_bedrock_model_text}") - - # Prepare message payload + + # Step 1: Analyze user intent + text_lower = message.lower() + data = None + resp_type = "chat" + context = "" + + # --- HEALTH STATUS --- + if "health" in text_lower or "status" in text_lower: + resp_type = "health" + data = await self.orchestrator.get_health_status() + + # Explicitly include raw JSON for accurate summarization + health_json_str = json.dumps(data, indent=2) + context = ( + "You are analyzing the following raw system health JSON. " + "Summarize it accurately and do NOT invent or guess any values. " + "If metrics like CPU, memory, or disk are missing, explicitly say 'not available'.\n\n" + f"```json\n{health_json_str}\n```" + ) + + # --- CRITICAL INSIGHTS --- + elif "critical" in text_lower or "urgent" in text_lower: + resp_type = "insights" + insights = await self.insight_fetch.fetch_critical_insights() + data = {"insights": insights} + + # Detect if user only wants a list + if "list" in text_lower or "show" in text_lower: + context = self._format_insights_list(insights) + else: + context = self._format_insights_context(insights, "critical") + + # --- RECENT INSIGHTS --- + elif "recent" in text_lower and ("insight" in text_lower or "analysis" in text_lower): + resp_type = "insights" + insights = await self.insight_fetch.fetch_recent_insights() + data = {"insights": insights} + + if "list" in text_lower or "show" in text_lower: + context = self._format_insights_list(insights) + else: + context = self._format_insights_context(insights, "recent") + + # --- SYSTEM ANALYSIS --- + elif any(k in text_lower for k in ("analy", "analysis", "metrics")): + resp_type = "analysis" + data = await self.orchestrator.run_analysis(window_minutes=15) + if data.get("findings"): + context = self._format_analysis_context(data) + + # --- ANOMALIES --- + elif any(k in text_lower for k in ("anomal", "anomaly")): + resp_type = "anomaly" + if hasattr(self.orchestrator, "detect_anomalies"): + data = await self.orchestrator.detect_anomalies(window_minutes=15) + else: + data = await self.orchestrator.run_analysis(window_minutes=15) + context = self._format_anomaly_context(data) + + # ============================================================ + # Step 2: Build Nova-Lite message payload (NO system role!) + # ============================================================ body = { "messages": [ { "role": "user", "content": [ { - "text": message + "text": ( + "You are ProactivePulse AI, a helpful assistant that analyzes system health " + "and performance. Use markdown for readability. " + "If the user asks to *list* insights, respond with a clean bullet list only, " + "no explanations or analysis.\n\n" + f"{context}\n\nUser Question: {message}" + ) } ] } ], "inferenceConfig": { "maxTokens": 512, - "temperature": 0.7, + "temperature": 0.5, "topP": 0.9 } } - - # Invoke model + + # Step 3: Invoke Bedrock Nova-Lite model response = self.bedrock_client.invoke_model( modelId=self.config.aws_bedrock_model_text, body=json.dumps(body) ) - # Parse response - response_body = json.loads(response["body"].read()) - completion = response_body.get('messages', [])[0].get('content', [])[0].get('text', '') if response_body.get('messages') else '' - - # Check for specific intents - if "analyze" in message.lower(): - analysis_results = await self.orchestrator.run_analysis() - return { - "type": "analysis", - "message": completion, - "data": analysis_results - } - - if "health" in message.lower(): - health_status = await self.orchestrator.get_health_status() - return { - "type": "health", - "message": completion, - "data": health_status - } - + # Step 4: Parse and extract completion + result_body = response["body"].read() if hasattr(response["body"], "read") else response["body"] + result = json.loads(result_body) + + completion = self._extract_completion(result) + + # Step 5: Ensure data is non-null and formatted + if data is None: + data = {} + + if resp_type == "insights": + data["formatted"] = self._format_insights_for_display(data.get("insights", [])) + return { - "type": "chat", - "message": completion + "type": resp_type, + "message": completion, + "data": data } - + except Exception as e: logger.error(f"Error processing message: {e}", exc_info=True) return { "type": "error", "message": "I encountered an error processing your request.", "data": {"error": str(e)} - } \ No newline at end of file + } + + # ============================================================ + # Helper Functions + # ============================================================ + + def _format_insights_context(self, insights: List[Dict], insight_type: str) -> str: + """Format insights for conversational LLM context""" + context = f"Based on {insight_type} system insights:\n\n" + for insight in insights: + context += f"- Issue: {insight['hypothesis']}\n" + context += f" Priority: {insight['priority'].upper()}\n" + context += f" Confidence: {insight['confidence']*100:.1f}%\n" + context += f" Finding: {insight['narrative']}\n" + if insight.get('recommended_actions'): + context += " Recommended Actions:\n" + for action in insight['recommended_actions'][:2]: + context += f" * {action}\n" + context += "\n" + return context + + def _format_insights_list(self, insights: List[Dict]) -> str: + """Return only a clean bullet list of critical insights""" + if not insights: + return "No critical insights available." + lines = ["List of critical insights:"] + for i, ins in enumerate(insights, start=1): + lines.append(f"{i}. {ins['hypothesis']} (Priority: {ins['priority'].upper()}, Confidence: {ins['confidence']*100:.1f}%)") + return "\n".join(lines) + + def _format_analysis_context(self, analysis_data: Dict) -> str: + """Format analysis data for LLM context""" + context = "Based on recent system analysis:\n\n" + if analysis_data.get("findings"): + context += "Key Findings:\n" + for finding in analysis_data["findings"]: + context += f"- {finding}\n" + return context + + def _format_anomaly_context(self, anomaly_data: Dict) -> str: + """Format anomaly data for LLM context""" + context = "Recent system anomalies detected:\n\n" + if anomaly_data.get("anomalies"): + for anomaly in anomaly_data["anomalies"]: + context += f"- {anomaly['metric']} on {anomaly['asset']}\n" + context += f" Score: {anomaly['score']*100:.1f}%\n" + context += f" Value: {anomaly['value']}\n\n" + return context + + def _extract_completion(self, result: Dict) -> str: + """Extract and clean up the model's response""" + try: + if "output" in result and "message" in result["output"]: + content = result["output"]["message"].get("content", []) + if content and isinstance(content[0], dict): + return content[0].get("text", "") + if isinstance(result.get("messages"), list) and result["messages"]: + return result["messages"][0].get("content", [])[0].get("text", "") + return result.get("outputText", "") or result.get("text", "") or "" + except Exception: + return "" + + def _format_insights_for_display(self, insights: List[Dict]) -> Dict[str, Any]: + """Format insights into structured display data""" + return { + "tables": { + "critical_issues": [ + { + "issue": i['hypothesis'], + "priority": i['priority'], + "confidence": f"{i['confidence']*100:.1f}%", + "actions": i['recommended_actions'] + } + for i in insights if i['priority'] == 'critical' + ], + "affected_systems": [ + { + "asset": a['asset'], + "metric": a['metric'], + "value": f"{a['value']:.2f}", + "score": f"{a['score']*100:.1f}%" + } + for i in insights + for a in i.get('anomaly_refs', []) + ] + }, + "summary": { + "total_insights": len(insights), + "critical_count": len([i for i in insights if i['priority'] == 'critical']), + "affected_systems": len(set(a['asset'] for i in insights for a in i.get('anomaly_refs', []))) + } + } diff --git a/backend/app/agents/insight_fetch.py b/backend/app/agents/insight_fetch.py new file mode 100644 index 0000000..18f7a2c --- /dev/null +++ b/backend/app/agents/insight_fetch.py @@ -0,0 +1,66 @@ +from typing import Dict, Any, List, Optional +from datetime import datetime, timedelta +from .base import ProactivePulseAgent +from app.dependencies import get_storage_dependency +import logging + +logger = logging.getLogger(__name__) + +class InsightFetchAgent(ProactivePulseAgent): + """Agent responsible for fetching and querying insights from storage.""" + + def __init__(self, config): + super().__init__( + name="Insight Fetch Agent", + description="Retrieves and filters insights from storage" + ) + self.config = config + self.storage = get_storage_dependency() + + async def fetch_recent_insights(self, limit: int = 5) -> List[Dict[str, Any]]: + """Fetch most recent insights.""" + try: + insights, _ = await self.storage.list_insights(limit=limit) + return [insight.dict() for insight in insights] + except Exception as e: + logger.error(f"Error fetching recent insights: {e}") + return [] + + async def fetch_insights_by_type(self, insight_type: str, limit: int = 5) -> List[Dict[str, Any]]: + """Fetch insights by type (analysis, anomaly, etc).""" + try: + insights, _ = await self.storage.list_insights( + limit=limit, + filters={"type": insight_type} + ) + return [insight.dict() for insight in insights] + except Exception as e: + logger.error(f"Error fetching insights by type: {e}") + return [] + + async def fetch_critical_insights(self, limit: int = 5) -> List[Dict[str, Any]]: + """Fetch critical priority insights.""" + try: + insights, _ = await self.storage.list_insights( + limit=limit, + filters={"priority": "CRITICAL"} + ) + return [insight.dict() for insight in insights] + except Exception as e: + logger.error(f"Error fetching critical insights: {e}") + return [] + + async def search_insights(self, query: str, limit: int = 5) -> List[Dict[str, Any]]: + """Search insights by content.""" + try: + # Basic keyword filtering (enhance with proper text search) + insights, _ = await self.storage.list_insights(limit=50) # Get more to filter + matched = [ + insight.dict() for insight in insights + if query.lower() in insight.summary.lower() + or query.lower() in insight.explanation.lower() + ] + return matched[:limit] + except Exception as e: + logger.error(f"Error searching insights: {e}") + return [] \ No newline at end of file diff --git a/frontend/components/chat/ChatDialog.tsx b/frontend/components/chat/ChatDialog.tsx index 5acb129..c836e18 100644 --- a/frontend/components/chat/ChatDialog.tsx +++ b/frontend/components/chat/ChatDialog.tsx @@ -2,11 +2,12 @@ import React, { useState } from 'react'; import { XMarkIcon } from '@heroicons/react/24/outline'; +import ReactMarkdown from 'react-markdown'; import { Card } from '../shared/Card'; import { Button } from '../shared/Button'; import { LoadingSpinner } from '../shared/LoadingSpinner'; import { sendChatMessage } from '../../lib/api/chat'; -import type { ChatResponse } from '../../lib/api/chat'; +import { InsightTable } from '../insights/InsightTable'; interface Message { type: 'user' | 'bot'; @@ -14,6 +15,89 @@ interface Message { data?: any; } +const MessageContent: React.FC<{ message: Message }> = ({ message }) => { + // Handle user messages + if (message.type === 'user') { + return

{message.text}

; + } + + // Handle bot messages with markdown and data + return ( +
+ {/* Markdown Content */} +
+ {message.text} +
+ + {/* Structured Data Display */} + {message.data && ( +
+ {/* Health Data */} + {message.data.services && ( +
+ {Object.entries(message.data.services).map(([service, data]: [string, any]) => ( + +

{service}

+
+ Status: + + {data.status} + + {data.metrics && Object.entries(data.metrics).map(([metric, value]) => ( + + {metric}: + {String(value)} + + ))} +
+
+ ))} +
+ )} + + {/* Insights Data */} + {message.data.insights && ( +
+ {message.data.formatted && ( +
+ +

Summary

+
+ Total Insights: + {message.data.formatted.summary.total_insights} + Critical Issues: + {message.data.formatted.summary.critical_count} + Affected Systems: + {message.data.formatted.summary.affected_systems} +
+
+
+ )} + +
+ )} + + {/* Analysis Data */} + {message.data.findings && ( + +

Analysis Findings

+
    + {message.data.findings.map((finding: string, index: number) => ( +
  • + {finding} +
  • + ))} +
+
+ )} +
+ )} +
+ ); +}; + export const ChatDialog: React.FC<{ onClose: () => void }> = ({ onClose }) => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); @@ -64,12 +148,7 @@ export const ChatDialog: React.FC<{ onClose: () => void }> = ({ onClose }) => { ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-700' }`}> -

{msg.text}

- {msg.data && ( -
-                    {JSON.stringify(msg.data, null, 2)}
-                  
- )} + ))} diff --git a/frontend/components/insights/InsightTable.tsx b/frontend/components/insights/InsightTable.tsx new file mode 100644 index 0000000..73c060b --- /dev/null +++ b/frontend/components/insights/InsightTable.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Badge } from '../shared/Badge'; +import { Card } from '../shared/Card'; +import { formatRelativeTime, formatPercentage } from '@/lib/utils/formatters'; +import type { Insight } from '@/lib/types'; + +interface InsightTableProps { + insights: Insight[]; +} + +export const InsightTable: React.FC = ({ insights }) => { + return ( + +
+ + + + + + + + + + + + {insights.map((insight) => ( + + + + + + + + ))} + +
+ Priority + + Hypothesis + + Score + + Time + + Actions +
+ + {insight.priority.toUpperCase()} + + +
+ {insight.hypothesis} +
+
+ {insight.narrative} +
+
+
+ {formatPercentage(insight.correlation_score)} +
+
+ Confidence: {formatPercentage(insight.confidence)} +
+
+ {formatRelativeTime(insight.created_at)} + +
+ {insight.recommended_actions.slice(0, 2).map((action, index) => ( +
+ • {action} +
+ ))} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 798f657..468f61b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "react-dom": "18.2.0", "react-hook-form": "7.47.0", "react-hot-toast": "2.4.1", + "react-markdown": "^9.0.0", "recharts": "2.8.0", "swr": "^2.3.6", "tailwind-merge": "^3.3.1", @@ -611,6 +612,39 @@ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -623,6 +657,21 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", @@ -665,6 +714,12 @@ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", @@ -857,8 +912,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", @@ -1466,6 +1520,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1636,6 +1700,16 @@ } ] }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1652,6 +1726,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1731,6 +1845,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1965,7 +2089,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -1983,6 +2106,19 @@ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2039,6 +2175,19 @@ "node": ">=6" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2708,6 +2857,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2722,6 +2881,12 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3274,6 +3439,56 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3325,6 +3540,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3347,6 +3568,30 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3493,6 +3738,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3554,6 +3809,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3611,6 +3876,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -3947,6 +4224,16 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3979,6 +4266,159 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3987,6 +4427,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4071,8 +4953,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -4400,6 +5281,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4722,6 +5628,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4818,6 +5734,33 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-resize-detector": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz", @@ -4966,6 +5909,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5287,6 +6263,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5481,6 +6467,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5525,6 +6525,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", + "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.11" + } + }, + "node_modules/style-to-object": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", + "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -5827,6 +6845,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -5994,6 +7032,93 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -6079,6 +7204,34 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -6343,6 +7496,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/frontend/package.json b/frontend/package.json index f8282b2..31e830f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "tailwind-merge": "^3.3.1", "tailwindcss": "3.3.6", "typescript": "5.2.2", + "react-markdown": "^9.0.0", "zod": "3.22.4" }, "devDependencies": { From 8615ca01c93999baaebc642fbe9c96883f6d2ff8 Mon Sep 17 00:00:00 2001 From: Amith M P Date: Sat, 1 Nov 2025 19:36:32 +0530 Subject: [PATCH 4/4] chatbot changes --- backend/app/agents/chatbot_agent.py | 205 +++++++++++++++------------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/backend/app/agents/chatbot_agent.py b/backend/app/agents/chatbot_agent.py index d3a05f6..a63264a 100644 --- a/backend/app/agents/chatbot_agent.py +++ b/backend/app/agents/chatbot_agent.py @@ -2,6 +2,7 @@ import json import logging from typing import Dict, Any, List +import httpx from .base import ProactivePulseAgent from .orchestrator import OrchestratorAgent from .insight_fetch import InsightFetchAgent @@ -38,67 +39,36 @@ async def process_message(self, message: str, conversation_id: str = None) -> Di logger.info(f"Processing message using Bedrock model: {self.config.aws_bedrock_model_text}") # Step 1: Analyze user intent - text_lower = message.lower() + intent, resp_type = self._classify_intent(message) data = None - resp_type = "chat" context = "" - # --- HEALTH STATUS --- - if "health" in text_lower or "status" in text_lower: - resp_type = "health" - data = await self.orchestrator.get_health_status() - - # Explicitly include raw JSON for accurate summarization - health_json_str = json.dumps(data, indent=2) - context = ( - "You are analyzing the following raw system health JSON. " - "Summarize it accurately and do NOT invent or guess any values. " - "If metrics like CPU, memory, or disk are missing, explicitly say 'not available'.\n\n" - f"```json\n{health_json_str}\n```" - ) - - # --- CRITICAL INSIGHTS --- - elif "critical" in text_lower or "urgent" in text_lower: - resp_type = "insights" + # Step 2: Execute agent based on intent + if intent == 'get_health_status': + async with httpx.AsyncClient() as client: + response = await client.get("http://localhost:8000/health/", timeout=30.0) + response.raise_for_status() + data = response.json() + context = self._get_intent_prompt(intent, data) + elif intent == 'get_critical_insights': insights = await self.insight_fetch.fetch_critical_insights() data = {"insights": insights} - - # Detect if user only wants a list - if "list" in text_lower or "show" in text_lower: - context = self._format_insights_list(insights) - else: - context = self._format_insights_context(insights, "critical") - - # --- RECENT INSIGHTS --- - elif "recent" in text_lower and ("insight" in text_lower or "analysis" in text_lower): - resp_type = "insights" + context = self._get_intent_prompt(intent, data, message) + elif intent == 'get_recent_insights': insights = await self.insight_fetch.fetch_recent_insights() data = {"insights": insights} - - if "list" in text_lower or "show" in text_lower: - context = self._format_insights_list(insights) - else: - context = self._format_insights_context(insights, "recent") - - # --- SYSTEM ANALYSIS --- - elif any(k in text_lower for k in ("analy", "analysis", "metrics")): - resp_type = "analysis" + context = self._get_intent_prompt(intent, data, message) + elif intent == 'run_analysis': data = await self.orchestrator.run_analysis(window_minutes=15) - if data.get("findings"): - context = self._format_analysis_context(data) - - # --- ANOMALIES --- - elif any(k in text_lower for k in ("anomal", "anomaly")): - resp_type = "anomaly" + context = self._get_intent_prompt(intent, data) + elif intent == 'detect_anomalies': if hasattr(self.orchestrator, "detect_anomalies"): data = await self.orchestrator.detect_anomalies(window_minutes=15) else: data = await self.orchestrator.run_analysis(window_minutes=15) - context = self._format_anomaly_context(data) + context = self._get_intent_prompt(intent, data) - # ============================================================ - # Step 2: Build Nova-Lite message payload (NO system role!) - # ============================================================ + # Step 3: Build Nova-Lite message payload body = { "messages": [ { @@ -123,22 +93,20 @@ async def process_message(self, message: str, conversation_id: str = None) -> Di } } - # Step 3: Invoke Bedrock Nova-Lite model + # Step 4: Invoke Bedrock Nova-Lite model response = self.bedrock_client.invoke_model( modelId=self.config.aws_bedrock_model_text, body=json.dumps(body) ) - # Step 4: Parse and extract completion + # Step 5: Parse and extract completion result_body = response["body"].read() if hasattr(response["body"], "read") else response["body"] result = json.loads(result_body) - completion = self._extract_completion(result) - # Step 5: Ensure data is non-null and formatted + # Step 6: Ensure data is non-null and formatted if data is None: data = {} - if resp_type == "insights": data["formatted"] = self._format_insights_for_display(data.get("insights", [])) @@ -157,51 +125,96 @@ async def process_message(self, message: str, conversation_id: str = None) -> Di } # ============================================================ - # Helper Functions + # Intent Classification and Prompt Generation # ============================================================ - def _format_insights_context(self, insights: List[Dict], insight_type: str) -> str: - """Format insights for conversational LLM context""" - context = f"Based on {insight_type} system insights:\n\n" - for insight in insights: - context += f"- Issue: {insight['hypothesis']}\n" - context += f" Priority: {insight['priority'].upper()}\n" - context += f" Confidence: {insight['confidence']*100:.1f}%\n" - context += f" Finding: {insight['narrative']}\n" - if insight.get('recommended_actions'): - context += " Recommended Actions:\n" - for action in insight['recommended_actions'][:2]: - context += f" * {action}\n" - context += "\n" - return context - - def _format_insights_list(self, insights: List[Dict]) -> str: - """Return only a clean bullet list of critical insights""" - if not insights: - return "No critical insights available." - lines = ["List of critical insights:"] - for i, ins in enumerate(insights, start=1): - lines.append(f"{i}. {ins['hypothesis']} (Priority: {ins['priority'].upper()}, Confidence: {ins['confidence']*100:.1f}%)") - return "\n".join(lines) - - def _format_analysis_context(self, analysis_data: Dict) -> str: - """Format analysis data for LLM context""" - context = "Based on recent system analysis:\n\n" - if analysis_data.get("findings"): - context += "Key Findings:\n" - for finding in analysis_data["findings"]: - context += f"- {finding}\n" - return context - - def _format_anomaly_context(self, anomaly_data: Dict) -> str: - """Format anomaly data for LLM context""" - context = "Recent system anomalies detected:\n\n" - if anomaly_data.get("anomalies"): - for anomaly in anomaly_data["anomalies"]: - context += f"- {anomaly['metric']} on {anomaly['asset']}\n" - context += f" Score: {anomaly['score']*100:.1f}%\n" - context += f" Value: {anomaly['value']}\n\n" - return context + def _classify_intent(self, message: str) -> (str, str): + """Classify user intent from the message.""" + text_lower = message.lower() + + if "health" in text_lower or "status" in text_lower: + return "get_health_status", "health" + if "critical" in text_lower or "urgent" in text_lower: + return "get_critical_insights", "insights" + if "recent" in text_lower and ("insight" in text_lower or "analysis" in text_lower): + return "get_recent_insights", "insights" + if any(k in text_lower for k in ("analy", "analysis", "metrics")): + return "run_analysis", "analysis" + if any(k in text_lower for k in ("anomal", "anomaly")): + return "detect_anomalies", "anomaly" + + return "chat", "chat" + + def _get_intent_prompt(self, intent: str, data: Dict, message: str = "") -> str: + """Generate a generalized prompt for the LLM based on the intent.""" + prompts = self._get_agent_prompts() + prompt_template = prompts.get(intent, prompts["chat"]) + + formatted_data = self._format_data_for_llm(intent, data, message) + + return prompt_template.format(data=formatted_data) + + def _get_agent_prompts(self) -> Dict[str, str]: + """Returns a dictionary of prompts for each agent.""" + return { + "get_health_status": "Analyze the following system health data and provide a summary. Do not invent values. If metrics are missing, state they are 'not available'.\n\n```json\n{data}\n```", + "get_critical_insights": "Analyze these critical system insights. If the user wants a list, provide a bulleted list of issues. Otherwise, summarize the findings.\n\n{data}", + "get_recent_insights": "Analyze these recent system insights. If the user wants a list, provide a bulleted list of issues. Otherwise, summarize the findings.\n\n{data}", + "run_analysis": "Summarize the key findings from the recent system analysis provided below.\n\n{data}", + "detect_anomalies": "Summarize the detected system anomalies provided below.\n\n{data}", + "chat": "You are a helpful assistant. Please respond to the user's question.", + } + + def _format_data_for_llm(self, intent: str, data: Dict, message: str) -> str: + """Formats data for the LLM based on the intent.""" + if not data: + return "No data available." + + if intent == "get_health_status": + return json.dumps(data.get('services', {}), indent=2) + + if intent in ["get_critical_insights", "get_recent_insights"]: + insights = data.get("insights", []) + if not insights: + return "No insights available." + + text_lower = message.lower() + if "list" in text_lower or "show" in text_lower: + lines = ["List of insights:"] + for i, ins in enumerate(insights, start=1): + lines.append(f"{i}. {ins['hypothesis']} (Priority: {ins['priority'].upper()}, Confidence: {ins['confidence']*100:.1f}%)") + return "\n".join(lines) + + context = "" + for insight in insights: + context += f"- Issue: {insight['hypothesis']}\n" + context += f" Priority: {insight['priority'].upper()}\n" + context += f" Confidence: {insight['confidence']*100:.1f}%\n" + context += f" Finding: {insight['narrative']}\n" + if insight.get('recommended_actions'): + context += " Recommended Actions:\n" + for action in insight['recommended_actions'][:2]: + context += f" * {action}\n" + context += "\n" + return context + + if intent == "run_analysis": + if data.get("findings"): + return "Key Findings:\n" + "\n".join([f"- {finding}" for finding in data["findings"]]) + return "No findings from the analysis." + + if intent == "detect_anomalies": + if data.get("anomalies"): + context = "" + for anomaly in data["anomalies"]: + context += f"- {anomaly['metric']} on {anomaly['asset']}\n" + context += f" Score: {anomaly['score']*100:.1f}%\n" + context += f" Value: {anomaly['value']}\n\n" + return context + return "No anomalies detected." + + return json.dumps(data, indent=2) + def _extract_completion(self, result: Dict) -> str: """Extract and clean up the model's response"""