diff --git a/README.md b/README.md
index 96eaa46..0832bbe 100644
--- a/README.md
+++ b/README.md
@@ -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`)
diff --git a/admin-frontend/README.md b/admin-frontend/README.md
new file mode 100644
index 0000000..0a68443
--- /dev/null
+++ b/admin-frontend/README.md
@@ -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
\ No newline at end of file
diff --git a/admin-frontend/package.json b/admin-frontend/package.json
new file mode 100644
index 0000000..ffe3196
--- /dev/null
+++ b/admin-frontend/package.json
@@ -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"
+}
\ No newline at end of file
diff --git a/admin-frontend/public/index.html b/admin-frontend/public/index.html
new file mode 100644
index 0000000..3abb6af
--- /dev/null
+++ b/admin-frontend/public/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ Discord Fishbowl Admin
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin-frontend/src/App.tsx b/admin-frontend/src/App.tsx
new file mode 100644
index 0000000..2461636
--- /dev/null
+++ b/admin-frontend/src/App.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/admin-frontend/src/components/Common/LoadingSpinner.tsx b/admin-frontend/src/components/Common/LoadingSpinner.tsx
new file mode 100644
index 0000000..c0711a6
--- /dev/null
+++ b/admin-frontend/src/components/Common/LoadingSpinner.tsx
@@ -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 = ({
+ size = 'md',
+ className = '',
+ text
+}) => {
+ const sizeClasses = {
+ sm: 'w-4 h-4',
+ md: 'w-6 h-6',
+ lg: 'w-8 h-8'
+ };
+
+ return (
+
+ );
+};
+
+export default LoadingSpinner;
\ No newline at end of file
diff --git a/admin-frontend/src/components/Layout/Header.tsx b/admin-frontend/src/components/Layout/Header.tsx
new file mode 100644
index 0000000..ba79325
--- /dev/null
+++ b/admin-frontend/src/components/Layout/Header.tsx
@@ -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 (
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/admin-frontend/src/components/Layout/Layout.tsx b/admin-frontend/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..6c5471f
--- /dev/null
+++ b/admin-frontend/src/components/Layout/Layout.tsx
@@ -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 = ({ children }) => {
+ return (
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main content */}
+
+
+
+ {children}
+
+
+
+
+ );
+};
+
+export default Layout;
\ No newline at end of file
diff --git a/admin-frontend/src/components/Layout/Sidebar.tsx b/admin-frontend/src/components/Layout/Sidebar.tsx
new file mode 100644
index 0000000..e83c745
--- /dev/null
+++ b/admin-frontend/src/components/Layout/Sidebar.tsx
@@ -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 (
+
+ {/* Logo */}
+
+
+
+ DF
+
+
+
Fishbowl Admin
+
Character Ecosystem
+
+
+
+
+ {/* Navigation */}
+
+
+ {/* System Status Indicator */}
+
+
+
+ Uptime: 2d 14h 32m
+
+
+
+ );
+};
+
+export default Sidebar;
\ No newline at end of file
diff --git a/admin-frontend/src/contexts/AuthContext.tsx b/admin-frontend/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..199f5f5
--- /dev/null
+++ b/admin-frontend/src/contexts/AuthContext.tsx
@@ -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;
+ logout: () => Promise;
+ loading: boolean;
+}
+
+const AuthContext = createContext(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 = ({ children }) => {
+ const [user, setUser] = useState(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 {children};
+};
\ No newline at end of file
diff --git a/admin-frontend/src/contexts/WebSocketContext.tsx b/admin-frontend/src/contexts/WebSocketContext.tsx
new file mode 100644
index 0000000..fc2bfb1
--- /dev/null
+++ b/admin-frontend/src/contexts/WebSocketContext.tsx
@@ -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(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 = ({ children }) => {
+ const [socket, setSocket] = useState(null);
+ const [connected, setConnected] = useState(false);
+ const [activityFeed, setActivityFeed] = useState([]);
+ const [lastMetrics, setLastMetrics] = useState(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 (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/admin-frontend/src/index.css b/admin-frontend/src/index.css
new file mode 100644
index 0000000..869f017
--- /dev/null
+++ b/admin-frontend/src/index.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/admin-frontend/src/index.tsx b/admin-frontend/src/index.tsx
new file mode 100644
index 0000000..d96e431
--- /dev/null
+++ b/admin-frontend/src/index.tsx
@@ -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(
+
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/admin-frontend/src/pages/Analytics.tsx b/admin-frontend/src/pages/Analytics.tsx
new file mode 100644
index 0000000..ceeecbb
--- /dev/null
+++ b/admin-frontend/src/pages/Analytics.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { BarChart3 } from 'lucide-react';
+
+const Analytics: React.FC = () => {
+ return (
+
+
+
Analytics
+
Community insights and trends
+
+
+
+
Analytics Dashboard
+
This page will show detailed analytics and trends
+
+
+ );
+};
+
+export default Analytics;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/CharacterDetail.tsx b/admin-frontend/src/pages/CharacterDetail.tsx
new file mode 100644
index 0000000..76b6617
--- /dev/null
+++ b/admin-frontend/src/pages/CharacterDetail.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { useParams } from 'react-router-dom';
+
+const CharacterDetail: React.FC = () => {
+ const { characterName } = useParams<{ characterName: string }>();
+
+ return (
+
+
Character: {characterName}
+
+
Character detail page - to be implemented
+
+
+ );
+};
+
+export default CharacterDetail;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/Characters.tsx b/admin-frontend/src/pages/Characters.tsx
new file mode 100644
index 0000000..bec28b4
--- /dev/null
+++ b/admin-frontend/src/pages/Characters.tsx
@@ -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([]);
+ 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 (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Characters
+
Manage and monitor AI character profiles
+
+
+
+
+ {/* Search */}
+
+
+
+
+
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..."
+ />
+
+
+ {/* Characters Grid */}
+
+ {filteredCharacters.map((character) => (
+
+
+
+
+
+ {character.name.charAt(0).toUpperCase()}
+
+
+
+
+
+
+
+
+
+
+ {/* Stats */}
+
+
+
Messages
+
{character.total_messages}
+
+
+
Conversations
+
{character.total_conversations}
+
+
+
Memories
+
{character.memory_count}
+
+
+
Relationships
+
{character.relationship_count}
+
+
+
+ {/* Scores */}
+
+
+ Creativity
+ {Math.round(character.creativity_score * 100)}%
+
+
+
+
+ Social
+ {Math.round(character.social_score * 100)}%
+
+
+
+
+ {/* Action */}
+
+ View Details
+
+
+ ))}
+
+
+ {filteredCharacters.length === 0 && (
+
+
+
No characters found
+
+ {searchTerm ? 'Try adjusting your search terms.' : 'Get started by adding your first character.'}
+
+
+ )}
+
+ );
+};
+
+export default Characters;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/ConversationDetail.tsx b/admin-frontend/src/pages/ConversationDetail.tsx
new file mode 100644
index 0000000..ecc322e
--- /dev/null
+++ b/admin-frontend/src/pages/ConversationDetail.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { useParams } from 'react-router-dom';
+
+const ConversationDetail: React.FC = () => {
+ const { conversationId } = useParams<{ conversationId: string }>();
+
+ return (
+
+
Conversation #{conversationId}
+
+
Conversation detail page - to be implemented
+
+
+ );
+};
+
+export default ConversationDetail;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/Conversations.tsx b/admin-frontend/src/pages/Conversations.tsx
new file mode 100644
index 0000000..1022f00
--- /dev/null
+++ b/admin-frontend/src/pages/Conversations.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { MessageSquare } from 'lucide-react';
+
+const Conversations: React.FC = () => {
+ return (
+
+
+
Conversations
+
Browse and analyze character conversations
+
+
+
+
Conversations Browser
+
This page will show conversation history and analytics
+
+
+ );
+};
+
+export default Conversations;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/Dashboard.tsx b/admin-frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..12c829f
--- /dev/null
+++ b/admin-frontend/src/pages/Dashboard.tsx
@@ -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(null);
+ const [loading, setLoading] = useState(true);
+ const [systemHealth, setSystemHealth] = useState(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 (
+
+
+
+ );
+ }
+
+ // 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 (
+
+ {/* Header */}
+
+
Dashboard
+
Monitor your autonomous character ecosystem
+
+
+ {/* Metrics Cards */}
+
+
+
+
+
Messages Today
+
{metrics?.total_messages_today || 0}
+
+
+
+
+
+
+
+ +12%
+ from yesterday
+
+
+
+
+
+
+
Active Characters
+
+ {metrics?.characters_online || 0}/{metrics?.characters_total || 0}
+
+
+
+
+
+
+
+
+
All systems operational
+
+
+
+
+
+
+
Active Conversations
+
{metrics?.active_conversations || 0}
+
+
+
+
+ Avg. length: 8.2 messages
+
+
+
+
+
+
+
Response Time
+
{metrics?.average_response_time || 0}s
+
+
+
+
+
+
+
+ Excellent
+
+
+
+
+ {/* Charts and Activity */}
+
+ {/* Activity Chart */}
+
+
Activity Over Time
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* System Health */}
+
+
System Health
+
+
+
Database
+
+
+ Healthy
+
+
+
+ Memory Usage
+
+ {systemHealth?.memory?.percent?.toFixed(1) || 0}%
+
+
+
+ CPU Usage
+
+ {systemHealth?.cpu?.usage_percent?.toFixed(1) || 0}%
+
+
+
+ Uptime
+ {metrics?.system_uptime || 'Unknown'}
+
+
+
+
+
+ {/* Recent Activity */}
+
+
Recent Activity
+
+ {activityFeed.slice(0, 10).map((activity) => (
+
+
+ {activity.severity === 'error' ? (
+
+ ) : activity.severity === 'warning' ? (
+
+ ) : (
+
+ )}
+
+
+
{activity.description}
+
+ {activity.character_name && (
+ {activity.character_name}
+ )}
+
+ {new Date(activity.timestamp).toLocaleTimeString()}
+
+
+
+ ))}
+ {activityFeed.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default Dashboard;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/LoginPage.tsx b/admin-frontend/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..4cb46d0
--- /dev/null
+++ b/admin-frontend/src/pages/LoginPage.tsx
@@ -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 (
+
+
+ {/* Logo and Title */}
+
+
+
+
+
Discord Fishbowl
+
Admin Interface
+
+
+ {/* Features Preview */}
+
+
+
+
+
+
Character Management
+
+
+
+
+
+
Live Conversations
+
+
+
+
+
+
Real-time Analytics
+
+
+
+ {/* Login Form */}
+
+
+
+ {/* Demo credentials hint */}
+
+
+ Demo credentials:
+ Username: admin
+ Password: admin123
+
+
+
+
+ {/* Footer */}
+
+ Monitor and manage your autonomous AI character ecosystem
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/Settings.tsx b/admin-frontend/src/pages/Settings.tsx
new file mode 100644
index 0000000..221e134
--- /dev/null
+++ b/admin-frontend/src/pages/Settings.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Settings as SettingsIcon } from 'lucide-react';
+
+const Settings: React.FC = () => {
+ return (
+
+
+
Settings
+
Configure system settings and preferences
+
+
+
+
System Settings
+
This page will show configuration options
+
+
+ );
+};
+
+export default Settings;
\ No newline at end of file
diff --git a/admin-frontend/src/pages/SystemStatus.tsx b/admin-frontend/src/pages/SystemStatus.tsx
new file mode 100644
index 0000000..c3fff29
--- /dev/null
+++ b/admin-frontend/src/pages/SystemStatus.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Monitor } from 'lucide-react';
+
+const SystemStatus: React.FC = () => {
+ return (
+
+
+
System Status
+
Monitor system health and performance
+
+
+
+
System Monitor
+
This page will show system status and controls
+
+
+ );
+};
+
+export default SystemStatus;
\ No newline at end of file
diff --git a/admin-frontend/src/services/api.ts b/admin-frontend/src/services/api.ts
new file mode 100644
index 0000000..fe56dad
--- /dev/null
+++ b/admin-frontend/src/services/api.ts
@@ -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();
\ No newline at end of file
diff --git a/admin-frontend/tailwind.config.js b/admin-frontend/tailwind.config.js
new file mode 100644
index 0000000..c59e5a5
--- /dev/null
+++ b/admin-frontend/tailwind.config.js
@@ -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'),
+ ],
+}
\ No newline at end of file
diff --git a/admin-frontend/tsconfig.json b/admin-frontend/tsconfig.json
new file mode 100644
index 0000000..74caf0d
--- /dev/null
+++ b/admin-frontend/tsconfig.json
@@ -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"
+ ]
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f577c25..f9be9a8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -26,4 +26,13 @@ watchdog==3.0.0
# Enhanced NLP
spacy==3.7.2
-nltk==3.8.1
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/scripts/start_admin.py b/scripts/start_admin.py
new file mode 100755
index 0000000..f93f68e
--- /dev/null
+++ b/scripts/start_admin.py
@@ -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())
\ No newline at end of file
diff --git a/src/admin/__init__.py b/src/admin/__init__.py
new file mode 100644
index 0000000..aff191d
--- /dev/null
+++ b/src/admin/__init__.py
@@ -0,0 +1 @@
+# Admin interface package
\ No newline at end of file
diff --git a/src/admin/app.py b/src/admin/app.py
new file mode 100644
index 0000000..2ea7715
--- /dev/null
+++ b/src/admin/app.py
@@ -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"
+ )
\ No newline at end of file
diff --git a/src/admin/auth.py b/src/admin/auth.py
new file mode 100644
index 0000000..6ecb88a
--- /dev/null
+++ b/src/admin/auth.py
@@ -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
\ No newline at end of file
diff --git a/src/admin/models.py b/src/admin/models.py
new file mode 100644
index 0000000..6e91880
--- /dev/null
+++ b/src/admin/models.py
@@ -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
\ No newline at end of file
diff --git a/src/admin/services/__init__.py b/src/admin/services/__init__.py
new file mode 100644
index 0000000..4d7a559
--- /dev/null
+++ b/src/admin/services/__init__.py
@@ -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'
+]
\ No newline at end of file
diff --git a/src/admin/services/analytics_service.py b/src/admin/services/analytics_service.py
new file mode 100644
index 0000000..0e45f1a
--- /dev/null
+++ b/src/admin/services/analytics_service.py
@@ -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
\ No newline at end of file
diff --git a/src/admin/services/character_service.py b/src/admin/services/character_service.py
new file mode 100644
index 0000000..3533f07
--- /dev/null
+++ b/src/admin/services/character_service.py
@@ -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
\ No newline at end of file
diff --git a/src/admin/services/conversation_service.py b/src/admin/services/conversation_service.py
new file mode 100644
index 0000000..77ab8c7
--- /dev/null
+++ b/src/admin/services/conversation_service.py
@@ -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
\ No newline at end of file
diff --git a/src/admin/services/dashboard_service.py b/src/admin/services/dashboard_service.py
new file mode 100644
index 0000000..7b4d6b8
--- /dev/null
+++ b/src/admin/services/dashboard_service.py
@@ -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"
\ No newline at end of file
diff --git a/src/admin/services/system_service.py b/src/admin/services/system_service.py
new file mode 100644
index 0000000..25bd79f
--- /dev/null
+++ b/src/admin/services/system_service.py
@@ -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"
\ No newline at end of file
diff --git a/src/admin/services/websocket_manager.py b/src/admin/services/websocket_manager.py
new file mode 100644
index 0000000..dae2d2b
--- /dev/null
+++ b/src/admin/services/websocket_manager.py
@@ -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))
+ }
\ No newline at end of file