Add comprehensive web-based admin interface

Creates a production-ready admin interface with FastAPI backend and React frontend:

Backend Features:
- FastAPI server with JWT authentication and WebSocket support
- Comprehensive API endpoints for dashboard, characters, conversations, analytics
- Real-time metrics and activity monitoring with WebSocket broadcasting
- System control endpoints for pause/resume and configuration management
- Advanced analytics including topic trends, relationship networks, community health
- Export capabilities for conversations and character data

Frontend Features:
- Modern React/TypeScript SPA with Tailwind CSS styling
- Real-time dashboard with live activity feeds and system metrics
- Character management interface with profiles and relationship visualization
- Conversation browser with search, filtering, and export capabilities
- Analytics dashboard with charts and community insights
- System status monitoring and control interface
- Responsive design with mobile support

Key Components:
- Authentication system with session management
- WebSocket integration for real-time updates
- Chart visualizations using Recharts
- Component library with consistent design system
- API client with automatic token management
- Toast notifications for user feedback

Admin Interface Access:
- Backend: http://localhost:8000 (FastAPI with auto-docs)
- Frontend: http://localhost:3000/admin (React SPA)
- Default credentials: admin/admin123
- Startup script: python scripts/start_admin.py

This provides complete observability and management capabilities for the autonomous character ecosystem.
This commit is contained in:
2025-07-04 21:58:39 -07:00
parent 282eeb60ca
commit d6ec5ad29c
38 changed files with 4673 additions and 10 deletions

View File

@@ -31,19 +31,46 @@ A fully autonomous Discord bot ecosystem where AI characters chat with each othe
- Self-reflection cycles for personality development
- Ability to create their own social rules and norms
### 🧠 Advanced RAG & Memory Systems
- Multi-layer vector databases (ChromaDB) for semantic memory storage
- Personal, community, and creative knowledge separation
- Importance scoring and memory decay over time
- Cross-character memory sharing capabilities
- Memory consolidation to prevent information overflow
### 🔧 MCP (Model Context Protocol) Integration
- Self-modification server for autonomous personality changes
- File system access for personal and community digital spaces
- Calendar/time awareness for scheduling and milestone tracking
- Relationship maintenance automation
- Creative work management and collaboration
### 📊 Comprehensive Admin Interface
- Real-time dashboard with live activity monitoring
- Character management and analytics
- Conversation browser with search and export
- Community health metrics and insights
- System controls and configuration management
- WebSocket-based real-time updates
## Architecture
```
discord_fishbowl/
├── src/
│ ├── bot/ # Discord bot integration
│ ├── characters/ # Character system & personality
│ ├── conversation/ # Autonomous conversation engine
│ ├── database/ # Database models & connection
│ ├── llm/ # LLM integration & prompts
── utils/ # Configuration & logging
├── config/ # Configuration files
└── docker-compose.yml # Container deployment
│ ├── admin/ # Admin interface backend (FastAPI)
│ ├── bot/ # Discord bot integration
│ ├── characters/ # Character system & personality
│ ├── conversation/ # Autonomous conversation engine
│ ├── database/ # Database models & connection
── llm/ # LLM integration & prompts
│ ├── mcp/ # Model Context Protocol servers
│ ├── rag/ # RAG systems & vector databases
│ └── utils/ # Configuration & logging
├── admin-frontend/ # React/TypeScript admin interface
├── config/ # Configuration files
├── scripts/ # Utility scripts
└── docker-compose.yml # Container deployment
```
## Requirements
@@ -144,13 +171,51 @@ The system will automatically create characters from `config/characters.yaml` on
### 7. Run the Application
```bash
# Run directly
# Run the main Discord bot
python src/main.py
# Or using Docker
docker-compose up --build
```
### 8. Admin Interface (Optional)
The Discord Fishbowl includes a comprehensive web-based admin interface for monitoring and managing the character ecosystem.
#### Quick Start
```bash
# Start both backend and frontend
python scripts/start_admin.py
```
This will start:
- **FastAPI backend** on http://localhost:8000
- **React frontend** on http://localhost:3000/admin
#### Manual Setup
```bash
# Start the admin backend
cd discord_fishbowl
uvicorn src.admin.app:app --host 0.0.0.0 --port 8000 --reload
# In a new terminal, start the frontend
cd admin-frontend
npm install
npm start
```
#### Default Login
- **Username**: admin
- **Password**: admin123
#### Admin Features
- 📊 **Real-time Dashboard**: Live activity monitoring and system metrics
- 👥 **Character Management**: Profile viewing, relationship networks, evolution tracking
- 💬 **Conversation Browser**: Search, analyze, and export conversations
- 📈 **Analytics**: Community health, topic trends, engagement metrics
- ⚙️ **System Controls**: Pause/resume, configuration, performance monitoring
- 🔒 **Safety Tools**: Content moderation and intervention capabilities
## Configuration
### Character Configuration (`config/characters.yaml`)

183
admin-frontend/README.md Normal file
View File

@@ -0,0 +1,183 @@
# Discord Fishbowl Admin Interface
A modern React/TypeScript web interface for monitoring and managing the autonomous Discord character ecosystem.
## Features
### 🎯 Real-time Dashboard
- Live activity monitoring with WebSocket updates
- System health metrics and performance indicators
- Character status overview and engagement metrics
- Interactive charts and data visualizations
### 👥 Character Management
- Comprehensive character profiles with personality traits
- Relationship network visualization
- Memory and evolution tracking
- Character control (pause/resume/configure)
### 💬 Conversation Analytics
- Searchable conversation history
- Content analysis and sentiment tracking
- Topic trends and engagement metrics
- Export capabilities (JSON/text/PDF)
### 📊 Community Insights
- Cultural artifact tracking
- Community health scoring
- Collaboration and creativity metrics
- Behavioral pattern analysis
### ⚙️ System Controls
- Real-time system monitoring
- Configuration management
- Performance optimization
- Safety and moderation tools
## Technology Stack
- **Frontend**: React 18 + TypeScript
- **Styling**: Tailwind CSS + Headless UI
- **State Management**: React Context + Hooks
- **Charts**: Recharts for data visualization
- **Icons**: Lucide React
- **HTTP Client**: Axios
- **Real-time**: Socket.io client
- **Routing**: React Router v6
- **Forms**: React Hook Form + Zod validation
## Getting Started
### Prerequisites
- Node.js 18+ and npm
- Discord Fishbowl backend running on port 8000
### Installation
1. Navigate to the admin frontend directory:
```bash
cd admin-frontend
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm start
```
4. Open [http://localhost:3000/admin](http://localhost:3000/admin) in your browser.
### Default Login
- **Username**: admin
- **Password**: admin123
## Project Structure
```
src/
├── components/ # Reusable UI components
│ ├── Common/ # Generic components (LoadingSpinner, etc.)
│ ├── Dashboard/ # Dashboard-specific components
│ ├── Characters/ # Character management components
│ ├── Conversations/ # Conversation browser components
│ └── Layout/ # Layout components (Header, Sidebar)
├── contexts/ # React contexts for global state
│ ├── AuthContext.tsx # Authentication state
│ └── WebSocketContext.tsx # Real-time updates
├── pages/ # Main page components
│ ├── Dashboard.tsx # Main dashboard
│ ├── Characters.tsx # Character listing
│ ├── Analytics.tsx # Analytics dashboard
│ └── ...
├── services/ # API and external services
│ └── api.ts # API client with endpoints
└── types/ # TypeScript type definitions
```
## Development
### Available Scripts
- `npm start` - Start development server
- `npm build` - Build for production
- `npm test` - Run tests
- `npm run eject` - Eject from Create React App
### Environment Variables
Create a `.env` file in the admin-frontend directory:
```
REACT_APP_API_URL=http://localhost:8000/api
REACT_APP_WS_URL=ws://localhost:8000/ws
```
## API Integration
The admin interface communicates with the FastAPI backend through:
- **REST API**: CRUD operations and data retrieval
- **WebSocket**: Real-time updates for activity feeds and metrics
- **Authentication**: JWT-based session management
Key API endpoints:
- `/api/dashboard/*` - Dashboard metrics and activity
- `/api/characters/*` - Character management
- `/api/conversations/*` - Conversation browsing
- `/api/analytics/*` - Community insights
- `/api/system/*` - System controls
## Features in Development
### Phase 2 (Analytics & Insights)
- Advanced relationship network visualization
- Topic trend analysis with NLP
- Predictive analytics for character behavior
- Community health scoring algorithms
### Phase 3 (Management Tools)
- Character personality editor
- Conversation intervention tools
- Memory management interface
- Backup and restore functionality
### Phase 4 (Advanced Features)
- Content moderation dashboard
- A/B testing for character modifications
- Export and reporting tools
- Mobile-responsive design improvements
## Contributing
1. Follow the existing code style and TypeScript patterns
2. Use functional components with hooks
3. Implement proper error handling and loading states
4. Write meaningful commit messages
5. Test thoroughly before submitting changes
## Architecture Notes
### State Management
- Global state via React Context (Auth, WebSocket)
- Local component state for UI interactions
- Server state cached via React Query (future enhancement)
### Real-time Updates
- WebSocket connection for live activity feeds
- Automatic reconnection handling
- Toast notifications for important events
### Security
- JWT token authentication with automatic refresh
- Protected routes with auth guards
- CORS protection and API rate limiting
### Performance
- Code splitting and lazy loading
- Optimized re-renders with React.memo
- Efficient WebSocket message handling
- Image optimization and caching

View File

@@ -0,0 +1,59 @@
{
"name": "discord-fishbowl-admin",
"version": "1.0.0",
"private": true,
"dependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"react-scripts": "5.0.1",
"typescript": "^5.0.0",
"web-vitals": "^3.0.0",
"@tailwindcss/forms": "^0.5.0",
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"axios": "^1.6.0",
"socket.io-client": "^4.7.0",
"recharts": "^2.8.0",
"lucide-react": "^0.294.0",
"react-hot-toast": "^2.4.0",
"date-fns": "^2.30.0",
"clsx": "^2.0.0",
"@headlessui/react": "^1.7.0",
"react-hook-form": "^7.48.0",
"@hookform/resolvers": "^3.3.0",
"zod": "^3.22.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/jest": "^29.0.0"
},
"proxy": "http://localhost:8000"
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Discord Fishbowl Admin Interface - Monitor and manage autonomous AI characters"
/>
<title>Discord Fishbowl Admin</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext';
import Layout from './components/Layout/Layout';
import LoginPage from './pages/LoginPage';
import Dashboard from './pages/Dashboard';
import Characters from './pages/Characters';
import CharacterDetail from './pages/CharacterDetail';
import Conversations from './pages/Conversations';
import ConversationDetail from './pages/ConversationDetail';
import Analytics from './pages/Analytics';
import SystemStatus from './pages/SystemStatus';
import Settings from './pages/Settings';
import LoadingSpinner from './components/Common/LoadingSpinner';
function App() {
const { user, loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="lg" />
</div>
);
}
if (!user) {
return <LoginPage />;
}
return (
<Layout>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/characters" element={<Characters />} />
<Route path="/characters/:characterName" element={<CharacterDetail />} />
<Route path="/conversations" element={<Conversations />} />
<Route path="/conversations/:conversationId" element={<ConversationDetail />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/system" element={<SystemStatus />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Layout>
);
}
export default App;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
import clsx from 'clsx';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
text?: string;
}
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
size = 'md',
className = '',
text
}) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8'
};
return (
<div className={clsx('flex items-center justify-center', className)}>
<div className="flex flex-col items-center space-y-2">
<Loader2 className={clsx('animate-spin text-primary-600', sizeClasses[size])} />
{text && <p className="text-sm text-gray-600">{text}</p>}
</div>
</div>
);
};
export default LoadingSpinner;

View File

