Implement Bipolar Mode: Dual persona arguments with webhooks, LLM arbiter, and persistent scoreboard
Major Features: - Complete Bipolar Mode system allowing Regular Miku and Evil Miku to coexist and argue via webhooks - LLM arbiter system using neutral model to judge argument winners with detailed reasoning - Persistent scoreboard tracking wins, percentages, and last 50 results with timestamps and reasoning - Automatic mode switching based on argument winner - Webhook management per channel with profile pictures and display names - Progressive probability system for dynamic argument lengths (starts at 10%, increases 5% per exchange, min 4 exchanges) - Draw handling with penalty system (-5% end chance, continues argument) - Integration with autonomous system for random argument triggers Argument System: - MIN_EXCHANGES = 4, progressive end chance starting at 10% - Enhanced prompts for both personas (strategic, short, punchy responses 1-3 sentences) - Evil Miku triumphant victory messages with gloating and satisfaction - Regular Miku assertive defense (not passive, shows backbone) - Message-based argument starting (can respond to specific messages via ID) - Conversation history tracking per argument with special user_id - Full context queries (personality, lore, lyrics, last 8 messages) LLM Arbiter: - Decisive prompt emphasizing picking winners (draws should be rare) - Improved parsing with first-line exact matching and fallback counting - Debug logging for decision transparency - Arbiter reasoning stored in scoreboard history for review - Uses neutral TEXT_MODEL (not evil) for unbiased judgment Web UI & API: - Bipolar mode toggle button (only visible when evil mode is on) - Channel ID + Message ID input fields for argument triggering - Scoreboard display with win percentages and recent history - Manual argument trigger endpoint with string-based IDs - GET /bipolar-mode/scoreboard endpoint for stats retrieval - Real-time active arguments tracking (refreshes every 5 seconds) Prompt Optimizations: - All argument prompts limited to 1-3 sentences for impact - Evil Miku system prompt with variable response length guidelines - Removed walls of text, emphasizing brevity and precision - "Sometimes the cruelest response is the shortest one" Evil Miku Updates: - Added height to lore (15.8m tall, 10x bigger than regular Miku) - Height added to prompt facts for size-based belittling - More strategic and calculating personality in arguments Integration: - Bipolar mode state restoration on bot startup - Bot skips processing messages during active arguments - Autonomous system checks for bipolar triggers after actions - Import fixes (apply_evil_mode_changes/revert_evil_mode_changes) Technical Details: - State persistence via JSON (bipolar_mode_state.json, bipolar_webhooks.json, bipolar_scoreboard.json) - Webhook caching per guild with fallback creation - Event loop management with asyncio.create_task - Rate limiting and argument conflict prevention - Globals integration (BIPOLAR_MODE, BIPOLAR_WEBHOOKS, BIPOLAR_ARGUMENT_IN_PROGRESS, MOOD_EMOJIS) Files Changed: - bot/bot.py: Added bipolar mode restoration and argument-in-progress checks - bot/globals.py: Added bipolar mode state variables and mood emoji mappings - bot/utils/bipolar_mode.py: Complete 1106-line implementation - bot/utils/autonomous.py: Added bipolar argument trigger checks - bot/utils/evil_mode.py: Updated system prompt, added height info to lore/prompt - bot/api.py: Added bipolar mode endpoints (trigger, toggle, scoreboard) - bot/static/index.html: Added bipolar controls section with scoreboard - bot/memory/: Various DM conversation updates - bot/evil_miku_lore.txt: Added height description - bot/evil_miku_prompt.txt: Added height to facts, updated personality guidelines
This commit is contained in:
176
bot/api.py
176
bot/api.py
@@ -229,6 +229,182 @@ def set_evil_mood_endpoint(data: EvilMoodSetRequest):
|
||||
|
||||
return {"status": "error", "message": "Failed to set evil mood"}
|
||||
|
||||
# ========== Bipolar Mode Management ==========
|
||||
class BipolarTriggerRequest(BaseModel):
|
||||
channel_id: str # String to handle large Discord IDs from JS
|
||||
message_id: str = None # Optional: starting message ID (string)
|
||||
context: str = ""
|
||||
|
||||
@app.get("/bipolar-mode")
|
||||
def get_bipolar_mode_status():
|
||||
"""Get current bipolar mode status"""
|
||||
from utils.bipolar_mode import is_bipolar_mode, is_argument_in_progress
|
||||
|
||||
# Get any active arguments
|
||||
active_arguments = {}
|
||||
for channel_id, data in globals.BIPOLAR_ARGUMENT_IN_PROGRESS.items():
|
||||
if data.get("active"):
|
||||
active_arguments[channel_id] = data
|
||||
|
||||
return {
|
||||
"bipolar_mode": is_bipolar_mode(),
|
||||
"evil_mode": globals.EVIL_MODE,
|
||||
"active_arguments": active_arguments,
|
||||
"webhooks_configured": len(globals.BIPOLAR_WEBHOOKS)
|
||||
}
|
||||
|
||||
@app.post("/bipolar-mode/enable")
|
||||
def enable_bipolar_mode():
|
||||
"""Enable bipolar mode"""
|
||||
from utils.bipolar_mode import enable_bipolar_mode as _enable
|
||||
|
||||
if globals.BIPOLAR_MODE:
|
||||
return {"status": "ok", "message": "Bipolar mode is already enabled", "bipolar_mode": True}
|
||||
|
||||
_enable()
|
||||
return {"status": "ok", "message": "Bipolar mode enabled", "bipolar_mode": True}
|
||||
|
||||
@app.post("/bipolar-mode/disable")
|
||||
def disable_bipolar_mode():
|
||||
"""Disable bipolar mode"""
|
||||
from utils.bipolar_mode import disable_bipolar_mode as _disable, cleanup_webhooks
|
||||
|
||||
if not globals.BIPOLAR_MODE:
|
||||
return {"status": "ok", "message": "Bipolar mode is already disabled", "bipolar_mode": False}
|
||||
|
||||
_disable()
|
||||
|
||||
# Optionally cleanup webhooks in background
|
||||
if globals.client and globals.client.loop and globals.client.loop.is_running():
|
||||
globals.client.loop.create_task(cleanup_webhooks(globals.client))
|
||||
|
||||
return {"status": "ok", "message": "Bipolar mode disabled", "bipolar_mode": False}
|
||||
|
||||
@app.post("/bipolar-mode/toggle")
|
||||
def toggle_bipolar_mode():
|
||||
"""Toggle bipolar mode on/off"""
|
||||
from utils.bipolar_mode import toggle_bipolar_mode as _toggle, cleanup_webhooks
|
||||
|
||||
new_state = _toggle()
|
||||
|
||||
# If disabled, cleanup webhooks
|
||||
if not new_state:
|
||||
if globals.client and globals.client.loop and globals.client.loop.is_running():
|
||||
globals.client.loop.create_task(cleanup_webhooks(globals.client))
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": f"Bipolar mode {'enabled' if new_state else 'disabled'}",
|
||||
"bipolar_mode": new_state
|
||||
}
|
||||
|
||||
@app.post("/bipolar-mode/trigger-argument")
|
||||
def trigger_argument(data: BipolarTriggerRequest):
|
||||
"""Manually trigger an argument in a specific channel
|
||||
|
||||
If message_id is provided, the argument will start from that message.
|
||||
The opposite persona will respond to it.
|
||||
"""
|
||||
from utils.bipolar_mode import force_trigger_argument, force_trigger_argument_from_message_id, is_bipolar_mode, is_argument_in_progress
|
||||
|
||||
# Parse IDs from strings
|
||||
try:
|
||||
channel_id = int(data.channel_id)
|
||||
except ValueError:
|
||||
return {"status": "error", "message": "Invalid channel ID format"}
|
||||
|
||||
message_id = None
|
||||
if data.message_id:
|
||||
try:
|
||||
message_id = int(data.message_id)
|
||||
except ValueError:
|
||||
return {"status": "error", "message": "Invalid message ID format"}
|
||||
|
||||
if not is_bipolar_mode():
|
||||
return {"status": "error", "message": "Bipolar mode is not enabled"}
|
||||
|
||||
if is_argument_in_progress(channel_id):
|
||||
return {"status": "error", "message": "An argument is already in progress in this channel"}
|
||||
|
||||
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
|
||||
return {"status": "error", "message": "Discord client not ready"}
|
||||
|
||||
# If message_id is provided, use the message-based trigger
|
||||
if message_id:
|
||||
import asyncio
|
||||
|
||||
async def trigger_from_message():
|
||||
success, error = await force_trigger_argument_from_message_id(
|
||||
channel_id, message_id, globals.client, data.context
|
||||
)
|
||||
if not success:
|
||||
print(f"⚠️ Failed to trigger argument from message: {error}")
|
||||
|
||||
globals.client.loop.create_task(trigger_from_message())
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": f"Argument triggered from message {message_id}",
|
||||
"channel_id": channel_id,
|
||||
"message_id": message_id
|
||||
}
|
||||
|
||||
# Otherwise, find the channel and trigger normally
|
||||
channel = globals.client.get_channel(channel_id)
|
||||
if not channel:
|
||||
return {"status": "error", "message": f"Channel {channel_id} not found"}
|
||||
|
||||
# Trigger the argument
|
||||
globals.client.loop.create_task(force_trigger_argument(channel, globals.client, data.context))
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": f"Argument triggered in #{channel.name}",
|
||||
"channel_id": channel_id
|
||||
}
|
||||
|
||||
@app.get("/bipolar-mode/scoreboard")
|
||||
def get_bipolar_scoreboard():
|
||||
"""Get the bipolar mode argument scoreboard"""
|
||||
from utils.bipolar_mode import load_scoreboard, get_scoreboard_summary
|
||||
|
||||
scoreboard = load_scoreboard()
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"scoreboard": {
|
||||
"miku_wins": scoreboard.get("miku", 0),
|
||||
"evil_wins": scoreboard.get("evil", 0),
|
||||
"total_arguments": scoreboard.get("miku", 0) + scoreboard.get("evil", 0),
|
||||
"history": scoreboard.get("history", [])[-10:] # Last 10 results
|
||||
},
|
||||
"summary": get_scoreboard_summary()
|
||||
}
|
||||
|
||||
@app.post("/bipolar-mode/cleanup-webhooks")
|
||||
def cleanup_bipolar_webhooks():
|
||||
"""Cleanup all bipolar webhooks from all servers"""
|
||||
from utils.bipolar_mode import cleanup_webhooks
|
||||
|
||||
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
|
||||
return {"status": "error", "message": "Discord client not ready"}
|
||||
|
||||
globals.client.loop.create_task(cleanup_webhooks(globals.client))
|
||||
return {"status": "ok", "message": "Webhook cleanup started"}
|
||||
|
||||
@app.get("/bipolar-mode/arguments")
|
||||
def get_active_arguments():
|
||||
"""Get all active arguments"""
|
||||
active = {}
|
||||
for channel_id, data in globals.BIPOLAR_ARGUMENT_IN_PROGRESS.items():
|
||||
if data.get("active"):
|
||||
channel = globals.client.get_channel(channel_id) if globals.client else None
|
||||
active[channel_id] = {
|
||||
**data,
|
||||
"channel_name": channel.name if channel else "Unknown"
|
||||
}
|
||||
return {"active_arguments": active}
|
||||
|
||||
# ========== Per-Server Mood Management ==========
|
||||
@app.get("/servers/{guild_id}/mood")
|
||||
def get_server_mood(guild_id: int):
|
||||
|
||||
Reference in New Issue
Block a user