Remove AI cog from the list of cogs to skip during startup

This commit is contained in:
Slipstream 2025-06-05 23:31:59 -06:00
parent 6ffbf87368
commit 870cd6e4a0
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
2 changed files with 657 additions and 1 deletions

657
discord_bot_sync_api.py Normal file
View File

@ -0,0 +1,657 @@
import os
import json
import asyncio
import datetime
from typing import Dict, List, Optional, Any, Union
from fastapi import FastAPI, HTTPException, Depends, Header, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles # Added for static files
from fastapi.responses import FileResponse # Added for serving HTML
from pydantic import BaseModel, Field
import discord
from discord.ext import commands
import aiohttp
import threading
from typing import Optional # Added for GurtCog type hint
# This file contains the API endpoints for syncing conversations between
# the Flutter app and the Discord bot, AND the Gurt stats endpoint.
# --- Placeholder for GurtCog instance and bot instance ---
# These need to be set by the script that starts the bot and API server
# Import GurtCog and ModLogCog conditionally to avoid dependency issues
try:
from gurt.cog import GurtCog # Import GurtCog for type hint and access
from cogs.mod_log_cog import ModLogCog # Import ModLogCog for type hint
gurt_cog_instance: Optional[GurtCog] = None
mod_log_cog_instance: Optional[ModLogCog] = None # Placeholder for ModLogCog
except ImportError as e:
print(f"Warning: Could not import GurtCog or ModLogCog: {e}")
# Use Any type as fallback
from typing import Any
gurt_cog_instance: Optional[Any] = None
mod_log_cog_instance: Optional[Any] = None
bot_instance = None # Will be set to the Discord bot instance
# ============= Models =============
class SyncedMessage(BaseModel):
content: str
role: str # "user", "assistant", or "system"
timestamp: datetime.datetime
reasoning: Optional[str] = None
usage_data: Optional[Dict[str, Any]] = None
class UserSettings(BaseModel):
# General settings
model_id: str = "openai/gpt-3.5-turbo"
temperature: float = 0.7
max_tokens: int = 1000
# Reasoning settings
reasoning_enabled: bool = False
reasoning_effort: str = "medium" # "low", "medium", "high"
# Web search settings
web_search_enabled: bool = False
# System message
system_message: Optional[str] = None
# Character settings
character: Optional[str] = None
character_info: Optional[str] = None
character_breakdown: bool = False
custom_instructions: Optional[str] = None
# UI settings
advanced_view_enabled: bool = False
streaming_enabled: bool = True
# Last updated timestamp
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)
sync_source: str = "discord" # "discord" or "flutter"
class SyncedConversation(BaseModel):
id: str
title: str
messages: List[SyncedMessage]
created_at: datetime.datetime
updated_at: datetime.datetime
model_id: str
sync_source: str = "discord" # "discord" or "flutter"
last_synced_at: Optional[datetime.datetime] = None
# Conversation-specific settings
reasoning_enabled: bool = False
reasoning_effort: str = "medium" # "low", "medium", "high"
temperature: float = 0.7
max_tokens: int = 1000
web_search_enabled: bool = False
system_message: Optional[str] = None
# Character-related settings
character: Optional[str] = None
character_info: Optional[str] = None
character_breakdown: bool = False
custom_instructions: Optional[str] = None
class SyncRequest(BaseModel):
conversations: List[SyncedConversation]
last_sync_time: Optional[datetime.datetime] = None
user_settings: Optional[UserSettings] = None
class SettingsSyncRequest(BaseModel):
user_settings: UserSettings
class SyncResponse(BaseModel):
success: bool
message: str
conversations: List[SyncedConversation] = []
user_settings: Optional[UserSettings] = None
# ============= Storage =============
# Files to store synced data
SYNC_DATA_FILE = "data/synced_conversations.json"
USER_SETTINGS_FILE = "data/synced_user_settings.json"
# Create data directory if it doesn't exist
os.makedirs(os.path.dirname(SYNC_DATA_FILE), exist_ok=True)
# In-memory storage for conversations and settings
user_conversations: Dict[str, List[SyncedConversation]] = {}
user_settings: Dict[str, UserSettings] = {}
# Load conversations from file
def load_conversations():
global user_conversations
if os.path.exists(SYNC_DATA_FILE):
try:
with open(SYNC_DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys (user IDs) back to strings
user_conversations = {
k: [SyncedConversation.model_validate(conv) for conv in v]
for k, v in data.items()
}
print(f"Loaded synced conversations for {len(user_conversations)} users")
except Exception as e:
print(f"Error loading synced conversations: {e}")
user_conversations = {}
# Save conversations to file
def save_conversations():
try:
# Convert to JSON-serializable format
serializable_data = {
user_id: [conv.model_dump() for conv in convs]
for user_id, convs in user_conversations.items()
}
with open(SYNC_DATA_FILE, "w", encoding="utf-8") as f:
json.dump(serializable_data, f, indent=2, default=str, ensure_ascii=False)
except Exception as e:
print(f"Error saving synced conversations: {e}")
# Load user settings from file
def load_user_settings():
global user_settings
if os.path.exists(USER_SETTINGS_FILE):
try:
with open(USER_SETTINGS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys (user IDs) back to strings
user_settings = {
k: UserSettings.model_validate(v) for k, v in data.items()
}
print(f"Loaded synced settings for {len(user_settings)} users")
except Exception as e:
print(f"Error loading synced user settings: {e}")
user_settings = {}
# Save user settings to file
def save_all_user_settings():
try:
# Convert to JSON-serializable format
serializable_data = {
user_id: settings.model_dump()
for user_id, settings in user_settings.items()
}
with open(USER_SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(serializable_data, f, indent=2, default=str, ensure_ascii=False)
except Exception as e:
print(f"Error saving synced user settings: {e}")
# ============= Discord OAuth Verification =============
async def verify_discord_token(authorization: str = Header(None)) -> str:
"""Verify the Discord token and return the user ID"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header missing")
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization format")
token = authorization.replace("Bearer ", "")
# Verify the token with Discord
async with aiohttp.ClientSession() as session:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(
"https://discord.com/api/v10/users/@me", headers=headers
) as resp:
if resp.status != 200:
raise HTTPException(status_code=401, detail="Invalid Discord token")
user_data = await resp.json()
return user_data["id"]
# ============= API Setup =============
# API Configuration
API_BASE_PATH = "/discordapi" # Base path for the API
SSL_CERT_FILE = "/etc/letsencrypt/live/slipstreamm.dev/fullchain.pem"
SSL_KEY_FILE = "/etc/letsencrypt/live/slipstreamm.dev/privkey.pem"
# Create the main FastAPI app
app = FastAPI(title="Discord Bot Sync API")
# Create a sub-application for the API
api_app = FastAPI(
title="Discord Bot Sync API", docs_url="/docs", openapi_url="/openapi.json"
)
# Mount the API app at the base path
app.mount(API_BASE_PATH, api_app)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Adjust this in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Also add CORS to the API app
api_app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Adjust this in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize by loading saved data
@app.on_event("startup")
async def startup_event():
load_conversations()
load_user_settings()
# AI Cog related settings merge has been removed.
pass
# ============= API Endpoints =============
@app.get(API_BASE_PATH + "/")
async def root():
return {"message": "Discord Bot Sync API is running"}
@api_app.get("/")
async def api_root():
return {"message": "Discord Bot Sync API is running"}
@api_app.get("/auth")
async def auth(code: str, state: str = None):
"""Handle OAuth callback"""
return {"message": "Authentication successful", "code": code, "state": state}
@api_app.get("/conversations")
async def get_conversations(user_id: str = Depends(verify_discord_token)):
"""Get all conversations for a user"""
if user_id not in user_conversations:
return {"conversations": []}
return {"conversations": user_conversations[user_id]}
@api_app.post("/sync")
async def sync_conversations(
sync_request: SyncRequest, user_id: str = Depends(verify_discord_token)
):
"""Sync conversations between the Flutter app and Discord bot"""
# Get existing conversations for this user
existing_conversations = user_conversations.get(user_id, [])
# Process incoming conversations
updated_conversations = []
for incoming_conv in sync_request.conversations:
# Check if this conversation already exists
existing_conv = next(
(conv for conv in existing_conversations if conv.id == incoming_conv.id),
None,
)
if existing_conv:
# If the incoming conversation is newer, update it
if incoming_conv.updated_at > existing_conv.updated_at:
# Replace the existing conversation
existing_conversations = [
conv
for conv in existing_conversations
if conv.id != incoming_conv.id
]
existing_conversations.append(incoming_conv)
updated_conversations.append(incoming_conv)
else:
# This is a new conversation, add it
existing_conversations.append(incoming_conv)
updated_conversations.append(incoming_conv)
# Update the storage
user_conversations[user_id] = existing_conversations
save_conversations()
# Process user settings if provided
user_settings_response = None
if sync_request.user_settings:
incoming_settings = sync_request.user_settings
existing_settings = user_settings.get(user_id)
# If we have existing settings, check which is newer
if existing_settings:
if (
not existing_settings.last_updated
or incoming_settings.last_updated > existing_settings.last_updated
):
user_settings[user_id] = incoming_settings
save_all_user_settings()
user_settings_response = incoming_settings
else:
user_settings_response = existing_settings
else:
# No existing settings, just save the incoming ones
user_settings[user_id] = incoming_settings
save_all_user_settings()
user_settings_response = incoming_settings
return SyncResponse(
success=True,
message=f"Synced {len(updated_conversations)} conversations",
conversations=existing_conversations,
user_settings=user_settings_response,
)
@api_app.delete("/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str, user_id: str = Depends(verify_discord_token)
):
"""Delete a conversation"""
if user_id not in user_conversations:
raise HTTPException(
status_code=404, detail="No conversations found for this user"
)
# Filter out the conversation to delete
original_count = len(user_conversations[user_id])
user_conversations[user_id] = [
conv for conv in user_conversations[user_id] if conv.id != conversation_id
]
# Check if any conversation was deleted
if len(user_conversations[user_id]) == original_count:
raise HTTPException(status_code=404, detail="Conversation not found")
save_conversations()
return {"success": True, "message": "Conversation deleted"}
# --- Gurt Stats Endpoint ---
@api_app.get("/gurt/stats")
async def get_gurt_stats_api():
"""Get internal statistics for the Gurt bot."""
if not gurt_cog_instance:
raise HTTPException(status_code=503, detail="Gurt cog not available")
try:
stats_data = await gurt_cog_instance.get_gurt_stats()
# Convert potential datetime objects if any (though get_gurt_stats should return serializable types)
# For safety, let's ensure basic types or handle conversion if needed later.
return stats_data
except Exception as e:
print(f"Error retrieving Gurt stats via API: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error retrieving Gurt stats: {e}")
# --- Gurt Dashboard Static Files ---
# Mount static files directory (adjust path if needed, assuming dashboard files are in discordbot/gurt_dashboard)
# Check if the directory exists before mounting
dashboard_dir = "discordbot/gurt_dashboard"
if os.path.exists(dashboard_dir) and os.path.isdir(dashboard_dir):
api_app.mount(
"/gurt/static", StaticFiles(directory=dashboard_dir), name="gurt_static"
)
print(f"Mounted Gurt dashboard static files from: {dashboard_dir}")
# Route for the main dashboard HTML
@api_app.get("/gurt/dashboard", response_class=FileResponse)
async def get_gurt_dashboard():
dashboard_html_path = os.path.join(dashboard_dir, "index.html")
if os.path.exists(dashboard_html_path):
return dashboard_html_path
else:
raise HTTPException(
status_code=404, detail="Dashboard index.html not found"
)
else:
print(
f"Warning: Gurt dashboard directory '{dashboard_dir}' not found. Dashboard endpoints will not be available."
)
@api_app.get("/settings")
async def get_user_settings(user_id: str = Depends(verify_discord_token)):
"""Get user settings"""
# AI Cog related settings merge has been removed.
if user_id not in user_settings:
# Create default settings if none exist
user_settings[user_id] = UserSettings()
save_all_user_settings()
return {"settings": user_settings[user_id]}
@api_app.post("/settings")
async def update_user_settings(
settings_request: SettingsSyncRequest, user_id: str = Depends(verify_discord_token)
):
"""Update user settings"""
incoming_settings = settings_request.user_settings
existing_settings = user_settings.get(user_id)
# Debug logging for character settings
print(f"Received settings update from user {user_id}:")
print(f"Character: {incoming_settings.character}")
print(f"Character Info: {incoming_settings.character_info}")
print(f"Character Breakdown: {incoming_settings.character_breakdown}")
print(f"Custom Instructions: {incoming_settings.custom_instructions}")
print(f"Last Updated: {incoming_settings.last_updated}")
print(f"Sync Source: {incoming_settings.sync_source}")
if existing_settings:
print(f"Existing settings for user {user_id}:")
print(f"Character: {existing_settings.character}")
print(f"Character Info: {existing_settings.character_info}")
print(f"Last Updated: {existing_settings.last_updated}")
print(f"Sync Source: {existing_settings.sync_source}")
# If we have existing settings, check which is newer
if existing_settings:
if (
not existing_settings.last_updated
or incoming_settings.last_updated > existing_settings.last_updated
):
print(f"Updating settings for user {user_id} (incoming settings are newer)")
user_settings[user_id] = incoming_settings
save_all_user_settings()
else:
# Return existing settings if they're newer
print(
f"Not updating settings for user {user_id} (existing settings are newer)"
)
return {
"success": True,
"message": "Existing settings are newer",
"settings": existing_settings,
}
else:
# No existing settings, just save the incoming ones
print(f"Creating new settings for user {user_id}")
user_settings[user_id] = incoming_settings
save_all_user_settings()
# Verify the settings were saved correctly
saved_settings = user_settings.get(user_id)
print(f"Saved settings for user {user_id}:")
print(f"Character: {saved_settings.character}")
print(f"Character Info: {saved_settings.character_info}")
print(f"Character Breakdown: {saved_settings.character_breakdown}")
print(f"Custom Instructions: {saved_settings.custom_instructions}")
# AI Cog related settings update has been removed.
return {
"success": True,
"message": "Settings updated",
"settings": user_settings[user_id],
}
# ============= Discord Bot Integration =============
# This function should be called from your Discord bot's AI cog
# to convert AI conversation history to the synced format
def convert_ai_history_to_synced(
user_id: str, conversation_history: Dict[int, List[Dict[str, Any]]]
):
"""Convert the AI conversation history to the synced format"""
synced_conversations = []
# Process each conversation in the history
for discord_user_id, messages in conversation_history.items():
if str(discord_user_id) != user_id:
continue
# Create a unique ID for this conversation
conv_id = f"discord_{discord_user_id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
# Convert messages to the synced format
synced_messages = []
for msg in messages:
role = msg.get("role", "")
if role not in ["user", "assistant", "system"]:
continue
synced_messages.append(
SyncedMessage(
content=msg.get("content", ""),
role=role,
timestamp=datetime.datetime.now(), # Use current time as we don't have the original timestamp
reasoning=None, # Discord bot doesn't store reasoning
usage_data=None, # Discord bot doesn't store usage data
)
)
# Create the synced conversation
synced_conversations.append(
SyncedConversation(
id=conv_id,
title="Discord Conversation", # Default title
messages=synced_messages,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now(),
model_id="openai/gpt-3.5-turbo", # Default model
sync_source="discord",
last_synced_at=datetime.datetime.now(),
reasoning_enabled=False,
reasoning_effort="medium",
temperature=0.7,
max_tokens=1000,
web_search_enabled=False,
system_message=None,
character=None,
character_info=None,
character_breakdown=False,
custom_instructions=None,
)
)
return synced_conversations
# This function should be called from your Discord bot's AI cog
# to save a new conversation from Discord
def save_discord_conversation(
user_id: str,
messages: List[Dict[str, Any]],
model_id: str = "openai/gpt-3.5-turbo",
conversation_id: Optional[str] = None,
title: str = "Discord Conversation",
reasoning_enabled: bool = False,
reasoning_effort: str = "medium",
temperature: float = 0.7,
max_tokens: int = 1000,
web_search_enabled: bool = False,
system_message: Optional[str] = None,
character: Optional[str] = None,
character_info: Optional[str] = None,
character_breakdown: bool = False,
custom_instructions: Optional[str] = None,
):
"""Save a conversation from Discord to the synced storage"""
# Convert messages to the synced format
synced_messages = []
for msg in messages:
role = msg.get("role", "")
if role not in ["user", "assistant", "system"]:
continue
synced_messages.append(
SyncedMessage(
content=msg.get("content", ""),
role=role,
timestamp=datetime.datetime.now(),
reasoning=msg.get("reasoning"),
usage_data=msg.get("usage_data"),
)
)
# Create a unique ID for this conversation if not provided
if not conversation_id:
conversation_id = (
f"discord_{user_id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
)
# Create the synced conversation
synced_conv = SyncedConversation(
id=conversation_id,
title=title,
messages=synced_messages,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now(),
model_id=model_id,
sync_source="discord",
last_synced_at=datetime.datetime.now(),
reasoning_enabled=reasoning_enabled,
reasoning_effort=reasoning_effort,
temperature=temperature,
max_tokens=max_tokens,
web_search_enabled=web_search_enabled,
system_message=system_message,
character=character,
character_info=character_info,
character_breakdown=character_breakdown,
custom_instructions=custom_instructions,
)
# Add to storage
if user_id not in user_conversations:
user_conversations[user_id] = []
# Check if we're updating an existing conversation
if conversation_id:
# Remove the old conversation with the same ID if it exists
user_conversations[user_id] = [
conv for conv in user_conversations[user_id] if conv.id != conversation_id
]
user_conversations[user_id].append(synced_conv)
save_conversations()
return synced_conv

View File

@ -779,7 +779,6 @@ async def main(args): # Pass parsed args
if args.disable_ai:
print("AI functionality disabled via command line flag.")
ai_cogs_to_skip = [
"cogs.ai_cog",
"cogs.multi_conversation_ai_cog",
# Add any other AI-related cogs from the 'cogs' folder here
]