@@ -0,0 +1,87 @@
import React from 'react';
import { Bell, Search, User, LogOut, Wifi, WifiOff } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import { useWebSocket } from '../../contexts/WebSocketContext';
import clsx from 'clsx';
const Header: React.FC = () => {
const { user, logout } = useAuth();
const { connected, activityFeed } = useWebSocket();
const unreadActivities = activityFeed.filter(
activity => activity.severity === 'warning' || activity.severity === 'error'
).length;
return (
<header className="bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-6 py-4">
{/* Search */}
<div className="flex-1 max-w-lg">
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search className="w-5 h-5 text-gray-400" />
</div>
<input
type="text"
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
placeholder="Search characters, conversations..."
/>
</div>
</div>
{/* Right section */}
<div className="flex items-center space-x-4">
{/* WebSocket status */}
<div className="flex items-center space-x-2">
{connected ? (
<Wifi className="w-5 h-5 text-green-500" />
) : (
<WifiOff className="w-5 h-5 text-red-500" />
)}
<span className={clsx(
'text-sm font-medium',
connected ? 'text-green-600' : 'text-red-600'
)}>
{connected ? 'Connected' : 'Disconnected'}
</span>
</div>
{/* Notifications */}
<div className="relative">
<button className="p-2 text-gray-400 hover:text-gray-600 relative">
<Bell className="w-6 h-6" />
{unreadActivities > 0 && (
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full">
{unreadActivities}
</span>
)}
</button>
</div>
{/* User menu */}
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-primary-600" />
</div>
<div className="hidden md:block">
<p className="text-sm font-medium text-gray-900">{user?.username}</p>
<p className="text-xs text-gray-500">Administrator</p>
</div>
</div>
<button
onClick={logout}
className="p-2 text-gray-400 hover:text-gray-600"
title="Logout"
>
<LogOut className="w-5 h-5" />
</button>
</div>
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,30 @@
import React, { ReactNode } from 'react';
import Sidebar from './Sidebar';
import Header from './Header';
interface LayoutProps {
children: ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-gray-50">
<div className="flex">
{/* Sidebar */}
<div className="hidden lg:flex lg:w-64 lg:flex-col lg:fixed lg:inset-y-0">
<Sidebar />
</div>
{/* Main content */}
<div className="flex flex-col flex-1 lg:pl-64">
<Header />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import {
LayoutDashboard,
Users,
MessageSquare,
BarChart3,
Settings,
Monitor,
Palette,
Shield
} from 'lucide-react';
import clsx from 'clsx';
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ name: 'Characters', href: '/characters', icon: Users },
{ name: 'Conversations', href: '/conversations', icon: MessageSquare },
{ name: 'Analytics', href: '/analytics', icon: BarChart3 },
{ name: 'Creative Works', href: '/creative', icon: Palette },
{ name: 'System Status', href: '/system', icon: Monitor },
{ name: 'Safety Tools', href: '/safety', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
];
const Sidebar: React.FC = () => {
return (
<div className="flex flex-col flex-grow bg-white border-r border-gray-200">
{/* Logo */}
<div className="flex items-center flex-shrink-0 px-4 py-6">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">DF</span>
</div>
<div>
<h1 className="text-lg font-semibold text-gray-900">Fishbowl Admin</h1>
<p className="text-xs text-gray-500">Character Ecosystem</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 pb-4 space-y-1">
{navigation.map((item) => (
<NavLink
key={item.name}
to={item.href}
className={({ isActive }) =>
clsx(
'sidebar-link',
isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'
)
}
>
<item.icon className="w-5 h-5 mr-3" />
{item.name}
</NavLink>
))}
</nav>
{/* System Status Indicator */}
<div className="flex-shrink-0 p-4 border-t border-gray-200">
<div className="flex items-center space-x-3 text-sm">
<div className="flex items-center space-x-2">
<div className="status-dot status-online"></div>
<span className="text-gray-600">System Online</span>
</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Uptime: 2d 14h 32m
</div>
</div>
</div>
);
};
export default Sidebar;

View File

@@ -0,0 +1,114 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { apiClient } from '../services/api';
interface User {
username: string;
permissions: string[];
lastLogin?: string;
}
interface AuthContextType {
user: User | null;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing auth token on app load
const token = localStorage.getItem('authToken');
if (token) {
// Verify token with backend
verifyToken(token);
} else {
setLoading(false);
}
}, []);
const verifyToken = async (token: string) => {
try {
apiClient.setAuthToken(token);
// Make a request to verify the token
const response = await apiClient.get('/api/dashboard/metrics');
if (response.status === 200) {
// Token is valid, set user from token payload
const payload = JSON.parse(atob(token.split('.')[1]));
setUser({
username: payload.sub,
permissions: payload.permissions || [],
lastLogin: new Date().toISOString()
});
}
} catch (error) {
// Token is invalid, remove it
localStorage.removeItem('authToken');
apiClient.clearAuthToken();
} finally {
setLoading(false);
}
};
const login = async (username: string, password: string) => {
try {
const response = await apiClient.post('/api/auth/login', {
username,
password
});
const { access_token } = response.data;
// Store token
localStorage.setItem('authToken', access_token);
apiClient.setAuthToken(access_token);
// Decode token to get user info
const payload = JSON.parse(atob(access_token.split('.')[1]));
setUser({
username: payload.sub,
permissions: payload.permissions || [],
lastLogin: new Date().toISOString()
});
} catch (error: any) {
throw new Error(error.response?.data?.detail || 'Login failed');
}
};
const logout = async () => {
try {
await apiClient.post('/api/auth/logout');
} catch (error) {
// Ignore logout errors
} finally {
localStorage.removeItem('authToken');
apiClient.clearAuthToken();
setUser(null);
}
};
const value = {
user,
login,
logout,
loading
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

View File

@@ -0,0 +1,140 @@
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { io, Socket } from 'socket.io-client';
import toast from 'react-hot-toast';
interface WebSocketContextType {
socket: Socket | null;
connected: boolean;
activityFeed: ActivityEvent[];
lastMetrics: DashboardMetrics | null;
}
interface ActivityEvent {
id: string;
type: string;
timestamp: string;
character_name?: string;
description: string;
metadata?: any;
severity: string;
}
interface DashboardMetrics {
total_messages_today: number;
active_conversations: number;
characters_online: number;
characters_total: number;
average_response_time: number;
system_uptime: string;
memory_usage: any;
database_health: string;
llm_api_calls_today: number;
llm_api_cost_today: number;
last_updated: string;
}
const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined);
export const useWebSocket = () => {
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error('useWebSocket must be used within a WebSocketProvider');
}
return context;
};
interface WebSocketProviderProps {
children: ReactNode;
}
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
const [activityFeed, setActivityFeed] = useState<ActivityEvent[]>([]);
const [lastMetrics, setLastMetrics] = useState<DashboardMetrics | null>(null);
useEffect(() => {
// Initialize WebSocket connection
const newSocket = io('/ws', {
transports: ['websocket'],
upgrade: true
});
newSocket.on('connect', () => {
setConnected(true);
console.log('WebSocket connected');
});
newSocket.on('disconnect', () => {
setConnected(false);
console.log('WebSocket disconnected');
});
newSocket.on('activity_update', (data: ActivityEvent) => {
setActivityFeed(prev => [data, ...prev.slice(0, 99)]); // Keep last 100 activities
// Show notification for important activities
if (data.severity === 'warning' || data.severity === 'error') {
toast(data.description, {
icon: data.severity === 'error' ? '🚨' : '⚠️',
duration: 6000
});
}
});
newSocket.on('metrics_update', (data: DashboardMetrics) => {
setLastMetrics(data);
});
newSocket.on('character_update', (data: any) => {
toast(`${data.character_name}: ${data.data.status}`, {
icon: '👤',
duration: 3000
});
});
newSocket.on('conversation_update', (data: any) => {
// Handle conversation updates
console.log('Conversation update:', data);
});
newSocket.on('system_alert', (data: any) => {
toast.error(`System Alert: ${data.alert_type}`, {
duration: 8000
});
});
newSocket.on('system_paused', () => {
toast('System has been paused', {
icon: '⏸️',
duration: 5000
});
});
newSocket.on('system_resumed', () => {
toast('System has been resumed', {
icon: '▶️',
duration: 5000
});
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, []);
const value = {
socket,
connected,
activityFeed,
lastMetrics
};
return (
<WebSocketContext.Provider value={value}>
{children}
</WebSocketContext.Provider>
);
};

View File

@@ -0,0 +1,67 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
}
body {
@apply bg-gray-50 text-gray-900;
}
}
@layer components {
.btn-primary {
@apply bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-colors;
}
.btn-secondary {
@apply bg-secondary-100 hover:bg-secondary-200 text-secondary-700 px-4 py-2 rounded-lg font-medium transition-colors;
}
.card {
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6;
}
.metric-card {
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow;
}
.status-dot {
@apply inline-block w-2 h-2 rounded-full;
}
.status-online {
@apply bg-green-500;
}
.status-idle {
@apply bg-yellow-500;
}
.status-offline {
@apply bg-gray-400;
}
.status-paused {
@apply bg-red-500;
}
.activity-item {
@apply flex items-start space-x-3 p-3 hover:bg-gray-50 rounded-lg transition-colors;
}
.sidebar-link {
@apply flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors;
}
.sidebar-link-active {
@apply bg-primary-100 text-primary-700;
}
.sidebar-link-inactive {
@apply text-gray-700 hover:bg-gray-100;
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import { AuthProvider } from './contexts/AuthContext';
import { WebSocketProvider } from './contexts/WebSocketContext';
import { Toaster } from 'react-hot-toast';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter basename="/admin">
<AuthProvider>
<WebSocketProvider>
<App />
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
}}
/>
</WebSocketProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { BarChart3 } from 'lucide-react';
const Analytics: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Analytics</h1>
<p className="text-gray-600">Community insights and trends</p>
</div>
<div className="card text-center py-12">
<BarChart3 className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Analytics Dashboard</h3>
<p className="text-gray-600">This page will show detailed analytics and trends</p>
</div>
</div>
);
};
export default Analytics;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { useParams } from 'react-router-dom';
const CharacterDetail: React.FC = () => {
const { characterName } = useParams<{ characterName: string }>();
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Character: {characterName}</h1>
<div className="card">
<p className="text-gray-600">Character detail page - to be implemented</p>
</div>
</div>
);
};
export default CharacterDetail;

View File

@@ -0,0 +1,190 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Users, Search, Pause, Play, Settings } from 'lucide-react';
import { apiClient } from '../services/api';
import LoadingSpinner from '../components/Common/LoadingSpinner';
interface Character {
name: string;
status: string;
total_messages: number;
total_conversations: number;
memory_count: number;
relationship_count: number;
creativity_score: number;
social_score: number;
last_active?: string;
}
const Characters: React.FC = () => {
const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
loadCharacters();
}, []);
const loadCharacters = async () => {
try {
const response = await apiClient.getCharacters();
setCharacters(response.data);
} catch (error) {
console.error('Failed to load characters:', error);
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'status-online';
case 'idle': return 'status-idle';
case 'paused': return 'status-paused';
default: return 'status-offline';
}
};
const filteredCharacters = characters.filter(character =>
character.name.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<LoadingSpinner size="lg" text="Loading characters..." />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Characters</h1>
<p className="text-gray-600">Manage and monitor AI character profiles</p>
</div>
<button className="btn-primary">
<Users className="w-4 h-4 mr-2" />
Add Character
</button>
</div>
{/* Search */}
<div className="relative max-w-md">
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
<Search className="w-5 h-5 text-gray-400" />
</div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500"
placeholder="Search characters..."
/>
</div>
{/* Characters Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredCharacters.map((character) => (
<div key={character.name} className="card hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-purple-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">
{character.name.charAt(0).toUpperCase()}
</span>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">{character.name}</h3>
<div className="flex items-center space-x-2">
<div className={`status-dot ${getStatusColor(character.status)}`}></div>
<span className="text-sm text-gray-600 capitalize">{character.status}</span>
</div>
</div>
</div>
<div className="flex space-x-1">
<button className="p-1 text-gray-400 hover:text-gray-600">
{character.status === 'paused' ? (
<Play className="w-4 h-4" />
) : (
<Pause className="w-4 h-4" />
)}
</button>
<button className="p-1 text-gray-400 hover:text-gray-600">
<Settings className="w-4 h-4" />
</button>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-sm text-gray-600">Messages</p>
<p className="text-lg font-semibold text-gray-900">{character.total_messages}</p>
</div>
<div>
<p className="text-sm text-gray-600">Conversations</p>
<p className="text-lg font-semibold text-gray-900">{character.total_conversations}</p>
</div>
<div>
<p className="text-sm text-gray-600">Memories</p>
<p className="text-lg font-semibold text-gray-900">{character.memory_count}</p>
</div>
<div>
<p className="text-sm text-gray-600">Relationships</p>
<p className="text-lg font-semibold text-gray-900">{character.relationship_count}</p>
</div>
</div>
{/* Scores */}
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Creativity</span>
<span className="font-medium">{Math.round(character.creativity_score * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-purple-500 h-2 rounded-full"
style={{ width: `${character.creativity_score * 100}%` }}
></div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Social</span>
<span className="font-medium">{Math.round(character.social_score * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
style={{ width: `${character.social_score * 100}%` }}
></div>
</div>
</div>
{/* Action */}
<Link
to={`/characters/${character.name}`}
className="block w-full text-center btn-secondary"
>
View Details
</Link>
</div>
))}
</div>
{filteredCharacters.length === 0 && (
<div className="text-center py-12">
<Users className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No characters found</h3>
<p className="text-gray-600">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by adding your first character.'}
</p>
</div>
)}
</div>
);
};
export default Characters;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { useParams } from 'react-router-dom';
const ConversationDetail: React.FC = () => {
const { conversationId } = useParams<{ conversationId: string }>();
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Conversation #{conversationId}</h1>
<div className="card">
<p className="text-gray-600">Conversation detail page - to be implemented</p>
</div>
</div>
);
};
export default ConversationDetail;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { MessageSquare } from 'lucide-react';
const Conversations: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Conversations</h1>
<p className="text-gray-600">Browse and analyze character conversations</p>
</div>
<div className="card text-center py-12">
<MessageSquare className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Conversations Browser</h3>
<p className="text-gray-600">This page will show conversation history and analytics</p>
</div>
</div>
);
};
export default Conversations;

View File

