adding README, cleanup

This commit is contained in:
2025-08-26 15:09:13 +02:00
parent 75636cff97
commit 9bbd8823f5
11 changed files with 626 additions and 529 deletions

118
README.md Normal file
View File

@@ -0,0 +1,118 @@
# Enclava
**Secure AI Platform with Privacy-First LLM Services**
Enclava is a comprehensive AI platform that provides secure chatbot services, document retrieval (RAG), and LLM integrations with Trusted Execution Environment (TEE) support via privateMode.ai.
## Key Features
- **AI Chatbots** - Customizable chatbots with prompt templates and RAG integration (openai compatible)
- **RAG System** - Document upload, processing, and semantic search with Qdrant
- **TEE Security** - Privacy-protected LLM inference via confidential computing
- **OpenAI Compatible** - Standard API endpoints for seamless integration with existing tools
- **Budget Management** - Built-in spend tracking and usage limits
## Quick Start
### Prerequisites
- Docker and Docker Compose
- Git
- [privatemode.ai](https://privatemode.ai) api key
### 1. Clone Repository
```bash
git clone <repository-url>
cd enclava
```
### 2. Configure Environment
```bash
# Copy example environment file
cp .env.example .env
# Edit .env with your settings
vim .env
```
**Required Configuration:**
```bash
# Security
JWT_SECRET=your-super-secret-jwt-key-here-change-in-production
# PrivateMode.ai API Key (optional but recommended)
PRIVATEMODE_API_KEY=your-privatemode-api-key
# Base URL for CORS and frontend
BASE_URL=localhost
```
### 3. Deploy with Docker
```bash
# Start all services
docker compose up --build
# Or run in background
docker compose up --build -d
```
### 4. Access Application
- **Main Application**: http://localhost
- **API Documentation**: http://localhost/docs (backend API)
- **Qdrant Dashboard**: http://localhost:56333/dashboard
### 5. Default Login
- **Username**: `admin`
- **Password**: `admin123`
*Change default credentials immediately in production!*
## Documentation
For comprehensive documentation, API references, and advanced configuration:
**[docs.enclava.ai](https://docs.enclava.ai)**
## Architecture
- **Frontend**: Next.js (React/TypeScript) with Tailwind CSS
- **Backend**: FastAPI (Python) with async/await patterns
- **Database**: PostgreSQL with automatic migrations
- **Vector DB**: Qdrant for document embeddings
- **Cache**: Redis for sessions and performance
- **LLM Service**: Native secure LLM service with TEE support
## Services
| Service | Port | Purpose |
|---------|------|---------|
| Nginx (Main) | 80 | Reverse proxy and main access |
| Backend API | 58000 | FastAPI application (internal) |
| Frontend | 3000 | Next.js application (internal) |
| PostgreSQL | 5432 | Primary database |
| Redis | 6379 | Caching and sessions |
| Qdrant | 56333 | Vector database for RAG |
## Configuration
### Environment Variables
See `.env.example` for all available configuration options.
## Support
- **Documentation**: [docs.enclava.ai](https://docs.enclava.ai)
- **Issues**: Use the GitHub issue tracker
- **Security**: Report security issues privately
---

View File

@@ -58,8 +58,7 @@ class Settings(BaseSettings):
# LLM Service Configuration (replaced LiteLLM) # LLM Service Configuration (replaced LiteLLM)
# LLM service configuration is now handled in app/services/llm/config.py # LLM service configuration is now handled in app/services/llm/config.py
# LLM Service Security # LLM Service Security (removed encryption - credentials handled by proxy)
LLM_ENCRYPTION_KEY: Optional[str] = None # Key for encrypting LLM provider API keys
# Plugin System Security # Plugin System Security
PLUGIN_ENCRYPTION_KEY: Optional[str] = None # Key for encrypting plugin secrets and configurations PLUGIN_ENCRYPTION_KEY: Optional[str] = None # Key for encrypting plugin secrets and configurations

View File

@@ -126,9 +126,6 @@ def create_default_config() -> LLMServiceConfig:
class EnvironmentVariables: class EnvironmentVariables:
"""Environment variables used by LLM service""" """Environment variables used by LLM service"""
# Encryption
LLM_ENCRYPTION_KEY: Optional[str] = None
# Provider API keys # Provider API keys
PRIVATEMODE_API_KEY: Optional[str] = None PRIVATEMODE_API_KEY: Optional[str] = None
OPENAI_API_KEY: Optional[str] = None OPENAI_API_KEY: Optional[str] = None
@@ -140,7 +137,6 @@ class EnvironmentVariables:
def __post_init__(self): def __post_init__(self):
"""Load values from environment""" """Load values from environment"""
self.LLM_ENCRYPTION_KEY = os.getenv("LLM_ENCRYPTION_KEY")
self.PRIVATEMODE_API_KEY = os.getenv("PRIVATEMODE_API_KEY") self.PRIVATEMODE_API_KEY = os.getenv("PRIVATEMODE_API_KEY")
self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
self.ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") self.ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
@@ -204,15 +200,9 @@ class ConfigurationManager:
config = self.get_config() config = self.get_config()
return [name for name, provider in config.providers.items() if provider.enabled] return [name for name, provider in config.providers.items() if provider.enabled]
def get_api_key(self, provider_name: str, encrypted: bool = False) -> Optional[str]: def get_api_key(self, provider_name: str) -> Optional[str]:
"""Get API key for provider""" """Get API key for provider"""
api_key = self._env_vars.get_api_key(provider_name) return self._env_vars.get_api_key(provider_name)
if api_key and encrypted:
from .security import security_manager
return security_manager.encrypt_api_key(api_key)
return api_key
def _validate_configuration(self): def _validate_configuration(self):
"""Validate current configuration""" """Validate current configuration"""

View File

@@ -1,7 +1,7 @@
""" """
LLM Security Manager LLM Security Manager
Handles API key encryption, prompt injection detection, and audit logging. Handles prompt injection detection and audit logging.
Provides comprehensive security for LLM interactions. Provides comprehensive security for LLM interactions.
""" """
@@ -12,10 +12,6 @@ import logging
import hashlib import hashlib
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from datetime import datetime from datetime import datetime
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
from app.core.config import settings from app.core.config import settings
@@ -26,51 +22,8 @@ class SecurityManager:
"""Manages security for LLM operations""" """Manages security for LLM operations"""
def __init__(self): def __init__(self):
self._fernet = None
self._setup_encryption()
self._setup_prompt_injection_patterns() self._setup_prompt_injection_patterns()
def _setup_encryption(self):
"""Setup Fernet encryption for API keys"""
try:
# Get encryption key from environment or generate one
encryption_key = os.getenv("LLM_ENCRYPTION_KEY")
if not encryption_key:
# Generate a key if none exists (for development)
# In production, this should be set as an environment variable
logger.warning("LLM_ENCRYPTION_KEY not set, generating temporary key")
key = Fernet.generate_key()
encryption_key = key.decode()
logger.info(f"Generated temporary encryption key: {encryption_key}")
else:
# Validate the key format
try:
key = encryption_key.encode()
Fernet(key) # Test if key is valid
except Exception:
# Key might be a password, derive Fernet key from it
key = self._derive_key_from_password(encryption_key)
self._fernet = Fernet(key if isinstance(key, bytes) else key.encode())
logger.info("Encryption system initialized successfully")
except Exception as e:
logger.error(f"Failed to setup encryption: {e}")
raise RuntimeError("Encryption setup failed")
def _derive_key_from_password(self, password: str) -> bytes:
"""Derive Fernet key from password using PBKDF2"""
# Use a fixed salt for consistency (in production, store this securely)
salt = b"enclava_llm_salt"
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
return key
def _setup_prompt_injection_patterns(self): def _setup_prompt_injection_patterns(self):
"""Setup patterns for prompt injection detection""" """Setup patterns for prompt injection detection"""
@@ -131,30 +84,6 @@ class SecurityManager:
self.compiled_patterns = [re.compile(pattern) for pattern in self.injection_patterns] self.compiled_patterns = [re.compile(pattern) for pattern in self.injection_patterns]
logger.info(f"Initialized {len(self.injection_patterns)} prompt injection patterns") logger.info(f"Initialized {len(self.injection_patterns)} prompt injection patterns")
def encrypt_api_key(self, api_key: str) -> str:
"""Encrypt an API key for secure storage"""
try:
if not api_key:
raise ValueError("API key cannot be empty")
encrypted = self._fernet.encrypt(api_key.encode())
return base64.urlsafe_b64encode(encrypted).decode()
except Exception as e:
logger.error(f"Failed to encrypt API key: {e}")
raise SecurityError("API key encryption failed")
def decrypt_api_key(self, encrypted_key: str) -> str:
"""Decrypt an API key for use"""
try:
if not encrypted_key:
raise ValueError("Encrypted key cannot be empty")
decoded = base64.urlsafe_b64decode(encrypted_key.encode())
decrypted = self._fernet.decrypt(decoded)
return decrypted.decode()
except Exception as e:
logger.error(f"Failed to decrypt API key: {e}")
raise SecurityError("API key decryption failed")
def validate_prompt_security(self, messages: List[Dict[str, str]]) -> Tuple[bool, float, List[str]]: def validate_prompt_security(self, messages: List[Dict[str, str]]) -> Tuple[bool, float, List[str]]:
""" """

View File

@@ -327,9 +327,9 @@ export default function AdminPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex space-x-4"> <div className="flex space-x-4">
<Button onClick={() => window.open('/modules', '_blank')}> <Button onClick={() => window.open('/settings', '_blank')}>
<Database className="mr-2 h-4 w-4" /> <Database className="mr-2 h-4 w-4" />
Module Manager System Settings
</Button> </Button>
<Button variant="outline" onClick={() => window.open('/budgets', '_blank')}> <Button variant="outline" onClick={() => window.open('/budgets', '_blank')}>
<DollarSign className="mr-2 h-4 w-4" /> <DollarSign className="mr-2 h-4 w-4" />

View File

@@ -59,6 +59,22 @@ interface ApiKey {
allowed_chatbots: string[]; allowed_chatbots: string[];
} }
interface Model {
id: string;
object: string;
created?: number;
owned_by?: string;
permission?: any[];
root?: string;
parent?: string;
provider?: string;
capabilities?: string[];
context_window?: number;
max_output_tokens?: number;
supports_streaming?: boolean;
supports_function_calling?: boolean;
}
interface NewApiKeyData { interface NewApiKeyData {
name: string; name: string;
description: string; description: string;
@@ -88,7 +104,7 @@ export default function ApiKeysPage() {
const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null); const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null);
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({}); const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({});
const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({}); const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({});
const [availableModels, setAvailableModels] = useState<any[]>([]); const [availableModels, setAvailableModels] = useState<Model[]>([]);
const [availableChatbots, setAvailableChatbots] = useState<any[]>([]); const [availableChatbots, setAvailableChatbots] = useState<any[]>([]);
const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({ const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({
@@ -108,9 +124,10 @@ export default function ApiKeysPage() {
fetchAvailableModels(); fetchAvailableModels();
fetchAvailableChatbots(); fetchAvailableChatbots();
// Check URL parameters for chatbot pre-selection // Check URL parameters for auto-opening create dialog
const chatbotId = searchParams.get('chatbot'); const chatbotId = searchParams.get('chatbot');
const chatbotName = searchParams.get('chatbot_name'); const chatbotName = searchParams.get('chatbot_name');
const createParam = searchParams.get('create');
if (chatbotId && chatbotName) { if (chatbotId && chatbotName) {
// Pre-populate the form with the chatbot selected and required permissions // Pre-populate the form with the chatbot selected and required permissions
@@ -128,6 +145,9 @@ export default function ApiKeysPage() {
title: "Chatbot Selected", title: "Chatbot Selected",
description: `Creating API key for ${decodeURIComponent(chatbotName)}` description: `Creating API key for ${decodeURIComponent(chatbotName)}`
}); });
} else if (createParam === 'true') {
// Automatically open the create dialog for general API key creation
setShowCreateDialog(true);
} }
}, [searchParams, toast]); }, [searchParams, toast]);
@@ -135,7 +155,7 @@ export default function ApiKeysPage() {
try { try {
setLoading(true); setLoading(true);
const result = await apiClient.get("/api-internal/v1/api-keys") as any; const result = await apiClient.get("/api-internal/v1/api-keys") as any;
setApiKeys(result.data || []); setApiKeys(result.api_keys || result.data || []);
} catch (error) { } catch (error) {
console.error("Failed to fetch API keys:", error); console.error("Failed to fetch API keys:", error);
toast({ toast({
@@ -477,7 +497,7 @@ export default function ApiKeysPage() {
className="rounded" className="rounded"
/> />
<Label htmlFor={`model-${model.id}`} className="text-sm"> <Label htmlFor={`model-${model.id}`} className="text-sm">
{model.name} {model.id}
</Label> </Label>
</div> </div>
))} ))}

View File

@@ -53,8 +53,18 @@ interface APIKey {
interface Model { interface Model {
id: string id: string
name: string object: string
provider: string created?: number
owned_by?: string
permission?: any[]
root?: string
parent?: string
provider?: string
capabilities?: string[]
context_window?: number
max_output_tokens?: number
supports_streaming?: boolean
supports_function_calling?: boolean
} }
export default function LLMPage() { export default function LLMPage() {
@@ -108,12 +118,9 @@ function LLMPageContent() {
}) })
]) ])
console.log('API keys data:', keysData)
setApiKeys(keysData.api_keys || []) setApiKeys(keysData.api_keys || [])
console.log('API keys state updated, count:', keysData.api_keys?.length || 0)
setModels(modelsData.data || []) setModels(modelsData.data || [])
console.log('Data fetch completed successfully')
} catch (error) { } catch (error) {
console.error('Error fetching data:', error) console.error('Error fetching data:', error)
toast({ toast({
@@ -315,7 +322,7 @@ function LLMPageContent() {
<Key className="h-5 w-5" /> <Key className="h-5 w-5" />
API Keys API Keys
</CardTitle> </CardTitle>
<Button onClick={() => router.push('/api-keys')}> <Button onClick={() => router.push('/api-keys?create=true')}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Create API Key Create API Key
</Button> </Button>
@@ -440,15 +447,53 @@ function LLMPageContent() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{models.map((model) => ( {models.map((model) => {
<div key={model.id} className="border rounded-lg p-4"> // Helper function to get provider from model ID
<h3 className="font-medium">{model.id}</h3> const getProviderFromModel = (modelId: string): string => {
<p className="text-sm text-muted-foreground">Provider: {model.owned_by}</p> if (modelId.startsWith('privatemode-')) return 'PrivateMode.ai'
<Badge variant="outline" className="mt-2"> if (modelId.startsWith('gpt-') || modelId.includes('openai')) return 'OpenAI'
{model.object} if (modelId.startsWith('claude-') || modelId.includes('anthropic')) return 'Anthropic'
</Badge> if (modelId.startsWith('gemini-') || modelId.includes('google')) return 'Google'
</div> if (modelId.includes('cohere')) return 'Cohere'
))} if (modelId.includes('mistral')) return 'Mistral'
if (modelId.includes('llama') && !modelId.startsWith('privatemode-')) return 'Meta'
return model.owned_by || 'Unknown'
}
return (
<div key={model.id} className="border rounded-lg p-4">
<h3 className="font-medium">{model.id}</h3>
<p className="text-sm text-muted-foreground">
Provider: {getProviderFromModel(model.id)}
</p>
<div className="flex gap-2 mt-2">
<Badge variant="outline">
{model.object || 'model'}
</Badge>
{model.supports_streaming && (
<Badge variant="secondary" className="text-xs">
Streaming
</Badge>
)}
{model.supports_function_calling && (
<Badge variant="secondary" className="text-xs">
Functions
</Badge>
)}
{model.capabilities?.includes('tee') && (
<Badge variant="outline" className="text-xs border-green-500 text-green-700">
TEE
</Badge>
)}
</div>
{model.context_window && (
<p className="text-xs text-muted-foreground mt-2">
Context: {model.context_window.toLocaleString()} tokens
</p>
)}
</div>
)
})}
{models.length === 0 && ( {models.length === 0 && (
<div className="col-span-full text-center py-8 text-muted-foreground"> <div className="col-span-full text-center py-8 text-muted-foreground">
No models available. Check your LLM platform configuration. No models available. Check your LLM platform configuration.

View File

@@ -1,399 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Database,
Play,
Square,
RefreshCw,
Settings,
Activity,
AlertTriangle,
CheckCircle,
Clock,
Cpu,
BarChart3
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { ProtectedRoute } from '@/components/auth/ProtectedRoute'
import { useModules, triggerModuleRefresh } from '@/contexts/ModulesContext'
import { apiClient } from '@/lib/api-client'
interface Module {
name: string;
status: "loaded" | "failed" | "disabled";
dependencies: string[];
config: Record<string, any>;
metrics?: {
requests_processed: number;
average_response_time: number;
error_rate: number;
last_activity: string;
};
health?: {
status: "healthy" | "warning" | "error";
message: string;
uptime: number;
};
}
interface ModuleStats {
total_modules: number;
loaded_modules: number;
failed_modules: number;
system_health: "healthy" | "warning" | "error";
}
export default function ModulesPage() {
return (
<ProtectedRoute>
<ModulesPageContent />
</ProtectedRoute>
)
}
function ModulesPageContent() {
const { toast } = useToast();
const { modules: contextModules, isLoading: contextLoading, refreshModules } = useModules();
const [stats, setStats] = useState<ModuleStats | null>(null);
const [actionLoading, setActionLoading] = useState<string | null>(null);
// Transform context modules to match existing interface
const modules: Module[] = contextModules.map(module => ({
name: module.name,
status: module.initialized && module.enabled ? "loaded" :
!module.enabled ? "disabled" : "failed",
dependencies: [], // Not provided in current API
config: module.stats || {},
metrics: {
requests_processed: module.stats?.total_requests || 0,
average_response_time: module.stats?.avg_analysis_time || 0,
error_rate: module.stats?.errors || 0,
last_activity: new Date().toISOString(),
},
health: {
status: module.initialized && module.enabled ? "healthy" : "error",
message: module.initialized && module.enabled ? "Module is running" :
!module.enabled ? "Module is disabled" : "Module failed to initialize",
uptime: module.stats?.uptime || 0,
}
}));
const loading = contextLoading;
useEffect(() => {
// Calculate stats from context modules
setStats({
total_modules: contextModules.length,
loaded_modules: contextModules.filter(m => m.initialized && m.enabled).length,
failed_modules: contextModules.filter(m => !m.initialized || !m.enabled).length,
system_health: contextModules.some(m => !m.initialized) ? "warning" : "healthy"
});
}, [contextModules]);
const handleModuleAction = async (moduleName: string, action: "start" | "stop" | "restart" | "reload") => {
try {
setActionLoading(`${moduleName}-${action}`);
const responseData = await apiClient.post(`/api-internal/v1/modules/${moduleName}/${action}`, {});
toast({
title: "Success",
description: `Module ${moduleName} ${action}ed successfully`,
});
// Refresh modules context and trigger navigation update
await refreshModules();
// Trigger navigation refresh if the response indicates it's needed
if (responseData.refreshRequired) {
triggerModuleRefresh();
}
} catch (error) {
console.error(`Failed to ${action} module:`, error);
toast({
title: "Error",
description: error instanceof Error ? error.message : `Failed to ${action} module`,
variant: "destructive",
});
} finally {
setActionLoading(null);
}
};
const handleModuleToggle = async (moduleName: string, enabled: boolean) => {
await handleModuleAction(moduleName, enabled ? "start" : "stop");
};
const getStatusIcon = (status: string) => {
switch (status) {
case "loaded":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "failed":
return <AlertTriangle className="h-4 w-4 text-red-500" />;
case "disabled":
return <Square className="h-4 w-4 text-gray-500" />;
default:
return <Clock className="h-4 w-4 text-yellow-500" />;
}
};
const getStatusBadge = (status: string) => {
const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
loaded: "default",
failed: "destructive",
disabled: "secondary"
};
return <Badge variant={variants[status] || "outline"}>{status}</Badge>;
};
const formatUptime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-empire-gold"></div>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">Module Manager</h1>
<p className="text-muted-foreground">
Manage dynamic modules and monitor system performance
</p>
</div>
<div className="flex space-x-2">
<Button onClick={refreshModules} variant="outline">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={() => handleModuleAction("all", "reload")}>
<Database className="mr-2 h-4 w-4" />
Reload All
</Button>
</div>
</div>
{/* System Health Alert */}
{stats && stats.system_health !== "healthy" && (
<Alert className="mb-6">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
System health: {stats.system_health}. Some modules may not be functioning properly.
</AlertDescription>
</Alert>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Modules</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.total_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Loaded</CardTitle>
<CheckCircle className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats?.loaded_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Failed</CardTitle>
<AlertTriangle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{stats?.failed_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Health</CardTitle>
{getStatusIcon(stats?.system_health || "unknown")}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold capitalize">{stats?.system_health || "Unknown"}</div>
</CardContent>
</Card>
</div>
{/* Modules List */}
<Tabs defaultValue="overview" className="space-y-6">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="performance">Performance</TabsTrigger>
<TabsTrigger value="configuration">Configuration</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{modules.map((module) => (
<Card key={module.name}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
{getStatusIcon(module.status)}
<div>
<CardTitle className="text-lg">{module.name}</CardTitle>
<CardDescription>
Dependencies: {module.dependencies.length > 0 ? module.dependencies.join(", ") : "None"}
</CardDescription>
</div>
</div>
<div className="flex items-center space-x-3">
{getStatusBadge(module.status)}
<Switch
checked={module.status === "loaded"}
onCheckedChange={(checked) => handleModuleToggle(module.name, checked)}
disabled={actionLoading?.startsWith(module.name)}
/>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2 mb-4">
<Button
size="sm"
variant="outline"
onClick={() => handleModuleAction(module.name, "restart")}
disabled={actionLoading === `${module.name}-restart`}
>
<RefreshCw className="mr-2 h-3 w-3" />
Restart
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleModuleAction(module.name, "reload")}
disabled={actionLoading === `${module.name}-reload`}
>
<Database className="mr-2 h-3 w-3" />
Reload
</Button>
{module.name === 'signal' && (
<Button
size="sm"
variant="outline"
onClick={() => window.location.href = '/signal'}
>
<Settings className="mr-2 h-3 w-3" />
Configure
</Button>
)}
{module.name === 'zammad' && (
<Button
size="sm"
variant="outline"
onClick={() => window.location.href = '/zammad'}
>
<Settings className="mr-2 h-3 w-3" />
Configure
</Button>
)}
</div>
{module.health && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium">Health:</span> {module.health.status}
</div>
<div>
<span className="font-medium">Uptime:</span> {formatUptime(module.health.uptime)}
</div>
<div>
<span className="font-medium">Message:</span> {module.health.message}
</div>
</div>
)}
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="performance" className="space-y-4">
{modules
.filter((module) => module.metrics)
.map((module) => (
<Card key={module.name}>
<CardHeader>
<CardTitle className="flex items-center">
<BarChart3 className="mr-2 h-5 w-5" />
{module.name} Performance
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold">{module.metrics?.requests_processed || 0}</div>
<p className="text-xs text-muted-foreground">Requests Processed</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{module.metrics?.average_response_time || 0}ms</div>
<p className="text-xs text-muted-foreground">Avg Response Time</p>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{(module.metrics?.error_rate || 0).toFixed(2)}%</div>
<p className="text-xs text-muted-foreground">Error Rate</p>
</div>
<div className="text-center">
<div className="text-sm font-medium">
{module.metrics?.last_activity ?
new Date(module.metrics.last_activity).toLocaleString() :
"Never"
}
</div>
<p className="text-xs text-muted-foreground">Last Activity</p>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="configuration" className="space-y-4">
{modules.map((module) => (
<Card key={module.name}>
<CardHeader>
<CardTitle className="flex items-center">
<Settings className="mr-2 h-5 w-5" />
{module.name} Configuration
</CardTitle>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-lg text-sm overflow-auto">
{JSON.stringify(module.config, null, 2)}
</pre>
</CardContent>
</Card>
))}
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -25,10 +25,15 @@ import {
Server, Server,
AlertTriangle, AlertTriangle,
CheckCircle, CheckCircle,
Info Info,
Square,
Clock,
Play
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { apiClient } from "@/lib/api-client"; import { apiClient } from "@/lib/api-client";
import { useModules, triggerModuleRefresh } from '@/contexts/ModulesContext';
import { Badge } from '@/components/ui/badge';
interface SystemSettings { interface SystemSettings {
// Security Settings // Security Settings
@@ -95,6 +100,31 @@ interface SystemSettings {
}; };
} }
interface Module {
name: string;
status: "loaded" | "failed" | "disabled";
dependencies: string[];
config: Record<string, any>;
metrics?: {
requests_processed: number;
average_response_time: number;
error_rate: number;
last_activity: string;
};
health?: {
status: "healthy" | "warning" | "error";
message: string;
uptime: number;
};
}
interface ModuleStats {
total_modules: number;
loaded_modules: number;
failed_modules: number;
system_health: "healthy" | "warning" | "error";
}
function SettingsPageContent() { function SettingsPageContent() {
const { toast } = useToast(); const { toast } = useToast();
const [settings, setSettings] = useState<SystemSettings | null>(null); const [settings, setSettings] = useState<SystemSettings | null>(null);
@@ -102,10 +132,46 @@ function SettingsPageContent() {
const [saving, setSaving] = useState<string | null>(null); const [saving, setSaving] = useState<string | null>(null);
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
// Modules state
const { modules: contextModules, isLoading: modulesLoading, refreshModules } = useModules();
const [moduleStats, setModuleStats] = useState<ModuleStats | null>(null);
const [actionLoading, setActionLoading] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetchSettings(); fetchSettings();
}, []); }, []);
// Transform context modules to match existing interface
const modules: Module[] = contextModules.map(module => ({
name: module.name,
status: module.initialized && module.enabled ? "loaded" :
!module.enabled ? "disabled" : "failed",
dependencies: [], // Not provided in current API
config: module.stats || {},
metrics: {
requests_processed: module.stats?.total_requests || 0,
average_response_time: module.stats?.avg_analysis_time || 0,
error_rate: module.stats?.errors || 0,
last_activity: new Date().toISOString(),
},
health: {
status: module.initialized && module.enabled ? "healthy" : "error",
message: module.initialized && module.enabled ? "Module is running" :
!module.enabled ? "Module is disabled" : "Module failed to initialize",
uptime: module.stats?.uptime || 0,
}
}));
useEffect(() => {
// Calculate stats from context modules
setModuleStats({
total_modules: contextModules.length,
loaded_modules: contextModules.filter(m => m.initialized && m.enabled).length,
failed_modules: contextModules.filter(m => !m.initialized || !m.enabled).length,
system_health: contextModules.some(m => !m.initialized) ? "warning" : "healthy"
});
}, [contextModules]);
const fetchSettings = async () => { const fetchSettings = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -202,6 +268,68 @@ function SettingsPageContent() {
} }
}; };
const handleModuleAction = async (moduleName: string, action: "start" | "stop" | "restart" | "reload") => {
try {
setActionLoading(`${moduleName}-${action}`);
const responseData = await apiClient.post(`/api-internal/v1/modules/${moduleName}/${action}`, {});
toast({
title: "Success",
description: `Module ${moduleName} ${action}ed successfully`,
});
// Refresh modules context and trigger navigation update
await refreshModules();
// Trigger navigation refresh if the response indicates it's needed
if (responseData.refreshRequired) {
triggerModuleRefresh();
}
} catch (error) {
console.error(`Failed to ${action} module:`, error);
toast({
title: "Error",
description: error instanceof Error ? error.message : `Failed to ${action} module`,
variant: "destructive",
});
} finally {
setActionLoading(null);
}
};
const handleModuleToggle = async (moduleName: string, enabled: boolean) => {
await handleModuleAction(moduleName, enabled ? "start" : "stop");
};
const getStatusIcon = (status: string) => {
switch (status) {
case "loaded":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "failed":
return <AlertTriangle className="h-4 w-4 text-red-500" />;
case "disabled":
return <Square className="h-4 w-4 text-gray-500" />;
default:
return <Clock className="h-4 w-4 text-yellow-500" />;
}
};
const getStatusBadge = (status: string) => {
const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
loaded: "default",
failed: "destructive",
disabled: "secondary"
};
return <Badge variant={variants[status] || "outline"}>{status}</Badge>;
};
const formatUptime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
};
if (loading) { if (loading) {
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
@@ -261,10 +389,11 @@ function SettingsPageContent() {
)} )}
<Tabs defaultValue="security" className="space-y-6"> <Tabs defaultValue="security" className="space-y-6">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="security">Security</TabsTrigger> <TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="api">API</TabsTrigger> <TabsTrigger value="api">API</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger> <TabsTrigger value="notifications">Notifications</TabsTrigger>
<TabsTrigger value="modules">Modules</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="security" className="space-y-6"> <TabsContent value="security" className="space-y-6">
@@ -849,6 +978,135 @@ function SettingsPageContent() {
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="modules" className="space-y-6">
{/* System Health Alert */}
{moduleStats && moduleStats.system_health !== "healthy" && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
System health: {moduleStats.system_health}. Some modules may not be functioning properly.
</AlertDescription>
</Alert>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Modules</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{moduleStats?.total_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Loaded</CardTitle>
<CheckCircle className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{moduleStats?.loaded_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Failed</CardTitle>
<AlertTriangle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{moduleStats?.failed_modules || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Health</CardTitle>
{getStatusIcon(moduleStats?.system_health || "unknown")}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold capitalize">{moduleStats?.system_health || "Unknown"}</div>
</CardContent>
</Card>
</div>
{/* Modules List */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Module Overview</h3>
<div className="flex space-x-2">
<Button onClick={refreshModules} variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
</div>
{modules.map((module) => (
<Card key={module.name}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
{getStatusIcon(module.status)}
<div>
<CardTitle className="text-lg">{module.name}</CardTitle>
<CardDescription>
Dependencies: {module.dependencies.length > 0 ? module.dependencies.join(", ") : "None"}
</CardDescription>
</div>
</div>
<div className="flex items-center space-x-3">
{getStatusBadge(module.status)}
<Switch
checked={module.status === "loaded"}
onCheckedChange={(checked) => handleModuleToggle(module.name, checked)}
disabled={actionLoading?.startsWith(module.name)}
/>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2 mb-4">
<Button
size="sm"
variant="outline"
onClick={() => handleModuleAction(module.name, "restart")}
disabled={actionLoading === `${module.name}-restart`}
>
<RefreshCw className="mr-2 h-3 w-3" />
Restart
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleModuleAction(module.name, "reload")}
disabled={actionLoading === `${module.name}-reload`}
>
<Database className="mr-2 h-3 w-3" />
Reload
</Button>
</div>
{module.health && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium">Health:</span> {module.health.status}
</div>
<div>
<span className="font-medium">Uptime:</span> {formatUptime(module.health.uptime)}
</div>
<div>
<span className="font-medium">Message:</span> {module.health.message}
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@@ -19,7 +19,6 @@ import {
Plus, Plus,
Settings, Settings,
Trash2, Trash2,
Copy,
Play, Play,
Bot, Bot,
Brain, Brain,
@@ -27,7 +26,9 @@ import {
BookOpen, BookOpen,
Palette, Palette,
Key, Key,
Globe Globe,
Copy,
Link
} from "lucide-react" } from "lucide-react"
import { useToast } from "@/hooks/use-toast" import { useToast } from "@/hooks/use-toast"
import { ChatInterface } from "./ChatInterface" import { ChatInterface } from "./ChatInterface"
@@ -684,11 +685,12 @@ export function ChatbotManager() {
</DialogHeader> </DialogHeader>
<Tabs defaultValue="basic" className="mt-6"> <Tabs defaultValue="basic" className="mt-6">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic">Basic</TabsTrigger> <TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="personality">Personality</TabsTrigger> <TabsTrigger value="personality">Personality</TabsTrigger>
<TabsTrigger value="knowledge">Knowledge</TabsTrigger> <TabsTrigger value="knowledge">Knowledge</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
<TabsTrigger value="integration">Integration</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="basic" className="space-y-4 mt-6"> <TabsContent value="basic" className="space-y-4 mt-6">
@@ -925,6 +927,115 @@ export function ChatbotManager() {
/> />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="integration" className="space-y-6 mt-6">
{editingChatbot && (
<div className="space-y-6">
<div className="flex items-center space-x-3 p-4 bg-muted/50 rounded-lg">
<Link className="h-5 w-5 text-primary" />
<div>
<h3 className="font-medium">API Integration</h3>
<p className="text-sm text-muted-foreground">
Use this OpenAI-compatible endpoint to integrate your chatbot into external applications
</p>
</div>
</div>
<div className="space-y-4">
<div>
<Label className="text-sm font-medium">OpenAI-Compatible Endpoint</Label>
<p className="text-xs text-muted-foreground mb-2">
Standard OpenAI API interface for seamless integration with existing tools and libraries
</p>
<div className="flex items-center space-x-2">
<Input
value={`${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions`}
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions`)
toast({
title: "Copied!",
description: "OpenAI endpoint copied to clipboard"
})
}}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-start space-x-2">
<div className="text-blue-600 dark:text-blue-400">
<Globe className="h-4 w-4 mt-0.5" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
Authentication Required
</p>
<p className="text-xs text-blue-700 dark:text-blue-200 mt-1">
These endpoints require API key authentication. Create and manage API keys from the{" "}
<button
className="underline hover:no-underline"
onClick={() => handleManageApiKeys(editingChatbot)}
>
API Keys page
</button>
.
</p>
</div>
</div>
</div>
<div className="space-y-3">
<h4 className="text-sm font-medium">Example Usage</h4>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">cURL Example</Label>
<div className="relative">
<pre className="text-xs bg-muted p-3 rounded-md overflow-x-auto">
<code>{`curl -X POST "${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 150,
"temperature": 0.7
}'`}</code>
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2"
onClick={() => {
navigator.clipboard.writeText(`curl -X POST "${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 150,
"temperature": 0.7
}'`)
toast({
title: "Copied!",
description: "cURL example copied to clipboard"
})
}}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
</div>
)}
</TabsContent>
</Tabs> </Tabs>
<div className="flex justify-end space-x-2 mt-6 pt-6 border-t"> <div className="flex justify-end space-x-2 mt-6 pt-6 border-t">
@@ -985,23 +1096,50 @@ export function ChatbotManager() {
</div> </div>
</div> </div>
<div className="flex items-center space-x-2 mt-4 pt-4 border-t"> <div className="space-y-3 mt-4 pt-4 border-t">
<Button size="sm" className="flex-1" onClick={() => handleTestChat(chatbot)}> <div className="flex items-center space-x-2">
<Play className="h-4 w-4 mr-2" /> <Button size="sm" className="flex-1" onClick={() => handleTestChat(chatbot)}>
Test Chat <Play className="h-4 w-4 mr-2" />
</Button> Test Chat
<Button size="sm" variant="outline" onClick={() => handleEditChat(chatbot)}> </Button>
<Settings className="h-4 w-4" /> <Button size="sm" variant="outline" onClick={() => handleEditChat(chatbot)}>
</Button> <Settings className="h-4 w-4" />
<Button size="sm" variant="outline" onClick={() => handleManageApiKeys(chatbot)}> </Button>
<Key className="h-4 w-4" /> <Button size="sm" variant="outline" onClick={() => handleManageApiKeys(chatbot)}>
</Button> <Key className="h-4 w-4" />
<Button size="sm" variant="outline"> </Button>
<Copy className="h-4 w-4" /> <Button size="sm" variant="outline" onClick={() => handleDeleteChat(chatbot)}>
</Button> <Trash2 className="h-4 w-4" />
<Button size="sm" variant="outline" onClick={() => handleDeleteChat(chatbot)}> </Button>
<Trash2 className="h-4 w-4" /> </div>
</Button>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground flex items-center">
<Link className="h-3 w-3 mr-1" />
Integration URL
</Label>
<div className="flex items-center space-x-2">
<Input
value={`${window.location.origin}/api/v1/chatbot/external/${chatbot.id}/chat/completions`}
readOnly
className="font-mono text-xs h-8"
/>
<Button
variant="outline"
size="sm"
className="h-8 px-2"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/api/v1/chatbot/external/${chatbot.id}/chat/completions`)
toast({
title: "Copied!",
description: "Integration URL copied to clipboard"
})
}}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -79,7 +79,6 @@ const Navigation = () => {
label: "Settings", label: "Settings",
children: [ children: [
{ href: "/settings", label: "System Settings" }, { href: "/settings", label: "System Settings" },
{ href: "/modules", label: "Modules" },
{ href: "/plugins", label: "Plugins" }, { href: "/plugins", label: "Plugins" },
{ href: "/prompt-templates", label: "Prompt Templates" }, { href: "/prompt-templates", label: "Prompt Templates" },
] ]