diff --git a/.env.example b/.env.example index 822229ea1..06659aeff 100644 --- a/.env.example +++ b/.env.example @@ -135,6 +135,17 @@ SEARXNG_SECRET= RCA_OPTIMIZE_COSTS=false GEMINI_DISABLE_THINKING= +# Embedding model for alert correlation (optional). +# Improves alert-to-incident grouping with semantic similarity. +# Requires a DIRECT API key — OpenRouter does NOT support embeddings. +# If left empty, uses Jaccard token similarity (works fine for most cases). +# Format: provider/model-name +# Examples: +# openai/text-embedding-3-small (requires OPENAI_API_KEY) +# google/gemini-embedding-2 (requires GOOGLE_AI_API_KEY) +# bedrock/amazon.titan-embed-text-v2:0 (requires BEDROCK_ACCESS_KEY_ID) +EMBEDDING_MODEL= + # Multi-agent RCA orchestrator (opt-in). When false (default), background RCA # uses the legacy single-agent ReAct loop with RCA_MODEL. When true, a lead # orchestrator triages each RCA and may fan out parallel read-only sub-agents. @@ -333,12 +344,6 @@ USE_UNTRUSTED_NODES= NEXT_PUBLIC_KUBECTL_AGENT_CHART_URL= # ----------------------------------------------------------------------------- -# Weaviate (Vector Database) -# ----------------------------------------------------------------------------- -WEAVIATE_HOST=weaviate -WEAVIATE_PORT=8080 -WEAVIATE_GRPC_PORT=50051 - # ----------------------------------------------------------------------------- # SSL/TLS for External Services # ----------------------------------------------------------------------------- @@ -351,9 +356,6 @@ POSTGRES_SSLROOTCERT= REDIS_SSL_CA_CERTS= REDIS_SSL_CERT_REQS= -# Weaviate: set to true for external instances with HTTPS+gRPC-TLS -WEAVIATE_SECURE=false - # ----------------------------------------------------------------------------- # Memgraph (Graph Database) # ----------------------------------------------------------------------------- diff --git a/.github/workflows/publish-airtight.yml b/.github/workflows/publish-airtight.yml index 383b07198..73d941a62 100644 --- a/.github/workflows/publish-airtight.yml +++ b/.github/workflows/publish-airtight.yml @@ -52,8 +52,6 @@ jobs: amazon/aws-cli:2.34.6 memgraph/memgraph-mage:3.8.1 memgraph/lab:3.8.0 - cr.weaviate.io/semitechnologies/weaviate:1.27.6 - cr.weaviate.io/semitechnologies/transformers-inference:sentence-transformers-all-MiniLM-L6-v2 steps: - name: Free disk space diff --git a/AGENTS.md b/AGENTS.md index 3362e60e6..83600a2b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,10 @@ - **Important**: Always update both `docker-compose.yaml` and `docker-compose.prod-local.yml` together to keep environment variables in sync. ## Architecture -- **Docker Compose stack**: aurora-server (Flask API on :5080), celery_worker (background tasks), chatbot (WebSocket on :5006), frontend (Next.js on :3000), postgres (:5432), weaviate (vector DB :8080), redis (:6379), vault (secrets :8200), seaweedfs (object storage :8333) +- **Docker Compose stack**: aurora-server (Flask API on :5080), celery_worker (background tasks), chatbot (WebSocket on :5006), frontend (Next.js on :3000), postgres (:5432), redis (:6379), vault (secrets :8200), seaweedfs (object storage :8333) - **Backend** (server/): Flask REST API (main_compute.py), WebSocket chatbot (main_chatbot.py), Celery tasks, connectors for GCP/AWS/Azure/Datadog/New Relic/Grafana, LangGraph agent workflow - **Frontend** (client/): Next.js 15, TypeScript, Tailwind CSS, shadcn/ui components, Auth.js authentication, path alias `@/*` → `./src/*` -- **Database**: PostgreSQL (aurora_db), Weaviate for semantic search, Redis for Celery queue +- **Database**: PostgreSQL (aurora_db), Redis for Celery queue - **Secrets**: HashiCorp Vault (KV v2 engine at `aurora` mount) - **Object Storage**: S3-compatible via SeaweedFS (default), supports AWS S3, Cloudflare R2, MinIO, etc. - **Config**: Environment in `./.env`, GCP service account in `server/connectors/gcp_connector/*.json` diff --git a/README.md b/README.md index d8716225c..2a0459bb4 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,6 @@ aurora/ | Backend | Python, Flask, Celery | | Frontend | Next.js, TypeScript | | Graph DB | Memgraph | -| Vector Store | Weaviate | | Secrets | HashiCorp Vault, AWS Secrets Manager | | Storage | PostgreSQL, Redis, S3-compatible | diff --git a/client/next.config.ts b/client/next.config.ts index a3190982f..ea99d5921 100644 --- a/client/next.config.ts +++ b/client/next.config.ts @@ -23,6 +23,12 @@ const nextConfig: NextConfig = { eslint: { ignoreDuringBuilds: true, }, + experimental: { + serverActions: { + bodySizeLimit: "50mb", + }, + middlewareClientMaxBodySize: "50mb", + }, cacheMaxMemorySize: 0, poweredByHeader: false, webpack: (config, { isServer, dev }) => { diff --git a/client/src/app/api/feedback/route.ts b/client/src/app/api/feedback/route.ts deleted file mode 100644 index 3409271f2..000000000 --- a/client/src/app/api/feedback/route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { auth } from '@/auth'; -import { readFeedback, writeFeedback, saveImage, StoredFeedback } from '@/lib/feedback-storage'; - -//Interface for feedback data received from the frontend form -interface FeedbackData { - description: string; - userAgent: string; - url: string; - userId?: string; - userEmail?: string; -} - -/** - * Submits new feedback from authenticated users - * - * Security: Requires authentication (any authenticated user can submit) - * - * Request: FormData containing: - * - feedbackData: JSON string with feedback details - * - image_0, image_1, image_2: Optional image files (max 3) - * - * Usage: Called when user clicks "Send" on feedback form - */ -export async function POST(req: NextRequest) { - try { - const session = await auth(); - if (!session?.userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - - // Parse form data from request - const formData = await req.formData(); - const feedbackDataStr = formData.get('feedbackData') as string; - - // Validate required feedback data exists - if (!feedbackDataStr) return NextResponse.json({ error: 'Missing feedback data' }, { status: 400 }); - - // Parse and validate feedback content - const feedbackData: FeedbackData = JSON.parse(feedbackDataStr); - if (!feedbackData.description?.trim()) { - return NextResponse.json({ error: 'Feedback description is required' }, { status: 400 }); - } - - // Generate unique ID for this feedback - const feedbackId = `feedback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - // Process any uploaded images (max 3) - const images = []; - const entries = Array.from(formData.entries()); - - for (const [key, value] of entries) { - // Look for image fields (image_0, image_1, image_2) - if (key.startsWith('image_') && value && typeof value === 'object' && 'arrayBuffer' in value) { - const imageData = await saveImage(value as File, feedbackId); - images.push(imageData); - } - } - - // Create complete feedback object - const newFeedback: StoredFeedback = { - id: feedbackId, - description: feedbackData.description.trim(), - userAgent: feedbackData.userAgent, - url: feedbackData.url, - userId: session.userId, - userEmail: session.user?.email || '', - timestamp: new Date().toISOString(), - images: images, - status: "new" // All new feedback starts with "new" status - }; - - // Save to storage - const existingFeedback = await readFeedback(); - existingFeedback.push(newFeedback); - await writeFeedback(existingFeedback); - - // Log successful submission for monitoring - // console.log('Feedback received and saved:', { - // id: newFeedback.id, - // description: newFeedback.description.substring(0, 100) + '...', - // userEmail: newFeedback.userEmail, - // imagesCount: images.length, - // totalImageSize: images.reduce((sum, img) => sum + img.size, 0), - // }); - - return NextResponse.json({ - success: true, - message: 'Feedback submitted successfully', - feedbackId: newFeedback.id - }); - } catch (error) { - console.error('Error processing feedback:', error); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); - } -} \ No newline at end of file diff --git a/client/src/app/api/incidents/[id]/feedback/route.ts b/client/src/app/api/incidents/[id]/feedback/route.ts deleted file mode 100644 index 538e9d2bd..000000000 --- a/client/src/app/api/incidents/[id]/feedback/route.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthenticatedUser } from '@/lib/auth-helper'; - -const API_BASE_URL = process.env.BACKEND_URL; - -export async function GET( - request: NextRequest, - { params }: { params: { id: string } } -) { - try { - if (!API_BASE_URL) { - return NextResponse.json( - { error: 'BACKEND_URL not configured' }, - { status: 500 } - ); - } - - const authResult = await getAuthenticatedUser(); - - if (authResult instanceof NextResponse) { - return authResult; - } - - const { headers: authHeaders } = authResult; - const { id } = await params; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); - - try { - const response = await fetch(`${API_BASE_URL}/api/incidents/${id}/feedback`, { - method: 'GET', - headers: authHeaders, - credentials: 'include', - cache: 'no-store', - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const text = await response.text(); - return NextResponse.json( - { error: text || 'Failed to get feedback' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (fetchError: unknown) { - clearTimeout(timeoutId); - if (fetchError instanceof Error && fetchError.name === 'AbortError') { - return NextResponse.json({ error: 'Request timeout' }, { status: 504 }); - } - throw fetchError; - } - } catch (error) { - console.error('[api/incidents/[id]/feedback] GET Error:', error); - return NextResponse.json({ error: 'Failed to get feedback' }, { status: 500 }); - } -} - -export async function POST( - request: NextRequest, - { params }: { params: { id: string } } -) { - try { - if (!API_BASE_URL) { - return NextResponse.json( - { error: 'BACKEND_URL not configured' }, - { status: 500 } - ); - } - - const authResult = await getAuthenticatedUser(); - - if (authResult instanceof NextResponse) { - return authResult; - } - - const { headers: authHeaders } = authResult; - const { id } = await params; - const body = await request.json(); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); - - try { - const response = await fetch(`${API_BASE_URL}/api/incidents/${id}/feedback`, { - method: 'POST', - headers: { - ...authHeaders, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - credentials: 'include', - cache: 'no-store', - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const data = await response.json().catch(() => ({})); - return NextResponse.json( - { error: data.error || 'Failed to submit feedback' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data, { status: 201 }); - } catch (fetchError: unknown) { - clearTimeout(timeoutId); - if (fetchError instanceof Error && fetchError.name === 'AbortError') { - return NextResponse.json({ error: 'Request timeout' }, { status: 504 }); - } - throw fetchError; - } - } catch (error) { - console.error('[api/incidents/[id]/feedback] POST Error:', error); - return NextResponse.json({ error: 'Failed to submit feedback' }, { status: 500 }); - } -} diff --git a/client/src/app/api/proxy/knowledge-base/[...path]/route.ts b/client/src/app/api/proxy/memory/[...path]/route.ts similarity index 68% rename from client/src/app/api/proxy/knowledge-base/[...path]/route.ts rename to client/src/app/api/proxy/memory/[...path]/route.ts index e15981fd1..244515783 100644 --- a/client/src/app/api/proxy/knowledge-base/[...path]/route.ts +++ b/client/src/app/api/proxy/memory/[...path]/route.ts @@ -6,8 +6,8 @@ async function handler( { params }: { params: Promise<{ path: string[] }> }, ) { const { path } = await params; - const backendPath = '/api/knowledge-base/' + path.join('/'); - return forwardRequest(request, request.method, backendPath, 'knowledge-base'); + const backendPath = '/api/memory/' + path.join('/'); + return forwardRequest(request, request.method, backendPath, 'memory'); } export { handler as GET, handler as POST, handler as PUT, handler as DELETE }; diff --git a/client/src/app/api/user/preferences/aurora-learn/route.ts b/client/src/app/api/user/preferences/aurora-learn/route.ts deleted file mode 100644 index d9572d2fc..000000000 --- a/client/src/app/api/user/preferences/aurora-learn/route.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthenticatedUser } from '@/lib/auth-helper'; - -const API_BASE_URL = process.env.BACKEND_URL; - -export async function GET(request: NextRequest) { - try { - if (!API_BASE_URL) { - return NextResponse.json( - { error: 'BACKEND_URL not configured' }, - { status: 500 } - ); - } - - const authResult = await getAuthenticatedUser(); - - if (authResult instanceof NextResponse) { - return authResult; - } - - const { headers: authHeaders } = authResult; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); - - try { - const response = await fetch(`${API_BASE_URL}/api/user/preferences/aurora-learn`, { - method: 'GET', - headers: authHeaders, - credentials: 'include', - cache: 'no-store', - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const text = await response.text(); - return NextResponse.json( - { error: text || 'Failed to get Aurora Learn setting' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (fetchError: unknown) { - clearTimeout(timeoutId); - if (fetchError instanceof Error && fetchError.name === 'AbortError') { - return NextResponse.json({ error: 'Request timeout' }, { status: 504 }); - } - throw fetchError; - } - } catch (error) { - console.error('[api/user/preferences/aurora-learn] GET Error:', error); - return NextResponse.json({ error: 'Failed to get Aurora Learn setting' }, { status: 500 }); - } -} - -export async function PUT(request: NextRequest) { - try { - if (!API_BASE_URL) { - return NextResponse.json( - { error: 'BACKEND_URL not configured' }, - { status: 500 } - ); - } - - const authResult = await getAuthenticatedUser(); - - if (authResult instanceof NextResponse) { - return authResult; - } - - const { headers: authHeaders } = authResult; - const body = await request.json(); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); - - try { - const response = await fetch(`${API_BASE_URL}/api/user/preferences/aurora-learn`, { - method: 'PUT', - headers: { - ...authHeaders, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - credentials: 'include', - cache: 'no-store', - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const data = await response.json().catch(() => ({})); - return NextResponse.json( - { error: data.error || 'Failed to update Aurora Learn setting' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (fetchError: unknown) { - clearTimeout(timeoutId); - if (fetchError instanceof Error && fetchError.name === 'AbortError') { - return NextResponse.json({ error: 'Request timeout' }, { status: 504 }); - } - throw fetchError; - } - } catch (error) { - console.error('[api/user/preferences/aurora-learn] PUT Error:', error); - return NextResponse.json({ error: 'Failed to update Aurora Learn setting' }, { status: 500 }); - } -} diff --git a/client/src/app/incidents/components/IncidentCard.tsx b/client/src/app/incidents/components/IncidentCard.tsx index 13e8ef48f..a89827c4d 100644 --- a/client/src/app/incidents/components/IncidentCard.tsx +++ b/client/src/app/incidents/components/IncidentCard.tsx @@ -31,7 +31,6 @@ import CitationBadge from './CitationBadge'; import CitationModal from './CitationModal'; import SuggestionModal from './SuggestionModal'; import FixSuggestionModal from './FixSuggestionModal'; -import IncidentFeedback from './IncidentFeedback'; import CorrelatedAlertsSection from './CorrelatedAlertsSection'; import RecentAlertsSection from './RecentAlertsSection'; import PostmortemPanel from './PostmortemPanel'; @@ -763,13 +762,6 @@ export default function IncidentCard({ incident, duration, showThoughts, onToggl - {/* Feedback Section - only show when analysis is complete */} - {incident.auroraStatus === 'complete' && ( -
- -
- )} - {/* Action Runs linked to this incident (collapsible, lazy-loaded) */}
diff --git a/client/src/app/incidents/components/IncidentFeedback.tsx b/client/src/app/incidents/components/IncidentFeedback.tsx deleted file mode 100644 index 8746cc234..000000000 --- a/client/src/app/incidents/components/IncidentFeedback.tsx +++ /dev/null @@ -1,266 +0,0 @@ -'use client'; - -import { useState, useEffect, ReactNode } from 'react'; -import { ThumbsUp, ThumbsDown, Loader2, LucideIcon } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { - incidentFeedbackService, - FeedbackType, - IncidentFeedback as FeedbackData, -} from '@/lib/services/incident-feedback'; -import { ApiError } from '@/lib/services/api-client'; -import { useToast } from '@/hooks/use-toast'; - -interface IncidentFeedbackProps { - incidentId: string; - existingFeedback?: FeedbackData | null; - readOnly?: boolean; -} - -interface FeedbackButtonProps { - icon: LucideIcon; - tooltip: string; - onClick?: () => void; - disabled?: boolean; - isLoading?: boolean; - isSelected?: boolean; - variant: 'helpful' | 'not_helpful'; -} - -function FeedbackButton({ - icon: Icon, - tooltip, - onClick, - disabled, - isLoading, - isSelected, - variant, -}: FeedbackButtonProps): ReactNode { - const colorClasses = variant === 'helpful' - ? { selected: 'bg-green-500/20 text-green-400', hover: 'text-white hover:text-green-400 hover:bg-green-500/10' } - : { selected: 'bg-red-500/20 text-red-400', hover: 'text-white hover:text-red-400 hover:bg-red-500/10' }; - - return ( - - - - - - - {tooltip} - - - - ); -} - -type FeedbackState = 'idle' | 'selecting' | 'submitting' | 'submitted'; - -export default function IncidentFeedback({ - incidentId, - existingFeedback, - readOnly = false, -}: IncidentFeedbackProps): ReactNode { - const { toast } = useToast(); - const [state, setState] = useState(existingFeedback ? 'submitted' : 'idle'); - const [selectedType, setSelectedType] = useState( - existingFeedback?.feedbackType || null - ); - const [comment, setComment] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [feedback, setFeedback] = useState(existingFeedback || null); - - // Fetch existing feedback on mount if not provided - useEffect(() => { - if (!existingFeedback && incidentId) { - incidentFeedbackService.getFeedback(incidentId) - .then((data) => { - if (data) { - setFeedback(data); - setSelectedType(data.feedbackType); - setState('submitted'); - } - }) - .catch((err) => { - console.error('Error fetching feedback:', err); - }); - } - }, [incidentId, existingFeedback]); - - const handleThumbsClick = (type: FeedbackType) => { - if (state === 'submitted' || readOnly) return; - - setSelectedType(type); - if (type === 'helpful') { - // Submit immediately for helpful - handleSubmit(type); - } else { - // Show comment field for not helpful - setState('selecting'); - } - }; - - const handleSubmit = async (type?: FeedbackType, skipComment?: boolean) => { - const feedbackType = type || selectedType; - if (!feedbackType) return; - - setIsLoading(true); - setState('submitting'); - - try { - const response = await incidentFeedbackService.submitFeedback( - incidentId, - feedbackType, - skipComment ? undefined : comment - ); - - setFeedback({ - id: response.feedbackId, - feedbackType: response.feedbackType, - comment: skipComment ? undefined : comment, - createdAt: response.createdAt, - }); - setState('submitted'); - - toast({ - title: 'Feedback submitted', - description: feedbackType === 'helpful' - ? 'Thanks! Aurora will learn from this analysis.' - : 'Thanks for your feedback.', - }); - } catch (err) { - console.error('Error submitting feedback:', err); - const apiError = err as ApiError; - const message = apiError.message || 'Failed to submit feedback'; - - if (apiError.code === 'AURORA_LEARN_DISABLED') { - toast({ - title: 'Aurora Learn is disabled', - description: 'Enable Aurora Learn in Settings > Knowledge Base to provide feedback.', - variant: 'destructive', - }); - } else { - toast({ - title: 'Failed to submit feedback', - description: message, - variant: 'destructive', - }); - } - - setState('idle'); - setSelectedType(null); - } finally { - setIsLoading(false); - } - }; - - const feedbackType = feedback?.feedbackType || selectedType; - - // Selecting not helpful (with comment) - if (state === 'selecting' && selectedType === 'not_helpful') { - return ( -
-
- handleThumbsClick('helpful')} - disabled={isLoading} - variant="helpful" - /> - -
- -
- -