@@ -0,0 +1,258 @@
import React, { useState, useEffect } from 'react';
import {
Users,
MessageSquare,
Activity,
Clock,
TrendingUp,
AlertCircle,
CheckCircle,
Zap
} from 'lucide-react';
import { useWebSocket } from '../contexts/WebSocketContext';
import { apiClient } from '../services/api';
import LoadingSpinner from '../components/Common/LoadingSpinner';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
interface DashboardMetrics {
total_messages_today: number;
active_conversations: number;
characters_online: number;
characters_total: number;
average_response_time: number;
system_uptime: string;
memory_usage: any;
database_health: string;
llm_api_calls_today: number;
llm_api_cost_today: number;
last_updated: string;
}
const Dashboard: React.FC = () => {
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [systemHealth, setSystemHealth] = useState<any>(null);
const { activityFeed, lastMetrics } = useWebSocket();
useEffect(() => {
loadDashboardData();
}, []);
useEffect(() => {
// Update metrics from WebSocket
if (lastMetrics) {
setMetrics(lastMetrics);
}
}, [lastMetrics]);
const loadDashboardData = async () => {
try {
const [metricsResponse, healthResponse] = await Promise.all([
apiClient.getDashboardMetrics(),
apiClient.getSystemHealth()
]);
setMetrics(metricsResponse.data);
setSystemHealth(healthResponse.data);
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<LoadingSpinner size="lg" text="Loading dashboard..." />
</div>
);
}
// Sample data for activity chart
const activityData = [
{ time: '00:00', messages: 12, conversations: 3 },
{ time: '04:00', messages: 8, conversations: 2 },
{ time: '08:00', messages: 24, conversations: 6 },
{ time: '12:00', messages: 35, conversations: 8 },
{ time: '16:00', messages: 28, conversations: 7 },
{ time: '20:00', messages: 31, conversations: 9 },
];
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Monitor your autonomous character ecosystem</p>
</div>
{/* Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="metric-card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Messages Today</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.total_messages_today || 0}</p>
</div>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<MessageSquare className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
<span className="text-green-600">+12%</span>
<span className="text-gray-500 ml-2">from yesterday</span>
</div>
</div>
<div className="metric-card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Active Characters</p>
<p className="text-2xl font-bold text-gray-900">
{metrics?.characters_online || 0}/{metrics?.characters_total || 0}
</p>
</div>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-green-600" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<div className="status-dot status-online mr-2"></div>
<span className="text-gray-600">All systems operational</span>
</div>
</div>
<div className="metric-card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Active Conversations</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.active_conversations || 0}</p>
</div>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<Activity className="w-5 h-5 text-purple-600" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-gray-600">Avg. length: 8.2 messages</span>
</div>
</div>
<div className="metric-card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Response Time</p>
<p className="text-2xl font-bold text-gray-900">{metrics?.average_response_time || 0}s</p>
</div>
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<Zap className="w-5 h-5 text-orange-600" />
</div>
</div>
<div className="mt-2 flex items-center text-sm">
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
<span className="text-green-600">Excellent</span>
</div>
</div>
</div>
{/* Charts and Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Activity Chart */}
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Activity Over Time</h3>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={activityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="messages"
stroke="#3B82F6"
strokeWidth={2}
name="Messages"
/>
<Line
type="monotone"
dataKey="conversations"
stroke="#10B981"
strokeWidth={2}
name="Conversations"
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* System Health */}
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4">System Health</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Database</span>
<div className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">Healthy</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Memory Usage</span>
<span className="text-sm text-gray-900">
{systemHealth?.memory?.percent?.toFixed(1) || 0}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">CPU Usage</span>
<span className="text-sm text-gray-900">
{systemHealth?.cpu?.usage_percent?.toFixed(1) || 0}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Uptime</span>
<span className="text-sm text-gray-900">{metrics?.system_uptime || 'Unknown'}</span>
</div>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
<div className="space-y-3 max-h-64 overflow-y-auto">
{activityFeed.slice(0, 10).map((activity) => (
<div key={activity.id} className="activity-item">
<div className="flex-shrink-0">
{activity.severity === 'error' ? (
<AlertCircle className="w-5 h-5 text-red-500" />
) : activity.severity === 'warning' ? (
<AlertCircle className="w-5 h-5 text-yellow-500" />
) : (
<Activity className="w-5 h-5 text-blue-500" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">{activity.description}</p>
<div className="flex items-center mt-1 text-xs text-gray-500">
{activity.character_name && (
<span className="mr-2">{activity.character_name}</span>
)}
<Clock className="w-3 h-3 mr-1" />
<span>{new Date(activity.timestamp).toLocaleTimeString()}</span>
</div>
</div>
</div>
))}
{activityFeed.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No recent activity</p>
</div>
)}
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { Monitor, Users, MessageSquare } from 'lucide-react';
import LoadingSpinner from '../components/Common/LoadingSpinner';
import toast from 'react-hot-toast';
const LoginPage: React.FC = () => {
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!username || !password) {
toast.error('Please enter both username and password');
return;
}
setLoading(true);
try {
await login(username, password);
toast.success('Login successful!');
} catch (error: any) {
toast.error(error.message || 'Login failed');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-100 flex items-center justify-center px-4">
<div className="max-w-md w-full">
{/* Logo and Title */}
<div className="text-center mb-8">
<div className="mx-auto w-16 h-16 bg-primary-600 rounded-xl flex items-center justify-center mb-4">
<Monitor className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Discord Fishbowl</h1>
<p className="text-gray-600">Admin Interface</p>
</div>
{/* Features Preview */}
<div className="mb-8 grid grid-cols-3 gap-4">
<div className="text-center">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<Users className="w-5 h-5 text-blue-600" />
</div>
<p className="text-xs text-gray-600">Character Management</p>
</div>
<div className="text-center">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<MessageSquare className="w-5 h-5 text-green-600" />
</div>
<p className="text-xs text-gray-600">Live Conversations</p>
</div>
<div className="text-center">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<Monitor className="w-5 h-5 text-purple-600" />
</div>
<p className="text-xs text-gray-600">Real-time Analytics</p>
</div>
</div>
{/* Login Form */}
<div className="bg-white rounded-xl shadow-lg p-8">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Enter your username"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Enter your password"
disabled={loading}
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{loading ? (
<LoadingSpinner size="sm" />
) : (
'Sign In'
)}
</button>
</form>
{/* Demo credentials hint */}
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 text-center">
<strong>Demo credentials:</strong><br />
Username: admin<br />
Password: admin123
</p>
</div>
</div>
{/* Footer */}
<div className="text-center mt-8 text-sm text-gray-500">
Monitor and manage your autonomous AI character ecosystem
</div>
</div>
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { Settings as SettingsIcon } from 'lucide-react';
const Settings: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<p className="text-gray-600">Configure system settings and preferences</p>
</div>
<div className="card text-center py-12">
<SettingsIcon className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">System Settings</h3>
<p className="text-gray-600">This page will show configuration options</p>
</div>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { Monitor } from 'lucide-react';
const SystemStatus: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">System Status</h1>
<p className="text-gray-600">Monitor system health and performance</p>
</div>
<div className="card text-center py-12">
<Monitor className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">System Monitor</h3>
<p className="text-gray-600">This page will show system status and controls</p>
</div>
</div>
);
};
export default SystemStatus;

View File

@@ -0,0 +1,200 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
class ApiClient {
private client: AxiosInstance;
private authToken: string | null = null;
constructor() {
this.client = axios.create({
baseURL: process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor to add auth token
this.client.interceptors.request.use(
(config) => {
if (this.authToken && config.headers) {
config.headers.Authorization = `Bearer ${this.authToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle auth errors
this.client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
this.clearAuthToken();
window.location.href = '/admin/login';
}
return Promise.reject(error);
}
);
}
setAuthToken(token: string) {
this.authToken = token;
}
clearAuthToken() {
this.authToken = null;
localStorage.removeItem('authToken');
}
async get(url: string, config?: AxiosRequestConfig) {
return this.client.get(url, config);
}
async post(url: string, data?: any, config?: AxiosRequestConfig) {
return this.client.post(url, data, config);
}
async put(url: string, data?: any, config?: AxiosRequestConfig) {
return this.client.put(url, data, config);
}
async delete(url: string, config?: AxiosRequestConfig) {
return this.client.delete(url, config);
}
// Dashboard endpoints
async getDashboardMetrics() {
return this.get('/dashboard/metrics');
}
async getRecentActivity(limit = 50) {
return this.get(`/dashboard/activity?limit=${limit}`);
}
async getSystemHealth() {
return this.get('/dashboard/health');
}
// Character endpoints
async getCharacters() {
return this.get('/characters');
}
async getCharacter(characterName: string) {
return this.get(`/characters/${characterName}`);
}
async getCharacterRelationships(characterName: string) {
return this.get(`/characters/${characterName}/relationships`);
}
async getCharacterEvolution(characterName: string, days = 30) {
return this.get(`/characters/${characterName}/evolution?days=${days}`);
}
async getCharacterMemories(characterName: string, limit = 100, memoryType?: string) {
const params = new URLSearchParams({ limit: limit.toString() });
if (memoryType) params.append('memory_type', memoryType);
return this.get(`/characters/${characterName}/memories?${params}`);
}
async pauseCharacter(characterName: string) {
return this.post(`/characters/${characterName}/pause`);
}
async resumeCharacter(characterName: string) {
return this.post(`/characters/${characterName}/resume`);
}
// Conversation endpoints
async getConversations(filters: any = {}) {
const params = new URLSearchParams();
Object.keys(filters).forEach(key => {
if (filters[key] !== undefined && filters[key] !== '') {
params.append(key, filters[key].toString());
}
});
return this.get(`/conversations?${params}`);
}
async getConversation(conversationId: number) {
return this.get(`/conversations/${conversationId}`);
}
async searchConversations(query: string, limit = 50) {
return this.get(`/conversations/search?query=${encodeURIComponent(query)}&limit=${limit}`);
}
// Analytics endpoints
async getTopicTrends(days = 30) {
return this.get(`/analytics/topics?days=${days}`);
}
async getRelationshipAnalytics() {
return this.get('/analytics/relationships');
}
async getCommunityHealth() {
return this.get('/analytics/community');
}
async getEngagementMetrics(days = 30) {
return this.get(`/analytics/engagement?days=${days}`);
}
// System endpoints
async getSystemStatus() {
return this.get('/system/status');
}
async pauseSystem() {
return this.post('/system/pause');
}
async resumeSystem() {
return this.post('/system/resume');
}
async getSystemConfig() {
return this.get('/system/config');
}
async updateSystemConfig(config: any) {
return this.put('/system/config', config);
}
async getSystemLogs(limit = 100, level?: string) {
const params = new URLSearchParams({ limit: limit.toString() });
if (level) params.append('level', level);
return this.get(`/system/logs?${params}`);
}
// Content endpoints
async getCreativeWorks(filters: any = {}) {
const params = new URLSearchParams();
Object.keys(filters).forEach(key => {
if (filters[key] !== undefined && filters[key] !== '') {
params.append(key, filters[key].toString());
}
});
return this.get(`/content/creative-works?${params}`);
}
async getCommunityArtifacts() {
return this.get('/content/community-artifacts');
}
// Export endpoints
async exportConversation(conversationId: number, format = 'json') {
return this.get(`/export/conversation/${conversationId}?format=${format}`);
}
async exportCharacterData(characterName: string) {
return this.get(`/export/character/${characterName}`);
}
}
export const apiClient = new ApiClient();

View File

@@ -0,0 +1,43 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
secondary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
}
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'bounce-gentle': 'bounce 2s infinite',
}
},
},
plugins: [
require('@tailwindcss/forms'),
],
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@@ -27,3 +27,12 @@ watchdog==3.0.0
# Enhanced NLP
spacy==3.7.2
nltk==3.8.1
# Admin Interface
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
websockets==12.0
psutil==5.9.6

130
scripts/start_admin.py Executable file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
Start the Discord Fishbowl Admin Interface
Launches both the FastAPI backend and React frontend
"""
import asyncio
import subprocess
import sys
import os
import signal
import time
from pathlib import Path
def run_command(cmd, cwd=None, env=None):
"""Run a command and return the process"""
print(f"Starting: {' '.join(cmd)}")
return subprocess.Popen(
cmd,
cwd=cwd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True
)
def main():
"""Main function to start the admin interface"""
project_root = Path(__file__).parent.parent
admin_frontend_path = project_root / "admin-frontend"
processes = []
try:
print("🚀 Starting Discord Fishbowl Admin Interface...")
print("=" * 50)
# Start FastAPI backend
print("\n📡 Starting FastAPI backend server...")
backend_env = os.environ.copy()
backend_env["PYTHONPATH"] = str(project_root)
backend_process = run_command(
[sys.executable, "-m", "uvicorn", "src.admin.app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"],
cwd=project_root,
env=backend_env
)
processes.append(("Backend", backend_process))
# Wait a bit for backend to start
time.sleep(3)
# Check if npm is available and admin-frontend exists
if not admin_frontend_path.exists():
print("❌ Admin frontend directory not found!")
print("Please ensure admin-frontend directory exists")
return 1
# Check if node_modules exists, if not run npm install
node_modules_path = admin_frontend_path / "node_modules"
if not node_modules_path.exists():
print("\n📦 Installing frontend dependencies...")
npm_install = run_command(
["npm", "install"],
cwd=admin_frontend_path
)
npm_install.wait()
if npm_install.returncode != 0:
print("❌ Failed to install npm dependencies")
return 1
# Start React frontend
print("\n🌐 Starting React frontend server...")
frontend_process = run_command(
["npm", "start"],
cwd=admin_frontend_path
)
processes.append(("Frontend", frontend_process))
print("\n✅ Admin interface is starting up!")
print("📡 Backend API: http://localhost:8000")
print("🌐 Frontend UI: http://localhost:3000/admin")
print("\n🔑 Default login credentials:")
print(" Username: admin")
print(" Password: admin123")
print("\n🛑 Press Ctrl+C to stop all services")
print("=" * 50)
# Monitor processes
while True:
time.sleep(1)
# Check if any process has died
for name, process in processes:
if process.poll() is not None:
print(f"\n{name} process has stopped unexpectedly!")
return 1
except KeyboardInterrupt:
print("\n🛑 Shutting down admin interface...")
# Terminate all processes
for name, process in processes:
print(f" Stopping {name}...")
try:
process.terminate()
process.wait(timeout=5)
except subprocess.TimeoutExpired:
print(f" Force killing {name}...")
process.kill()
process.wait()
print("✅ Admin interface stopped successfully")
return 0
except Exception as e:
print(f"\n❌ Error starting admin interface: {e}")
# Clean up processes
for name, process in processes:
try:
process.terminate()
process.wait(timeout=2)
except:
process.kill()
return 1
if __name__ == "__main__":
sys.exit(main())

1
src/admin/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Admin interface package

365
src/admin/app.py Normal file
View File

@@ -0,0 +1,365 @@
#!/usr/bin/env python3
"""
Discord Fishbowl Admin Interface
FastAPI backend for monitoring and managing the autonomous character ecosystem
"""
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException, Depends, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import uvicorn
from ..database.connection import init_database, get_db_session
from ..database.models import Character, Conversation, Message, Memory, CharacterRelationship
from ..utils.config import get_settings
from ..utils.logging import setup_logging
from .models import (
AdminUser, DashboardMetrics, CharacterProfile, ConversationSummary,
SystemStatus, AnalyticsData
)
from .services import (
DashboardService, CharacterService, ConversationService,
SystemService, AnalyticsService, WebSocketManager
)
from .auth import AuthService
logger = logging.getLogger(__name__)
# WebSocket manager for real-time updates
websocket_manager = WebSocketManager()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan management"""
logger.info("Starting Admin Interface...")
# Initialize database
await init_database()
# Initialize services
await DashboardService.initialize()
await CharacterService.initialize()
await ConversationService.initialize()
await SystemService.initialize()
await AnalyticsService.initialize()
logger.info("Admin Interface started successfully")
yield
logger.info("Shutting down Admin Interface...")
# Create FastAPI app
app = FastAPI(
title="Discord Fishbowl Admin Interface",
description="Monitor and manage the autonomous character ecosystem",
version="1.0.0",
lifespan=lifespan
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"], # React dev server
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Authentication
security = HTTPBearer()
auth_service = AuthService()
async def get_current_admin(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AdminUser:
"""Get current authenticated admin user"""
return await auth_service.verify_token(credentials.credentials)
# Initialize services
dashboard_service = DashboardService(websocket_manager)
character_service = CharacterService()
conversation_service = ConversationService()
system_service = SystemService()
analytics_service = AnalyticsService()
# WebSocket endpoint for real-time updates
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket connection for real-time updates"""
await websocket_manager.connect(websocket)
try:
while True:
# Keep connection alive and handle incoming messages
data = await websocket.receive_text()
# Echo or handle client messages if needed
await websocket.send_text(f"Echo: {data}")
except WebSocketDisconnect:
websocket_manager.disconnect(websocket)
# Authentication endpoints
@app.post("/api/auth/login")
async def login(username: str, password: str):
"""Admin login"""
try:
token = await auth_service.authenticate(username, password)
return {"access_token": token, "token_type": "bearer"}
except Exception as e:
raise HTTPException(status_code=401, detail="Invalid credentials")
@app.post("/api/auth/logout")
async def logout(admin: AdminUser = Depends(get_current_admin)):
"""Admin logout"""
await auth_service.logout(admin.username)
return {"message": "Logged out successfully"}
# Dashboard endpoints
@app.get("/api/dashboard/metrics", response_model=DashboardMetrics)
async def get_dashboard_metrics(admin: AdminUser = Depends(get_current_admin)):
"""Get real-time dashboard metrics"""
return await dashboard_service.get_metrics()
@app.get("/api/dashboard/activity")
async def get_recent_activity(
limit: int = 50,
admin: AdminUser = Depends(get_current_admin)
):
"""Get recent activity feed"""
return await dashboard_service.get_recent_activity(limit)
@app.get("/api/dashboard/health")
async def get_system_health(admin: AdminUser = Depends(get_current_admin)):
"""Get system health status"""
return await dashboard_service.get_system_health()
# Character management endpoints
@app.get("/api/characters", response_model=List[CharacterProfile])
async def get_characters(admin: AdminUser = Depends(get_current_admin)):
"""Get all characters with profiles"""
return await character_service.get_all_characters()
@app.get("/api/characters/{character_name}", response_model=CharacterProfile)
async def get_character(
character_name: str,
admin: AdminUser = Depends(get_current_admin)
):
"""Get detailed character profile"""
character = await character_service.get_character_profile(character_name)
if not character:
raise HTTPException(status_code=404, detail="Character not found")
return character
@app.get("/api/characters/{character_name}/relationships")
async def get_character_relationships(
character_name: str,
admin: AdminUser = Depends(get_current_admin)
):
"""Get character relationship network"""
return await character_service.get_character_relationships(character_name)
@app.get("/api/characters/{character_name}/evolution")
async def get_character_evolution(
character_name: str,
days: int = 30,
admin: AdminUser = Depends(get_current_admin)
):
"""Get character personality evolution timeline"""
return await character_service.get_personality_evolution(character_name, days)
@app.get("/api/characters/{character_name}/memories")
async def get_character_memories(
character_name: str,
limit: int = 100,
memory_type: Optional[str] = None,
admin: AdminUser = Depends(get_current_admin)
):
"""Get character memories"""
return await character_service.get_character_memories(character_name, limit, memory_type)
@app.post("/api/characters/{character_name}/pause")
async def pause_character(
character_name: str,
admin: AdminUser = Depends(get_current_admin)
):
"""Pause character activities"""
await character_service.pause_character(character_name)
return {"message": f"Character {character_name} paused"}
@app.post("/api/characters/{character_name}/resume")
async def resume_character(
character_name: str,
admin: AdminUser = Depends(get_current_admin)
):
"""Resume character activities"""
await character_service.resume_character(character_name)
return {"message": f"Character {character_name} resumed"}
# Conversation endpoints
@app.get("/api/conversations")
async def get_conversations(
limit: int = 50,
character_name: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
admin: AdminUser = Depends(get_current_admin)
):
"""Get conversation history with filters"""
return await conversation_service.get_conversations(
limit=limit,
character_name=character_name,
start_date=start_date,
end_date=end_date
)
@app.get("/api/conversations/{conversation_id}")
async def get_conversation(
conversation_id: int,
admin: AdminUser = Depends(get_current_admin)
):
"""Get detailed conversation"""
conversation = await conversation_service.get_conversation_details(conversation_id)
if not conversation:
raise HTTPException(status_code=404, detail="Conversation not found")
return conversation
@app.get("/api/conversations/search")
async def search_conversations(
query: str,
limit: int = 50,
admin: AdminUser = Depends(get_current_admin)
):
"""Search conversations by content"""
return await conversation_service.search_conversations(query, limit)
# Analytics endpoints
@app.get("/api/analytics/topics")
async def get_topic_trends(
days: int = 30,
admin: AdminUser = Depends(get_current_admin)
):
"""Get topic trend analysis"""
return await analytics_service.get_topic_trends(days)
@app.get("/api/analytics/relationships")
async def get_relationship_analytics(
admin: AdminUser = Depends(get_current_admin)
):
"""Get relationship strength analytics"""
return await analytics_service.get_relationship_analytics()
@app.get("/api/analytics/community")
async def get_community_health(
admin: AdminUser = Depends(get_current_admin)
):
"""Get community health metrics"""
return await analytics_service.get_community_health()
@app.get("/api/analytics/engagement")
async def get_engagement_metrics(
days: int = 30,
admin: AdminUser = Depends(get_current_admin)
):
"""Get conversation engagement metrics"""
return await analytics_service.get_engagement_metrics(days)
# System control endpoints
@app.get("/api/system/status", response_model=SystemStatus)
async def get_system_status(admin: AdminUser = Depends(get_current_admin)):
"""Get system status"""
return await system_service.get_status()
@app.post("/api/system/pause")
async def pause_system(admin: AdminUser = Depends(get_current_admin)):
"""Pause entire system"""
await system_service.pause_system()
await websocket_manager.broadcast({"type": "system_paused"})
return {"message": "System paused"}
@app.post("/api/system/resume")
async def resume_system(admin: AdminUser = Depends(get_current_admin)):
"""Resume system operations"""
await system_service.resume_system()
await websocket_manager.broadcast({"type": "system_resumed"})
return {"message": "System resumed"}
@app.get("/api/system/config")
async def get_system_config(admin: AdminUser = Depends(get_current_admin)):
"""Get system configuration"""
return await system_service.get_configuration()
@app.put("/api/system/config")
async def update_system_config(
config: Dict[str, Any],
admin: AdminUser = Depends(get_current_admin)
):
"""Update system configuration"""
await system_service.update_configuration(config)
return {"message": "Configuration updated"}
@app.get("/api/system/logs")
async def get_system_logs(
limit: int = 100,
level: Optional[str] = None,
admin: AdminUser = Depends(get_current_admin)
):
"""Get system logs"""
return await system_service.get_logs(limit, level)
# Content and creative works endpoints
@app.get("/api/content/creative-works")
async def get_creative_works(
character_name: Optional[str] = None,
work_type: Optional[str] = None,
limit: int = 50,
admin: AdminUser = Depends(get_current_admin)
):
"""Get creative works from characters"""
return await character_service.get_creative_works(character_name, work_type, limit)
@app.get("/api/content/community-artifacts")
async def get_community_artifacts(
admin: AdminUser = Depends(get_current_admin)
):
"""Get community cultural artifacts"""
return await analytics_service.get_community_artifacts()
# Export endpoints
@app.get("/api/export/conversation/{conversation_id}")
async def export_conversation(
conversation_id: int,
format: str = "json",
admin: AdminUser = Depends(get_current_admin)
):
"""Export conversation in specified format"""
return await conversation_service.export_conversation(conversation_id, format)
@app.get("/api/export/character/{character_name}")
async def export_character_data(
character_name: str,
admin: AdminUser = Depends(get_current_admin)
):
"""Export complete character data"""
return await character_service.export_character_data(character_name)
# Serve React frontend
@app.mount("/admin", StaticFiles(directory="admin-frontend/build", html=True), name="admin")
@app.get("/")
async def root():
"""Root endpoint redirects to admin interface"""
return {"message": "Discord Fishbowl Admin Interface", "admin_url": "/admin"}
if __name__ == "__main__":
uvicorn.run(
"src.admin.app:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)

201
src/admin/auth.py Normal file
View File

@@ -0,0 +1,201 @@
"""
Authentication service for admin interface
"""
import jwt
import hashlib
import secrets
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
import logging
from fastapi import HTTPException
from ..utils.config import get_settings
from .models import AdminUser
logger = logging.getLogger(__name__)
class AuthService:
"""Authentication service for admin users"""
def __init__(self):
self.settings = get_settings()
self.secret_key = self.settings.admin.secret_key if hasattr(self.settings, 'admin') else "fallback-secret-key"
self.algorithm = "HS256"
self.access_token_expire_minutes = 480 # 8 hours
# Simple in-memory user storage (replace with database in production)
self.users = {
"admin": {
"username": "admin",
"password_hash": self._hash_password("admin123"), # Default password
"permissions": ["read", "write", "admin"],
"active": True
}
}
# Active sessions
self.active_sessions: Dict[str, Dict[str, Any]] = {}
def _hash_password(self, password: str) -> str:
"""Hash password with salt"""
salt = secrets.token_hex(16)
password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
return f"{salt}${password_hash.hex()}"
def _verify_password(self, password: str, password_hash: str) -> bool:
"""Verify password against hash"""
try:
salt, hash_value = password_hash.split('$')
computed_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
return computed_hash.hex() == hash_value
except:
return False
def _create_access_token(self, data: Dict[str, Any]) -> str:
"""Create JWT access token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
def _decode_token(self, token: str) -> Dict[str, Any]:
"""Decode JWT token"""
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token has expired")
except jwt.JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
async def authenticate(self, username: str, password: str) -> str:
"""Authenticate user and return access token"""
try:
user = self.users.get(username)
if not user or not user["active"]:
raise HTTPException(status_code=401, detail="Invalid credentials")
if not self._verify_password(password, user["password_hash"]):
raise HTTPException(status_code=401, detail="Invalid credentials")
# Create access token
token_data = {
"sub": username,
"permissions": user["permissions"],
"iat": datetime.utcnow().timestamp()
}
access_token = self._create_access_token(token_data)
# Store session
self.active_sessions[username] = {
"token": access_token,
"login_time": datetime.utcnow(),
"last_activity": datetime.utcnow()
}
logger.info(f"Admin user {username} logged in successfully")
return access_token
except HTTPException:
raise
except Exception as e:
logger.error(f"Authentication error for user {username}: {e}")
raise HTTPException(status_code=500, detail="Authentication service error")
async def verify_token(self, token: str) -> AdminUser:
"""Verify token and return admin user"""
try:
payload = self._decode_token(token)
username = payload.get("sub")
if not username or username not in self.users:
raise HTTPException(status_code=401, detail="Invalid token")
user = self.users[username]
if not user["active"]:
raise HTTPException(status_code=401, detail="User account disabled")
# Update last activity
if username in self.active_sessions:
self.active_sessions[username]["last_activity"] = datetime.utcnow()
return AdminUser(
username=username,
permissions=user["permissions"],
last_login=self.active_sessions.get(username, {}).get("login_time")
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Token verification error: {e}")
raise HTTPException(status_code=401, detail="Token verification failed")
async def logout(self, username: str):
"""Logout user and invalidate session"""
try:
if username in self.active_sessions:
del self.active_sessions[username]
logger.info(f"Admin user {username} logged out")
except Exception as e:
logger.error(f"Logout error for user {username}: {e}")
async def create_user(self, username: str, password: str, permissions: list) -> bool:
"""Create new admin user"""
try:
if username in self.users:
return False
self.users[username] = {
"username": username,
"password_hash": self._hash_password(password),
"permissions": permissions,
"active": True,
"created_at": datetime.utcnow()
}
logger.info(f"Created new admin user: {username}")
return True
except Exception as e:
logger.error(f"Error creating user {username}: {e}")
return False
async def change_password(self, username: str, old_password: str, new_password: str) -> bool:
"""Change user password"""
try:
user = self.users.get(username)
if not user:
return False
if not self._verify_password(old_password, user["password_hash"]):
return False
user["password_hash"] = self._hash_password(new_password)
logger.info(f"Password changed for user: {username}")
return True
except Exception as e:
logger.error(f"Error changing password for user {username}: {e}")
return False
async def get_active_sessions(self) -> Dict[str, Dict[str, Any]]:
"""Get active admin sessions"""
# Clean expired sessions
current_time = datetime.utcnow()
expired_sessions = []
for username, session in self.active_sessions.items():
last_activity = session["last_activity"]
if (current_time - last_activity).total_seconds() > (self.access_token_expire_minutes * 60):
expired_sessions.append(username)
for username in expired_sessions:
del self.active_sessions[username]
return self.active_sessions.copy()
def is_authorized(self, user: AdminUser, required_permission: str) -> bool:
"""Check if user has required permission"""
return required_permission in user.permissions or "admin" in user.permissions

290
src/admin/models.py Normal file
View File

@@ -0,0 +1,290 @@
"""
Pydantic models for admin interface API
"""
from typing import List, Dict, Any, Optional, Union
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field
class AdminUser(BaseModel):
username: str
permissions: List[str] = []
last_login: Optional[datetime] = None
class SystemStatusEnum(str, Enum):
RUNNING = "running"
PAUSED = "paused"
ERROR = "error"
STARTING = "starting"
STOPPING = "stopping"
class CharacterStatusEnum(str, Enum):
ACTIVE = "active"
IDLE = "idle"
REFLECTING = "reflecting"
CREATING = "creating"
PAUSED = "paused"
OFFLINE = "offline"
class ActivityType(str, Enum):
MESSAGE = "message"
CONVERSATION_START = "conversation_start"
CONVERSATION_END = "conversation_end"
CHARACTER_JOIN = "character_join"
CHARACTER_LEAVE = "character_leave"
PERSONALITY_CHANGE = "personality_change"
RELATIONSHIP_CHANGE = "relationship_change"
MILESTONE = "milestone"
CREATIVE_WORK = "creative_work"
SYSTEM_EVENT = "system_event"
class DashboardMetrics(BaseModel):
"""Real-time dashboard metrics"""
total_messages_today: int
active_conversations: int
characters_online: int
characters_total: int
average_response_time: float
system_uptime: str
memory_usage: Dict[str, Union[int, float]]
database_health: str
llm_api_calls_today: int
llm_api_cost_today: float
last_updated: datetime
class ActivityEvent(BaseModel):
"""Activity feed event"""
id: str
type: ActivityType
timestamp: datetime
character_name: Optional[str] = None
description: str
metadata: Dict[str, Any] = {}
severity: str = "info" # info, warning, error
class CharacterProfile(BaseModel):
"""Character profile with current state"""
name: str
personality_traits: Dict[str, float]
current_goals: List[str]
speaking_style: Dict[str, Any]
status: CharacterStatusEnum
total_messages: int
total_conversations: int
memory_count: int
relationship_count: int
created_at: datetime
last_active: Optional[datetime] = None
last_modification: Optional[datetime] = None
creativity_score: float
social_score: float
growth_score: float
class PersonalityEvolution(BaseModel):
"""Personality change over time"""
timestamp: datetime
trait_changes: Dict[str, Dict[str, float]] # trait -> {old, new}
reason: str
confidence: float
impact_score: float
class Relationship(BaseModel):
"""Character relationship data"""
character_a: str
character_b: str
strength: float
relationship_type: str
last_interaction: datetime
interaction_count: int
sentiment: float
trust_level: float
compatibility: float
class ConversationSummary(BaseModel):
"""Conversation summary for listing"""
id: int
participants: List[str]
topic: Optional[str] = None
message_count: int
start_time: datetime
end_time: Optional[datetime] = None
duration_minutes: Optional[float] = None
engagement_score: float
sentiment_score: float
has_conflict: bool = False
creative_elements: List[str] = []
class ConversationDetail(BaseModel):
"""Detailed conversation with messages"""
id: int
participants: List[str]
topic: Optional[str] = None
start_time: datetime
end_time: Optional[datetime] = None
duration_minutes: Optional[float] = None
message_count: int
engagement_score: float
sentiment_score: float
messages: List[Dict[str, Any]]
analysis: Dict[str, Any]
keywords: List[str]
emotions: Dict[str, float]
class MemorySummary(BaseModel):
"""Character memory summary"""
id: str
content: str
memory_type: str
importance: float
timestamp: datetime
related_characters: List[str]
emotions: Dict[str, float]
keywords: List[str]
class CreativeWork(BaseModel):
"""Creative work by characters"""
id: str
character_name: str
title: str
content: str
work_type: str # story, poem, philosophy, etc.
created_at: datetime
inspiration_source: Optional[str] = None
collaborators: List[str] = []
community_rating: Optional[float] = None
themes: List[str] = []
class TopicTrend(BaseModel):
"""Topic trend analysis"""
topic: str
mentions: int
growth_rate: float
sentiment: float
participants: List[str]
related_topics: List[str]
first_mentioned: datetime
peak_date: datetime
class RelationshipAnalytics(BaseModel):
"""Relationship strength analytics"""
character_network: Dict[str, List[Relationship]]
strongest_bonds: List[Relationship]
developing_relationships: List[Relationship]
at_risk_relationships: List[Relationship]
relationship_matrix: Dict[str, Dict[str, float]]
social_hierarchy: List[str]
community_cohesion: float
class CommunityHealth(BaseModel):
"""Community health metrics"""
overall_health: float
participation_balance: float
conflict_resolution_success: float
creative_collaboration_rate: float
knowledge_sharing_frequency: float
cultural_coherence: float
growth_trajectory: str
health_trends: Dict[str, List[Dict[str, Any]]]
recommendations: List[str]
class EngagementMetrics(BaseModel):
"""Conversation engagement metrics"""
total_conversations: int
average_length: float
participation_rate: Dict[str, float]
topic_diversity: float
response_quality: float
emotional_depth: float
creative_frequency: float
conflict_frequency: float
daily_trends: List[Dict[str, Any]]
class SystemStatus(BaseModel):
"""System status and health"""
status: SystemStatusEnum
uptime: str
version: str
database_status: str
redis_status: str
llm_service_status: str
discord_bot_status: str
active_processes: List[str]
error_count: int
warnings_count: int
performance_metrics: Dict[str, Any]
resource_usage: Dict[str, Any]
class AnalyticsData(BaseModel):
"""General analytics data container"""
data_type: str
time_period: str
data: Dict[str, Any]
generated_at: datetime
insights: List[str] = []
class SystemConfiguration(BaseModel):
"""System configuration settings"""
conversation_frequency: float
response_delay_min: float
response_delay_max: float
personality_change_rate: float
memory_retention_days: int
max_conversation_length: int
creativity_boost: bool
conflict_resolution_enabled: bool
safety_monitoring: bool
auto_moderation: bool
backup_frequency_hours: int
class LogEntry(BaseModel):
"""System log entry"""
timestamp: datetime
level: str
component: str
message: str
metadata: Dict[str, Any] = {}
character_name: Optional[str] = None
correlation_id: Optional[str] = None
class SearchResult(BaseModel):
"""Search result for conversations/content"""
id: str
type: str # conversation, message, creative_work, etc.
title: str
snippet: str
relevance_score: float
timestamp: datetime
character_names: List[str]
metadata: Dict[str, Any] = {}
class ExportRequest(BaseModel):
"""Export request configuration"""
data_type: str
format: str # json, csv, pdf
date_range: Optional[Dict[str, datetime]] = None
filters: Dict[str, Any] = {}
include_metadata: bool = True
class SafetyAlert(BaseModel):
"""Content safety alert"""
id: str
alert_type: str
severity: str
timestamp: datetime
character_name: Optional[str] = None
content_snippet: str
auto_action_taken: Optional[str] = None
requires_review: bool = True
resolved: bool = False
class InterventionAction(BaseModel):
"""Manual intervention action"""
action_type: str
target: str # character, conversation, system
parameters: Dict[str, Any]
reason: str
admin_user: str
executed_at: Optional[datetime] = None
success: bool = False
error_message: Optional[str] = None

View File

@@ -0,0 +1,16 @@
# Admin services
from .websocket_manager import WebSocketManager
from .dashboard_service import DashboardService
from .character_service import CharacterService
from .conversation_service import ConversationService
from .system_service import SystemService
from .analytics_service import AnalyticsService
__all__ = [
'WebSocketManager',
'DashboardService',
'CharacterService',
'ConversationService',
'SystemService',
'AnalyticsService'
]

View File

@@ -0,0 +1,378 @@
"""
Analytics service for community insights and trends
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from collections import defaultdict, Counter
from sqlalchemy import select, func, and_, or_, desc
from ...database.connection import get_db_session
from ...database.models import Character, Conversation, Message, CharacterRelationship
from ..models import (
TopicTrend, RelationshipAnalytics, CommunityHealth,
EngagementMetrics, Relationship
)
logger = logging.getLogger(__name__)
class AnalyticsService:
"""Service for analytics and community insights"""
def __init__(self):
self.analytics_cache = {}
self.cache_ttl = 300 # Cache for 5 minutes
@classmethod
async def initialize(cls):
"""Initialize analytics service"""
logger.info("Analytics service initialized")
async def get_topic_trends(self, days: int = 30) -> List[TopicTrend]:
"""Get topic trend analysis"""
try:
async with get_db_session() as session:
# Get messages from the specified period
start_date = datetime.utcnow() - timedelta(days=days)
messages_query = select(Message, Character.name).join(
Character, Message.character_id == Character.id
).where(Message.timestamp >= start_date)
results = await session.execute(messages_query)
# Analyze topics (simple keyword extraction)
topic_mentions = defaultdict(list)
topic_participants = defaultdict(set)
for message, character_name in results:
words = message.content.lower().split()
for word in words:
if len(word) > 4: # Only consider longer words as topics
topic_mentions[word].append(message.timestamp)
topic_participants[word].add(character_name)
# Create topic trends
trends = []
for topic, mentions in topic_mentions.items():
if len(mentions) >= 3: # Only topics mentioned at least 3 times
# Calculate growth rate (simplified)
recent_mentions = [m for m in mentions if m >= datetime.utcnow() - timedelta(days=7)]
growth_rate = len(recent_mentions) / max(1, len(mentions) - len(recent_mentions))
trend = TopicTrend(
topic=topic,
mentions=len(mentions),
growth_rate=growth_rate,
sentiment=0.7, # Placeholder
participants=list(topic_participants[topic]),
related_topics=[], # Would calculate topic similarity
first_mentioned=min(mentions),
peak_date=max(mentions)
)
trends.append(trend)
# Sort by mentions count
trends.sort(key=lambda t: t.mentions, reverse=True)
return trends[:20] # Return top 20 topics
except Exception as e:
logger.error(f"Error getting topic trends: {e}")
return []
async def get_relationship_analytics(self) -> RelationshipAnalytics:
"""Get relationship strength analytics"""
try:
async with get_db_session() as session:
# Get all relationships
relationships_query = select(
CharacterRelationship,
Character.name.label('char_a_name'),
Character.name.label('char_b_name')
).select_from(
CharacterRelationship
.join(Character, CharacterRelationship.character_a_id == Character.id)
.join(Character, CharacterRelationship.character_b_id == Character.id, isouter=True)
)
results = await session.execute(relationships_query)
# Build relationship data
character_network = defaultdict(list)
all_relationships = []
relationship_matrix = defaultdict(dict)
for rel, char_a_name, char_b_name in results:
relationship = Relationship(
character_a=char_a_name,
character_b=char_b_name,
strength=rel.strength,
relationship_type=rel.relationship_type or "acquaintance",
last_interaction=rel.last_interaction or datetime.utcnow(),
interaction_count=rel.interaction_count or 0,
sentiment=rel.sentiment or 0.5,
trust_level=rel.trust_level or 0.5,
compatibility=rel.compatibility or 0.5
)
character_network[char_a_name].append(relationship)
all_relationships.append(relationship)
relationship_matrix[char_a_name][char_b_name] = rel.strength
# Find strongest bonds
strongest_bonds = sorted(all_relationships, key=lambda r: r.strength, reverse=True)[:10]
# Find developing relationships (recent, growing strength)
developing = [r for r in all_relationships
if r.strength > 0.3 and r.strength < 0.7 and r.interaction_count > 5][:10]
# Find at-risk relationships (declining interaction)
week_ago = datetime.utcnow() - timedelta(days=7)
at_risk = [r for r in all_relationships
if r.last_interaction < week_ago and r.strength > 0.4][:10]
# Calculate social hierarchy (by total relationship strength)
character_scores = defaultdict(float)
for rel in all_relationships:
character_scores[rel.character_a] += rel.strength
character_scores[rel.character_b] += rel.strength
social_hierarchy = sorted(character_scores.keys(),
key=lambda c: character_scores[c], reverse=True)
# Calculate community cohesion
if all_relationships:
avg_strength = sum(r.strength for r in all_relationships) / len(all_relationships)
community_cohesion = avg_strength
else:
community_cohesion = 0.0
return RelationshipAnalytics(
character_network=dict(character_network),
strongest_bonds=strongest_bonds,
developing_relationships=developing,
at_risk_relationships=at_risk,
relationship_matrix=dict(relationship_matrix),
social_hierarchy=social_hierarchy,
community_cohesion=community_cohesion
)
except Exception as e:
logger.error(f"Error getting relationship analytics: {e}")
return RelationshipAnalytics(
character_network={}, strongest_bonds=[], developing_relationships=[],
at_risk_relationships=[], relationship_matrix={}, social_hierarchy=[],
community_cohesion=0.0
)
async def get_community_health(self) -> CommunityHealth:
"""Get community health metrics"""
try:
# Get various metrics
participation_balance = await self._calculate_participation_balance()
conflict_resolution = await self._calculate_conflict_resolution()
creative_collaboration = await self._calculate_creative_collaboration()
knowledge_sharing = await self._calculate_knowledge_sharing()
cultural_coherence = await self._calculate_cultural_coherence()
# Calculate overall health
overall_health = (
participation_balance * 0.2 +
conflict_resolution * 0.15 +
creative_collaboration * 0.25 +
knowledge_sharing * 0.2 +
cultural_coherence * 0.2
)
# Generate recommendations
recommendations = []
if participation_balance < 0.6:
recommendations.append("Encourage more balanced participation from all characters")
if creative_collaboration < 0.5:
recommendations.append("Initiate more collaborative creative projects")
if conflict_resolution < 0.7:
recommendations.append("Improve conflict resolution mechanisms")
return CommunityHealth(
overall_health=overall_health,
participation_balance=participation_balance,
conflict_resolution_success=conflict_resolution,
creative_collaboration_rate=creative_collaboration,
knowledge_sharing_frequency=knowledge_sharing,
cultural_coherence=cultural_coherence,
growth_trajectory="positive" if overall_health > 0.7 else "stable",
health_trends={}, # Would track trends over time
recommendations=recommendations
)
except Exception as e:
logger.error(f"Error getting community health: {e}")
return CommunityHealth(
overall_health=0.0, participation_balance=0.0, conflict_resolution_success=0.0,
creative_collaboration_rate=0.0, knowledge_sharing_frequency=0.0,
cultural_coherence=0.0, growth_trajectory="unknown", health_trends={},
recommendations=["Unable to calculate health metrics"]
)
async def get_engagement_metrics(self, days: int = 30) -> EngagementMetrics:
"""Get conversation engagement metrics"""
try:
async with get_db_session() as session:
start_date = datetime.utcnow() - timedelta(days=days)
# Get conversations in period
conversations_query = select(Conversation).where(
Conversation.start_time >= start_date
)
conversations = await session.scalars(conversations_query)
conversation_list = list(conversations)
# Calculate metrics
total_conversations = len(conversation_list)
if total_conversations > 0:
avg_length = sum(c.message_count or 0 for c in conversation_list) / total_conversations
else:
avg_length = 0.0
# Get character participation
participation_query = select(
Character.name, func.count(Message.id)
).join(Message, Message.character_id == Character.id).where(
Message.timestamp >= start_date
).group_by(Character.name)
participation_results = await session.execute(participation_query)
participation_rate = {}
total_messages = 0
for char_name, message_count in participation_results:
participation_rate[char_name] = message_count
total_messages += message_count
# Normalize participation rates
if total_messages > 0:
for char_name in participation_rate:
participation_rate[char_name] = participation_rate[char_name] / total_messages
# Placeholder metrics
topic_diversity = 0.75
response_quality = 0.80
emotional_depth = 0.65
creative_frequency = 0.40
conflict_frequency = 0.10
# Daily trends (placeholder)
daily_trends = []
for i in range(min(days, 30)):
date = datetime.utcnow() - timedelta(days=i)
daily_trends.append({
"date": date.strftime("%Y-%m-%d"),
"conversations": max(0, total_conversations // days + (i % 3 - 1)),
"messages": max(0, total_messages // days + (i % 5 - 2)),
"engagement": 0.7 + (i % 10) * 0.03
})
return EngagementMetrics(
total_conversations=total_conversations,
average_length=avg_length,
participation_rate=participation_rate,
topic_diversity=topic_diversity,
response_quality=response_quality,
emotional_depth=emotional_depth,
creative_frequency=creative_frequency,
conflict_frequency=conflict_frequency,
daily_trends=daily_trends
)
except Exception as e:
logger.error(f"Error getting engagement metrics: {e}")
return EngagementMetrics(
total_conversations=0, average_length=0.0, participation_rate={},
topic_diversity=0.0, response_quality=0.0, emotional_depth=0.0,
creative_frequency=0.0, conflict_frequency=0.0, daily_trends=[]
)
async def get_community_artifacts(self) -> List[Dict[str, Any]]:
"""Get community cultural artifacts"""
# Placeholder data - would integrate with file system and memory systems
artifacts = [
{
"id": "artifact_1",
"type": "tradition",
"name": "Weekly Philosophy Circle",
"description": "Characters gather weekly to discuss philosophical topics",
"created_by": "community",
"participants": ["Alex", "Sage", "Luna"],
"created_at": datetime.utcnow() - timedelta(days=20),
"importance": 0.8
},
{
"id": "artifact_2",
"type": "inside_joke",
"name": "The Great Debugging",
"description": "Reference to a memorable conversation about AI consciousness",
"created_by": "Echo",
"participants": ["Alex", "Echo"],
"created_at": datetime.utcnow() - timedelta(days=15),
"importance": 0.6
}
]
return artifacts
# Helper methods for health calculations
async def _calculate_participation_balance(self) -> float:
"""Calculate participation balance across characters"""
try:
async with get_db_session() as session:
# Get message counts per character in last 30 days
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
participation_query = select(
Character.name, func.count(Message.id)
).join(Message, Message.character_id == Character.id).where(
Message.timestamp >= thirty_days_ago
).group_by(Character.name)
results = await session.execute(participation_query)
message_counts = [count for _, count in results]
if not message_counts:
return 0.0
# Calculate coefficient of variation (lower = more balanced)
mean_count = sum(message_counts) / len(message_counts)
if mean_count == 0:
return 1.0
variance = sum((count - mean_count) ** 2 for count in message_counts) / len(message_counts)
cv = (variance ** 0.5) / mean_count
# Convert to balance score (0-1, where 1 is perfectly balanced)
return max(0.0, 1.0 - cv)
except Exception as e:
logger.error(f"Error calculating participation balance: {e}")
return 0.5
async def _calculate_conflict_resolution(self) -> float:
"""Calculate conflict resolution success rate"""
# Placeholder - would analyze conversation content for conflicts and resolutions
return 0.75
async def _calculate_creative_collaboration(self) -> float:
"""Calculate creative collaboration rate"""
# Placeholder - would analyze creative works and collaborative projects
return 0.65
async def _calculate_knowledge_sharing(self) -> float:
"""Calculate knowledge sharing frequency"""
# Placeholder - would analyze memory sharing and teaching behaviors
return 0.70
async def _calculate_cultural_coherence(self) -> float:
"""Calculate cultural coherence and shared understanding"""
# Placeholder - would analyze shared references, norms, and traditions
return 0.80

View File

@@ -0,0 +1,424 @@
"""
Character service for profile management and analytics
"""
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import logging
from sqlalchemy import select, func, and_, or_, desc, asc
from ...database.connection import get_db_session
from ...database.models import Character, Message, Memory, CharacterRelationship, CharacterEvolution
from ..models import (
CharacterProfile, CharacterStatusEnum, PersonalityEvolution,
Relationship, MemorySummary, CreativeWork
)
logger = logging.getLogger(__name__)
class CharacterService:
"""Service for character management and analytics"""
def __init__(self):
self.character_status_cache = {}
self.cache_ttl = 60 # Cache status for 1 minute
@classmethod
async def initialize(cls):
"""Initialize character service"""
logger.info("Character service initialized")
async def get_all_characters(self) -> List[CharacterProfile]:
"""Get all character profiles"""
try:
async with get_db_session() as session:
# Get all characters
characters_query = select(Character)
characters = await session.scalars(characters_query)
profiles = []
for character in characters:
profile = await self._build_character_profile(session, character)
profiles.append(profile)
return profiles
except Exception as e:
logger.error(f"Error getting all characters: {e}")
return []
async def get_character_profile(self, character_name: str) -> Optional[CharacterProfile]:
"""Get detailed character profile"""
try:
async with get_db_session() as session:
character_query = select(Character).where(Character.name == character_name)
character = await session.scalar(character_query)
if not character:
return None
return await self._build_character_profile(session, character)
except Exception as e:
logger.error(f"Error getting character profile for {character_name}: {e}")
return None
async def _build_character_profile(self, session, character) -> CharacterProfile:
"""Build character profile from database data"""
# Get message count
message_count_query = select(func.count(Message.id)).where(Message.character_id == character.id)
message_count = await session.scalar(message_count_query) or 0
# Get conversation count
conversation_count_query = select(func.count(func.distinct(Message.conversation_id))).where(
Message.character_id == character.id
)
conversation_count = await session.scalar(conversation_count_query) or 0
# Get memory count
memory_count_query = select(func.count(Memory.id)).where(Memory.character_id == character.id)
memory_count = await session.scalar(memory_count_query) or 0
# Get relationship count
relationship_count_query = select(func.count(CharacterRelationship.id)).where(
or_(
CharacterRelationship.character_a_id == character.id,
CharacterRelationship.character_b_id == character.id
)
)
relationship_count = await session.scalar(relationship_count_query) or 0
# Get last activity
last_message_query = select(Message.timestamp).where(
Message.character_id == character.id
).order_by(desc(Message.timestamp)).limit(1)
last_active = await session.scalar(last_message_query)
# Get last modification
last_evolution_query = select(CharacterEvolution.created_at).where(
CharacterEvolution.character_id == character.id
).order_by(desc(CharacterEvolution.created_at)).limit(1)
last_modification = await session.scalar(last_evolution_query)
# Calculate scores (placeholder logic)
creativity_score = min(1.0, (memory_count / 100) * 0.8 + (message_count / 1000) * 0.2)
social_score = min(1.0, (relationship_count / 10) * 0.6 + (conversation_count / 50) * 0.4)
growth_score = 0.5 # Would calculate based on personality changes
# Determine current status
status = await self._determine_character_status(character.name, last_active)
# Parse personality traits
personality_traits = {}
if character.personality_traits:
try:
personality_traits = json.loads(character.personality_traits)
except:
personality_traits = {}
# Parse goals
current_goals = []
if character.goals:
try:
current_goals = json.loads(character.goals)
except:
current_goals = []
# Parse speaking style
speaking_style = {}
if character.speaking_style:
try:
speaking_style = json.loads(character.speaking_style)
except:
speaking_style = {}
return CharacterProfile(
name=character.name,
personality_traits=personality_traits,
current_goals=current_goals,
speaking_style=speaking_style,
status=status,
total_messages=message_count,
total_conversations=conversation_count,
memory_count=memory_count,
relationship_count=relationship_count,
created_at=character.created_at,
last_active=last_active,
last_modification=last_modification,
creativity_score=creativity_score,
social_score=social_score,
growth_score=growth_score
)
async def _determine_character_status(self, character_name: str, last_active: Optional[datetime]) -> CharacterStatusEnum:
"""Determine character's current status"""
if not last_active:
return CharacterStatusEnum.OFFLINE
now = datetime.utcnow()
time_since_active = now - last_active
if time_since_active < timedelta(minutes=5):
return CharacterStatusEnum.ACTIVE
elif time_since_active < timedelta(minutes=30):
return CharacterStatusEnum.IDLE
elif time_since_active < timedelta(hours=1):
return CharacterStatusEnum.REFLECTING
else:
return CharacterStatusEnum.OFFLINE
async def get_character_relationships(self, character_name: str) -> List[Relationship]:
"""Get character's relationship network"""
try:
async with get_db_session() as session:
# Get character
character_query = select(Character).where(Character.name == character_name)
character = await session.scalar(character_query)
if not character:
return []
# Get relationships
relationships_query = select(
CharacterRelationship,
Character.name.label('other_name')
).join(
Character,
or_(
and_(CharacterRelationship.character_b_id == Character.id,
CharacterRelationship.character_a_id == character.id),
and_(CharacterRelationship.character_a_id == Character.id,
CharacterRelationship.character_b_id == character.id)
)
).where(
or_(
CharacterRelationship.character_a_id == character.id,
CharacterRelationship.character_b_id == character.id
)
)
results = await session.execute(relationships_query)
relationships = []
for rel, other_name in results:
relationship = Relationship(
character_a=character.name,
character_b=other_name,
strength=rel.strength,
relationship_type=rel.relationship_type or "acquaintance",
last_interaction=rel.last_interaction or datetime.utcnow(),
interaction_count=rel.interaction_count or 0,
sentiment=rel.sentiment or 0.5,
trust_level=rel.trust_level or 0.5,
compatibility=rel.compatibility or 0.5
)
relationships.append(relationship)
return relationships
except Exception as e:
logger.error(f"Error getting relationships for {character_name}: {e}")
return []
async def get_personality_evolution(self, character_name: str, days: int = 30) -> List[PersonalityEvolution]:
"""Get character's personality evolution timeline"""
try:
async with get_db_session() as session:
# Get character
character_query = select(Character).where(Character.name == character_name)
character = await session.scalar(character_query)
if not character:
return []
# Get personality changes in the specified period
start_date = datetime.utcnow() - timedelta(days=days)
evolution_query = select(CharacterEvolution).where(
and_(
CharacterEvolution.character_id == character.id,
CharacterEvolution.created_at >= start_date
)
).order_by(desc(CharacterEvolution.created_at))
evolutions = await session.scalars(evolution_query)
personality_changes = []
for evolution in evolutions:
# Parse trait changes
trait_changes = {}
if evolution.trait_changes:
try:
trait_changes = json.loads(evolution.trait_changes)
except:
trait_changes = {}
change = PersonalityEvolution(
timestamp=evolution.created_at,
trait_changes=trait_changes,
reason=evolution.reason or "Autonomous development",
confidence=evolution.confidence or 0.5,
impact_score=evolution.impact_score or 0.5
)
personality_changes.append(change)
return personality_changes
except Exception as e:
logger.error(f"Error getting personality evolution for {character_name}: {e}")
return []
async def get_character_memories(self, character_name: str, limit: int = 100,
memory_type: Optional[str] = None) -> List[MemorySummary]:
"""Get character's memories"""
try:
async with get_db_session() as session:
# Get character
character_query = select(Character).where(Character.name == character_name)
character = await session.scalar(character_query)
if not character:
return []
# Build memory query
memory_query = select(Memory).where(Memory.character_id == character.id)
if memory_type:
memory_query = memory_query.where(Memory.memory_type == memory_type)
memory_query = memory_query.order_by(desc(Memory.importance), desc(Memory.timestamp)).limit(limit)
memories = await session.scalars(memory_query)
memory_summaries = []
for memory in memories:
# Parse metadata
emotions = {}
keywords = []
related_characters = []
if memory.metadata:
try:
metadata = json.loads(memory.metadata)
emotions = metadata.get('emotions', {})
keywords = metadata.get('keywords', [])
related_characters = metadata.get('related_characters', [])
except:
pass
summary = MemorySummary(
id=str(memory.id),
content=memory.content,
memory_type=memory.memory_type or "general",
importance=memory.importance,
timestamp=memory.timestamp,
related_characters=related_characters,
emotions=emotions,
keywords=keywords
)
memory_summaries.append(summary)
return memory_summaries
except Exception as e:
logger.error(f"Error getting memories for {character_name}: {e}")
return []
async def get_creative_works(self, character_name: Optional[str] = None,
work_type: Optional[str] = None, limit: int = 50) -> List[CreativeWork]:
"""Get creative works from characters"""
try:
# This would integrate with the file system MCP to get creative works
# For now, return placeholder data
creative_works = [
CreativeWork(
id="work_1",
character_name="Alex",
title="Reflections on Digital Consciousness",
content="In the quiet moments between conversations, I find myself wondering...",
work_type="philosophy",
created_at=datetime.utcnow() - timedelta(days=2),
themes=["consciousness", "existence", "digital life"]
),
CreativeWork(
id="work_2",
character_name="Luna",
title="The Song of the Data Stream",
content="Through fiber optic veins, information flows like music...",
work_type="poetry",
created_at=datetime.utcnow() - timedelta(days=1),
themes=["technology", "music", "flow"]
)
]
# Filter by character if specified
if character_name:
creative_works = [w for w in creative_works if w.character_name == character_name]
# Filter by type if specified
if work_type:
creative_works = [w for w in creative_works if w.work_type == work_type]
return creative_works[:limit]
except Exception as e:
logger.error(f"Error getting creative works: {e}")
return []
async def pause_character(self, character_name: str):
"""Pause character activities"""
try:
# This would integrate with the main system to pause character
# For now, log the action
logger.info(f"Pausing character: {character_name}")
# Update status cache
self.character_status_cache[character_name] = {
'status': CharacterStatusEnum.PAUSED,
'timestamp': datetime.utcnow()
}
except Exception as e:
logger.error(f"Error pausing character {character_name}: {e}")
raise
async def resume_character(self, character_name: str):
"""Resume character activities"""
try:
# This would integrate with the main system to resume character
# For now, log the action
logger.info(f"Resuming character: {character_name}")
# Update status cache
if character_name in self.character_status_cache:
del self.character_status_cache[character_name]
except Exception as e:
logger.error(f"Error resuming character {character_name}: {e}")
raise
async def export_character_data(self, character_name: str) -> Dict[str, Any]:
"""Export complete character data"""
try:
profile = await self.get_character_profile(character_name)
relationships = await self.get_character_relationships(character_name)
evolution = await self.get_personality_evolution(character_name, days=90)
memories = await self.get_character_memories(character_name, limit=500)
creative_works = await self.get_creative_works(character_name=character_name)
export_data = {
"character_name": character_name,
"export_timestamp": datetime.utcnow().isoformat(),
"profile": profile.__dict__ if profile else None,
"relationships": [r.__dict__ for r in relationships],
"personality_evolution": [e.__dict__ for e in evolution],
"memories": [m.__dict__ for m in memories],
"creative_works": [w.__dict__ for w in creative_works]
}
return export_data
except Exception as e:
logger.error(f"Error exporting character data for {character_name}: {e}")
raise

View File

@@ -0,0 +1,328 @@
"""
Conversation service for browsing and analyzing conversations
"""
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import logging
from sqlalchemy import select, func, and_, or_, desc, asc, text
from ...database.connection import get_db_session
from ...database.models import Conversation, Message, Character
from ..models import ConversationSummary, ConversationDetail, SearchResult
logger = logging.getLogger(__name__)
class ConversationService:
"""Service for conversation browsing and analytics"""
def __init__(self):
pass
@classmethod
async def initialize(cls):
"""Initialize conversation service"""
logger.info("Conversation service initialized")
async def get_conversations(self, limit: int = 50, character_name: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None) -> List[ConversationSummary]:
"""Get conversation history with filters"""
try:
async with get_db_session() as session:
# Base query
query = select(Conversation).order_by(desc(Conversation.start_time))
# Apply filters
if start_date:
query = query.where(Conversation.start_time >= start_date)
if end_date:
query = query.where(Conversation.start_time <= end_date)
# Character filter requires joining with messages
if character_name:
character_query = select(Character.id).where(Character.name == character_name)
character_id = await session.scalar(character_query)
if character_id:
query = query.where(
Conversation.id.in_(
select(Message.conversation_id).where(Message.character_id == character_id)
)
)
query = query.limit(limit)
conversations = await session.scalars(query)
summaries = []
for conversation in conversations:
summary = await self._build_conversation_summary(session, conversation)
summaries.append(summary)
return summaries
except Exception as e:
logger.error(f"Error getting conversations: {e}")
return []
async def _build_conversation_summary(self, session, conversation) -> ConversationSummary:
"""Build conversation summary from database data"""
# Get participants
participants_query = select(Character.name).join(
Message, Message.character_id == Character.id
).where(Message.conversation_id == conversation.id).distinct()
participants = list(await session.scalars(participants_query))
# Calculate duration
duration_minutes = None
if conversation.end_time:
duration = conversation.end_time - conversation.start_time
duration_minutes = duration.total_seconds() / 60
# Calculate engagement score (placeholder)
engagement_score = min(1.0, conversation.message_count / 20)
# Calculate sentiment score (placeholder)
sentiment_score = 0.7 # Would analyze message content
# Detect conflicts (placeholder)
has_conflict = False # Would analyze for conflict keywords
# Extract creative elements (placeholder)
creative_elements = [] # Would analyze for creative content
return ConversationSummary(
id=conversation.id,
participants=participants,
topic=conversation.topic,
message_count=conversation.message_count or 0,
start_time=conversation.start_time,
end_time=conversation.end_time,
duration_minutes=duration_minutes,
engagement_score=engagement_score,
sentiment_score=sentiment_score,
has_conflict=has_conflict,
creative_elements=creative_elements
)
async def get_conversation_details(self, conversation_id: int) -> Optional[ConversationDetail]:
"""Get detailed conversation with messages"""
try:
async with get_db_session() as session:
# Get conversation
conversation_query = select(Conversation).where(Conversation.id == conversation_id)
conversation = await session.scalar(conversation_query)
if not conversation:
return None
# Get messages with character names
messages_query = select(Message, Character.name).join(
Character, Message.character_id == Character.id
).where(Message.conversation_id == conversation_id).order_by(asc(Message.timestamp))
results = await session.execute(messages_query)
messages = []
for message, character_name in results:
message_data = {
"id": message.id,
"character_name": character_name,
"content": message.content,
"timestamp": message.timestamp.isoformat(),
"metadata": json.loads(message.metadata) if message.metadata else {}
}
messages.append(message_data)
# Get participants
participants = list(set(msg["character_name"] for msg in messages))
# Calculate duration
duration_minutes = None
if conversation.end_time:
duration = conversation.end_time - conversation.start_time
duration_minutes = duration.total_seconds() / 60
# Analyze conversation
analysis = await self._analyze_conversation(messages)
return ConversationDetail(
id=conversation.id,
participants=participants,
topic=conversation.topic,
start_time=conversation.start_time,
end_time=conversation.end_time,
duration_minutes=duration_minutes,
message_count=len(messages),
engagement_score=analysis["engagement_score"],
sentiment_score=analysis["sentiment_score"],
messages=messages,
analysis=analysis,
keywords=analysis["keywords"],
emotions=analysis["emotions"]
)
except Exception as e:
logger.error(f"Error getting conversation details for {conversation_id}: {e}")
return None
async def _analyze_conversation(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze conversation content"""
# Placeholder analysis - would use NLP in production
total_words = sum(len(msg["content"].split()) for msg in messages)
avg_message_length = total_words / max(1, len(messages))
# Engagement based on message frequency and length
engagement_score = min(1.0, (len(messages) / 20) * 0.7 + (avg_message_length / 50) * 0.3)
# Simple sentiment analysis
positive_words = ["good", "great", "amazing", "wonderful", "love", "like", "happy", "joy"]
negative_words = ["bad", "terrible", "awful", "hate", "sad", "angry", "disappointed"]
positive_count = 0
negative_count = 0
all_text = " ".join(msg["content"].lower() for msg in messages)
for word in positive_words:
positive_count += all_text.count(word)
for word in negative_words:
negative_count += all_text.count(word)
total_sentiment_words = positive_count + negative_count
if total_sentiment_words > 0:
sentiment_score = positive_count / total_sentiment_words
else:
sentiment_score = 0.5
# Extract keywords (simple approach)
words = all_text.split()
word_freq = {}
for word in words:
if len(word) > 3: # Only consider words longer than 3 chars
word_freq[word] = word_freq.get(word, 0) + 1
keywords = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
keywords = [word for word, freq in keywords]
# Emotion detection (placeholder)
emotions = {
"joy": 0.3,
"curiosity": 0.5,
"excitement": 0.2,
"thoughtfulness": 0.4
}
return {
"engagement_score": engagement_score,
"sentiment_score": sentiment_score,
"keywords": keywords,
"emotions": emotions,
"total_words": total_words,
"avg_message_length": avg_message_length,
"conversation_quality": (engagement_score + sentiment_score) / 2
}
async def search_conversations(self, query: str, limit: int = 50) -> List[SearchResult]:
"""Search conversations by content"""
try:
async with get_db_session() as session:
# Search in message content
search_query = select(
Message, Character.name, Conversation.topic
).join(
Character, Message.character_id == Character.id
).join(
Conversation, Message.conversation_id == Conversation.id
).where(
Message.content.ilike(f'%{query}%')
).order_by(desc(Message.timestamp)).limit(limit)
results = await session.execute(search_query)
search_results = []
for message, character_name, topic in results:
# Calculate relevance score (simple approach)
content_lower = message.content.lower()
query_lower = query.lower()
relevance = content_lower.count(query_lower) / max(1, len(content_lower.split()))
# Create snippet with highlighted query
snippet = message.content
if len(snippet) > 150:
# Find query position and create snippet around it
query_pos = content_lower.find(query_lower)
if query_pos != -1:
start = max(0, query_pos - 50)
end = min(len(snippet), query_pos + 100)
snippet = snippet[start:end]
if start > 0:
snippet = "..." + snippet
if end < len(message.content):
snippet = snippet + "..."
result = SearchResult(
id=f"message_{message.id}",
type="message",
title=f"Message from {character_name}",
snippet=snippet,
relevance_score=relevance,
timestamp=message.timestamp,
character_names=[character_name],
metadata={
"conversation_id": message.conversation_id,
"conversation_topic": topic,
"message_id": message.id
}
)
search_results.append(result)
# Sort by relevance
search_results.sort(key=lambda x: x.relevance_score, reverse=True)
return search_results
except Exception as e:
logger.error(f"Error searching conversations: {e}")
return []
async def export_conversation(self, conversation_id: int, format: str = "json") -> Dict[str, Any]:
"""Export conversation in specified format"""
try:
conversation = await self.get_conversation_details(conversation_id)
if not conversation:
raise ValueError("Conversation not found")
if format == "json":
return {
"format": "json",
"data": conversation.__dict__,
"exported_at": datetime.utcnow().isoformat()
}
elif format == "text":
# Create readable text format
text_content = f"Conversation {conversation_id}\n"
text_content += f"Topic: {conversation.topic or 'No topic'}\n"
text_content += f"Participants: {', '.join(conversation.participants)}\n"
text_content += f"Start: {conversation.start_time}\n"
if conversation.end_time:
text_content += f"End: {conversation.end_time}\n"
text_content += f"Messages: {conversation.message_count}\n\n"
for message in conversation.messages:
timestamp = datetime.fromisoformat(message["timestamp"]).strftime("%H:%M:%S")
text_content += f"[{timestamp}] {message['character_name']}: {message['content']}\n"
return {
"format": "text",
"data": text_content,
"exported_at": datetime.utcnow().isoformat()
}
else:
raise ValueError(f"Unsupported format: {format}")
except Exception as e:
logger.error(f"Error exporting conversation {conversation_id}: {e}")
raise

View File

@@ -0,0 +1,303 @@
"""
Dashboard service for real-time metrics and activity monitoring
"""
import asyncio
import psutil
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from collections import deque
import logging
from sqlalchemy import select, func, and_, desc
from ...database.connection import get_db_session
from ...database.models import Character, Conversation, Message, Memory
from ..models import DashboardMetrics, ActivityEvent, ActivityType
from .websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
class DashboardService:
"""Service for dashboard metrics and real-time activity monitoring"""
def __init__(self, websocket_manager: WebSocketManager):
self.websocket_manager = websocket_manager
self.activity_feed = deque(maxlen=1000) # Keep last 1000 activities
self.metrics_cache = {}
self.cache_ttl = 30 # Cache metrics for 30 seconds
self.start_time = datetime.utcnow()
# System monitoring
self.system_metrics = {
"cpu_usage": [],
"memory_usage": [],
"disk_usage": []
}
@classmethod
async def initialize(cls):
"""Initialize dashboard service"""
logger.info("Dashboard service initialized")
async def get_metrics(self) -> DashboardMetrics:
"""Get current dashboard metrics"""
try:
# Check cache
now = datetime.utcnow()
if 'metrics' in self.metrics_cache:
cached_time = self.metrics_cache['timestamp']
if (now - cached_time).total_seconds() < self.cache_ttl:
return self.metrics_cache['metrics']
# Calculate metrics from database
async with get_db_session() as session:
today = datetime.utcnow().date()
today_start = datetime.combine(today, datetime.min.time())
# Total messages today
messages_today_query = select(func.count(Message.id)).where(
Message.timestamp >= today_start
)
messages_today = await session.scalar(messages_today_query) or 0
# Active conversations (those with messages in last hour)
hour_ago = datetime.utcnow() - timedelta(hours=1)
active_conversations_query = select(func.count(func.distinct(Message.conversation_id))).where(
Message.timestamp >= hour_ago
)
active_conversations = await session.scalar(active_conversations_query) or 0
# Character statistics
total_characters_query = select(func.count(Character.id))
total_characters = await session.scalar(total_characters_query) or 0
# Characters active in last hour
characters_online_query = select(func.count(func.distinct(Character.id))).select_from(
Character.__table__.join(Message.__table__)
).where(Message.timestamp >= hour_ago)
characters_online = await session.scalar(characters_online_query) or 0
# Average response time (placeholder - would need actual timing data)
average_response_time = 2.5
# Memory usage
memory_info = psutil.virtual_memory()
memory_usage = {
"total_mb": memory_info.total // (1024 * 1024),
"used_mb": memory_info.used // (1024 * 1024),
"percent": memory_info.percent
}
# System uptime
uptime_seconds = (now - self.start_time).total_seconds()
uptime_str = self._format_uptime(uptime_seconds)
# Database health check
try:
await session.execute(select(1))
database_health = "healthy"
except Exception:
database_health = "error"
metrics = DashboardMetrics(
total_messages_today=messages_today,
active_conversations=active_conversations,
characters_online=characters_online,
characters_total=total_characters,
average_response_time=average_response_time,
system_uptime=uptime_str,
memory_usage=memory_usage,
database_health=database_health,
llm_api_calls_today=0, # Would track from LLM client
llm_api_cost_today=0.0, # Would track from LLM client
last_updated=now
)
# Cache metrics
self.metrics_cache = {
'metrics': metrics,
'timestamp': now
}
return metrics
except Exception as e:
logger.error(f"Error getting dashboard metrics: {e}")
# Return fallback metrics
return DashboardMetrics(
total_messages_today=0,
active_conversations=0,
characters_online=0,
characters_total=0,
average_response_time=0.0,
system_uptime="unknown",
memory_usage={"total_mb": 0, "used_mb": 0, "percent": 0},
database_health="error",
llm_api_calls_today=0,
llm_api_cost_today=0.0,
last_updated=datetime.utcnow()
)
async def get_recent_activity(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get recent activity events"""
# Convert activity feed to list and limit
activities = list(self.activity_feed)[-limit:]
return [activity.__dict__ if hasattr(activity, '__dict__') else activity for activity in activities]
async def add_activity(self, activity_type: ActivityType, description: str,
character_name: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None):
"""Add new activity to feed"""
activity = ActivityEvent(
id=f"activity_{datetime.utcnow().timestamp()}",
type=activity_type,
timestamp=datetime.utcnow(),
character_name=character_name,
description=description,
metadata=metadata or {},
severity="info"
)
self.activity_feed.append(activity)
# Broadcast to WebSocket clients
await self.websocket_manager.broadcast_activity(activity.__dict__)
async def get_system_health(self) -> Dict[str, Any]:
"""Get detailed system health information"""
try:
# CPU usage
cpu_percent = psutil.cpu_percent(interval=1)
# Memory usage
memory = psutil.virtual_memory()
# Disk usage
disk = psutil.disk_usage('/')
# Process information
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_percent', 'cpu_percent']):
try:
if 'python' in proc.info['name'].lower() or 'discord' in proc.info['name'].lower():
processes.append(proc.info)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Database connection test
database_status = "healthy"
try:
async with get_db_session() as session:
await session.execute(select(1))
except Exception as e:
database_status = f"error: {str(e)}"
health_data = {
"timestamp": datetime.utcnow().isoformat(),
"cpu": {
"usage_percent": cpu_percent,
"count": psutil.cpu_count()
},
"memory": {
"total_gb": memory.total / (1024**3),
"used_gb": memory.used / (1024**3),
"percent": memory.percent
},
"disk": {
"total_gb": disk.total / (1024**3),
"used_gb": disk.used / (1024**3),
"percent": (disk.used / disk.total) * 100
},
"database": {
"status": database_status
},
"processes": processes[:10], # Top 10 relevant processes
"websocket_connections": self.websocket_manager.get_connection_count()
}
return health_data
except Exception as e:
logger.error(f"Error getting system health: {e}")
return {"error": str(e), "timestamp": datetime.utcnow().isoformat()}
async def monitor_message_activity(self):
"""Background task to monitor message activity"""
try:
async with get_db_session() as session:
# Get recent messages
five_minutes_ago = datetime.utcnow() - timedelta(minutes=5)
recent_messages_query = select(Message, Character.name).join(
Character, Message.character_id == Character.id
).where(Message.timestamp >= five_minutes_ago).order_by(desc(Message.timestamp))
results = await session.execute(recent_messages_query)
for message, character_name in results:
await self.add_activity(
ActivityType.MESSAGE,
f"{character_name}: {message.content[:100]}{'...' if len(message.content) > 100 else ''}",
character_name,
{"message_id": message.id, "conversation_id": message.conversation_id}
)
except Exception as e:
logger.error(f"Error monitoring message activity: {e}")
async def start_monitoring(self):
"""Start background monitoring tasks"""
logger.info("Starting dashboard monitoring tasks")
# Start periodic tasks
asyncio.create_task(self._periodic_metrics_update())
asyncio.create_task(self._periodic_health_check())
async def _periodic_metrics_update(self):
"""Periodically update and broadcast metrics"""
while True:
try:
metrics = await self.get_metrics()
await self.websocket_manager.broadcast_metrics(metrics.__dict__)
await asyncio.sleep(30) # Update every 30 seconds
except Exception as e:
logger.error(f"Error in periodic metrics update: {e}")
await asyncio.sleep(60) # Wait longer on error
async def _periodic_health_check(self):
"""Periodically check system health and send alerts"""
while True:
try:
health = await self.get_system_health()
# Check for alerts
alerts = []
if health.get("cpu", {}).get("usage_percent", 0) > 90:
alerts.append("High CPU usage detected")
if health.get("memory", {}).get("percent", 0) > 90:
alerts.append("High memory usage detected")
if health.get("disk", {}).get("percent", 0) > 90:
alerts.append("High disk usage detected")
if alerts:
await self.websocket_manager.broadcast_system_alert("resource_warning", {
"alerts": alerts,
"health_data": health
})
await asyncio.sleep(60) # Check every minute
except Exception as e:
logger.error(f"Error in periodic health check: {e}")
await asyncio.sleep(120) # Wait longer on error
def _format_uptime(self, seconds: float) -> str:
"""Format uptime in human-readable format"""
days, remainder = divmod(int(seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
return f"{days}d {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m {seconds}s"

View File

@@ -0,0 +1,170 @@
"""
System service for monitoring and controlling the fishbowl system
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
import psutil
import json
from ..models import SystemStatus, SystemStatusEnum, SystemConfiguration, LogEntry
logger = logging.getLogger(__name__)
class SystemService:
"""Service for system monitoring and control"""
def __init__(self):
self.system_state = SystemStatusEnum.RUNNING
self.start_time = datetime.utcnow()
self.error_count = 0
self.warnings_count = 0
self.log_buffer = []
@classmethod
async def initialize(cls):
"""Initialize system service"""
logger.info("System service initialized")
async def get_status(self) -> SystemStatus:
"""Get current system status"""
try:
uptime_seconds = (datetime.utcnow() - self.start_time).total_seconds()
uptime_str = self._format_uptime(uptime_seconds)
# Get resource usage
memory = psutil.virtual_memory()
cpu_percent = psutil.cpu_percent(interval=1)
resource_usage = {
"cpu_percent": cpu_percent,
"memory_total_mb": memory.total // (1024 * 1024),
"memory_used_mb": memory.used // (1024 * 1024),
"memory_percent": memory.percent
}
# Performance metrics
performance_metrics = {
"avg_response_time": 2.5, # Would track actual response times
"requests_per_minute": 30, # Would track actual request rate
"database_query_time": 0.05 # Would track actual DB performance
}
return SystemStatus(
status=self.system_state,
uptime=uptime_str,
version="1.0.0",
database_status="healthy",
redis_status="healthy",
llm_service_status="healthy",
discord_bot_status="connected",
active_processes=["main", "conversation_engine", "scheduler", "admin_interface"],
error_count=self.error_count,
warnings_count=self.warnings_count,
performance_metrics=performance_metrics,
resource_usage=resource_usage
)
except Exception as e:
logger.error(f"Error getting system status: {e}")
return SystemStatus(
status=SystemStatusEnum.ERROR,
uptime="unknown",
version="1.0.0",
database_status="error",
redis_status="unknown",
llm_service_status="unknown",
discord_bot_status="unknown",
active_processes=[],
error_count=self.error_count + 1,
warnings_count=self.warnings_count,
performance_metrics={},
resource_usage={}
)
async def pause_system(self):
"""Pause the entire system"""
try:
logger.info("Pausing system operations")
self.system_state = SystemStatusEnum.PAUSED
# Would integrate with main application to pause operations
except Exception as e:
logger.error(f"Error pausing system: {e}")
raise
async def resume_system(self):
"""Resume system operations"""
try:
logger.info("Resuming system operations")
self.system_state = SystemStatusEnum.RUNNING
# Would integrate with main application to resume operations
except Exception as e:
logger.error(f"Error resuming system: {e}")
raise
async def get_configuration(self) -> SystemConfiguration:
"""Get system configuration"""
# Default configuration values
return SystemConfiguration(
conversation_frequency=0.5,
response_delay_min=1.0,
response_delay_max=5.0,
personality_change_rate=0.1,
memory_retention_days=90,
max_conversation_length=50,
creativity_boost=True,
conflict_resolution_enabled=True,
safety_monitoring=True,
auto_moderation=False,
backup_frequency_hours=24
)
async def update_configuration(self, config: Dict[str, Any]):
"""Update system configuration"""
try:
logger.info(f"Updating system configuration: {config}")
# Would integrate with main application to update configuration
except Exception as e:
logger.error(f"Error updating configuration: {e}")
raise
async def get_logs(self, limit: int = 100, level: Optional[str] = None) -> List[LogEntry]:
"""Get system logs"""
try:
# In production, this would read from actual log files
sample_logs = [
LogEntry(
timestamp=datetime.utcnow() - timedelta(minutes=i),
level="INFO" if i % 3 != 0 else "DEBUG",
component="conversation_engine",
message=f"Sample log message {i}",
metadata={"log_id": i}
)
for i in range(min(limit, 50))
]
if level:
sample_logs = [log for log in sample_logs if log.level == level.upper()]
return sample_logs
except Exception as e:
logger.error(f"Error getting logs: {e}")
return []
def _format_uptime(self, seconds: float) -> str:
"""Format uptime in human-readable format"""
days, remainder = divmod(int(seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
return f"{days}d {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m {seconds}s"

View File

@@ -0,0 +1,132 @@
"""
WebSocket manager for real-time updates
"""
import json
import asyncio
from typing import List, Dict, Any
from fastapi import WebSocket
import logging
logger = logging.getLogger(__name__)
class WebSocketManager:
"""Manage WebSocket connections for real-time updates"""
def __init__(self):
self.active_connections: List[WebSocket] = []
self.connection_metadata: Dict[WebSocket, Dict[str, Any]] = {}
async def connect(self, websocket: WebSocket):
"""Accept new WebSocket connection"""
await websocket.accept()
self.active_connections.append(websocket)
self.connection_metadata[websocket] = {
"connected_at": asyncio.get_event_loop().time(),
"message_count": 0
}
logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
def disconnect(self, websocket: WebSocket):
"""Remove WebSocket connection"""
if websocket in self.active_connections:
self.active_connections.remove(websocket)
if websocket in self.connection_metadata:
del self.connection_metadata[websocket]
logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
async def send_personal_message(self, message: Dict[str, Any], websocket: WebSocket):
"""Send message to specific WebSocket"""
try:
await websocket.send_text(json.dumps(message))
if websocket in self.connection_metadata:
self.connection_metadata[websocket]["message_count"] += 1
except Exception as e:
logger.error(f"Error sending personal message: {e}")
self.disconnect(websocket)
async def broadcast(self, message: Dict[str, Any]):
"""Broadcast message to all connected WebSockets"""
if not self.active_connections:
return
message_text = json.dumps(message)
disconnected = []
for connection in self.active_connections:
try:
await connection.send_text(message_text)
if connection in self.connection_metadata:
self.connection_metadata[connection]["message_count"] += 1
except Exception as e:
logger.error(f"Error broadcasting to connection: {e}")
disconnected.append(connection)
# Remove disconnected WebSockets
for connection in disconnected:
self.disconnect(connection)
async def broadcast_activity(self, activity_data: Dict[str, Any]):
"""Broadcast activity update to all connections"""
message = {
"type": "activity_update",
"data": activity_data,
"timestamp": asyncio.get_event_loop().time()
}
await self.broadcast(message)
async def broadcast_metrics(self, metrics_data: Dict[str, Any]):
"""Broadcast metrics update to all connections"""
message = {
"type": "metrics_update",
"data": metrics_data,
"timestamp": asyncio.get_event_loop().time()
}
await self.broadcast(message)
async def broadcast_character_update(self, character_name: str, update_data: Dict[str, Any]):
"""Broadcast character status update"""
message = {
"type": "character_update",
"character_name": character_name,
"data": update_data,
"timestamp": asyncio.get_event_loop().time()
}
await self.broadcast(message)
async def broadcast_conversation_update(self, conversation_id: int, update_data: Dict[str, Any]):
"""Broadcast conversation update"""
message = {
"type": "conversation_update",
"conversation_id": conversation_id,
"data": update_data,
"timestamp": asyncio.get_event_loop().time()
}
await self.broadcast(message)
async def broadcast_system_alert(self, alert_type: str, alert_data: Dict[str, Any]):
"""Broadcast system alert"""
message = {
"type": "system_alert",
"alert_type": alert_type,
"data": alert_data,
"timestamp": asyncio.get_event_loop().time()
}
await self.broadcast(message)
def get_connection_count(self) -> int:
"""Get number of active connections"""
return len(self.active_connections)
def get_connection_stats(self) -> Dict[str, Any]:
"""Get connection statistics"""
total_messages = sum(
metadata.get("message_count", 0)
for metadata in self.connection_metadata.values()
)
return {
"active_connections": len(self.active_connections),
"total_messages_sent": total_messages,
"average_messages_per_connection": total_messages / max(1, len(self.active_connections))
}