Compare commits

...

2 Commits

Author SHA1 Message Date
7d5881ebe7 fix: inject argument topic into EVERY exchange, not just the first message
The topic was only being injected into the initial breakthrough message via
get_argument_start_prompt(). After that, every subsequent exchange called
get_miku_argument_prompt() / get_evil_argument_prompt() which had no concept
of the topic — so both personas forgot what they were arguing about after the
first exchange and reverted to generic identity-crisis arguments.

Fix: added argument_topic parameter to both persona prompt functions and inject
it as a bold ARGUMENT THEME reminder in every single exchange. The topic block
explicitly tells the LLM to stay on-topic and not drift into generic territory.
2026-04-30 12:57:48 +03:00
e6c818f647 fix: merge context + topic into single field — one clear purpose
- Removed separate 'topic' field from BipolarTriggerRequest model
- Removed topic parameter from force_trigger_argument, force_trigger_argument_from_message_id, and run_argument
- trigger_context now doubles as the argument theme: if provided by user, it becomes the topic;
  if blank, a random topic is selected from the rotation pool
- Web UI: replaced two confusing fields (Context + Topic) with one clear field labeled
  'What should they argue about? (optional)' with a plain-English description
- JS: removed topic field reference, context.trim() ensures empty strings aren't sent
2026-04-30 12:30:49 +03:00
5 changed files with 47 additions and 47 deletions

View File

@@ -129,7 +129,7 @@ def trigger_argument(data: BipolarTriggerRequest):
if message_id: if message_id:
async def trigger_from_message(): async def trigger_from_message():
success, error = await force_trigger_argument_from_message_id( success, error = await force_trigger_argument_from_message_id(
channel_id, message_id, globals.client, data.context, topic=data.topic channel_id, message_id, globals.client, data.context
) )
if not success: if not success:
logger.error(f"Failed to trigger argument from message: {error}") logger.error(f"Failed to trigger argument from message: {error}")
@@ -148,8 +148,8 @@ def trigger_argument(data: BipolarTriggerRequest):
if not channel: if not channel:
return JSONResponse(status_code=404, content={"status": "error", "message": f"Channel {channel_id} not found"}) return JSONResponse(status_code=404, content={"status": "error", "message": f"Channel {channel_id} not found"})
# Trigger the argument with optional custom topic # Trigger the argument — context doubles as the argument theme
globals.client.loop.create_task(force_trigger_argument(channel, globals.client, data.context, topic=data.topic)) globals.client.loop.create_task(force_trigger_argument(channel, globals.client, data.context))
return { return {
"status": "ok", "status": "ok",

View File

@@ -45,8 +45,7 @@ class LogFilterUpdateRequest(BaseModel):
class BipolarTriggerRequest(BaseModel): class BipolarTriggerRequest(BaseModel):
channel_id: str # String to handle large Discord IDs from JS channel_id: str # String to handle large Discord IDs from JS
message_id: str = None # Optional: starting message ID (string) message_id: str = None # Optional: starting message ID (string)
context: str = "" context: str = "" # Optional: argument theme/context — tells them what to argue about
topic: str = "" # Optional: custom argument topic (overrides random topic selection)
class ManualCropRequest(BaseModel): class ManualCropRequest(BaseModel):

View File

@@ -234,15 +234,10 @@
</div> </div>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="bipolar-context">Argument Context (optional):</label> <label for="bipolar-context">What should they argue about? (optional):</label>
<input type="text" id="bipolar-context" placeholder="e.g., They're fighting about who's the real Miku..." style="width: 100%; margin-top: 0.3rem;"> <input type="text" id="bipolar-context" placeholder="e.g., Who's the real Miku? Whether kindness is weakness. A petty grudge..." style="width: 100%; margin-top: 0.3rem;">
</div>
<div style="margin-bottom: 1rem;">
<label for="bipolar-topic">Argument Topic (optional — overrides random topic):</label>
<input type="text" id="bipolar-topic" placeholder="e.g., Who deserves the spotlight? A philosophical debate about worth..." style="width: 100%; margin-top: 0.3rem;">
<div style="font-size: 0.75rem; color: #777; margin-top: 0.2rem;"> <div style="font-size: 0.75rem; color: #777; margin-top: 0.2rem;">
Leave blank for random topic selection. Enter a custom theme to frame the argument uniquely. Leave blank for a random topic. Write anything to set the argument's theme.
</div> </div>
</div> </div>

View File

@@ -248,8 +248,7 @@ async function triggerPersonaDialogue() {
async function triggerBipolarArgument() { async function triggerBipolarArgument() {
const channelIdInput = document.getElementById('bipolar-channel-id').value.trim(); const channelIdInput = document.getElementById('bipolar-channel-id').value.trim();
const messageIdInput = document.getElementById('bipolar-message-id').value.trim(); const messageIdInput = document.getElementById('bipolar-message-id').value.trim();
const context = document.getElementById('bipolar-context').value; const context = document.getElementById('bipolar-context').value.trim();
const topic = document.getElementById('bipolar-topic').value.trim();
const statusDiv = document.getElementById('bipolar-status'); const statusDiv = document.getElementById('bipolar-status');
if (!channelIdInput) { if (!channelIdInput) {
@@ -279,10 +278,6 @@ async function triggerBipolarArgument() {
requestBody.message_id = messageIdInput; requestBody.message_id = messageIdInput;
} }
if (topic) {
requestBody.topic = topic;
}
const result = await apiCall('/bipolar-mode/trigger-argument', 'POST', requestBody); const result = await apiCall('/bipolar-mode/trigger-argument', 'POST', requestBody);
if (result.status === 'error') { if (result.status === 'error') {
@@ -295,7 +290,6 @@ async function triggerBipolarArgument() {
showNotification(`⚔️ Argument triggered!`); showNotification(`⚔️ Argument triggered!`);
document.getElementById('bipolar-context').value = ''; document.getElementById('bipolar-context').value = '';
document.getElementById('bipolar-topic').value = '';
document.getElementById('bipolar-message-id').value = ''; document.getElementById('bipolar-message-id').value = '';
loadActiveArguments(); loadActiveArguments();

View File

@@ -764,7 +764,7 @@ def _get_mood_argument_guidance(persona: str) -> str:
return "" return ""
def get_miku_argument_prompt(evil_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "") -> str: def get_miku_argument_prompt(evil_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "", argument_topic: str = "") -> str:
"""Get prompt for Regular Miku to respond in an argument""" """Get prompt for Regular Miku to respond in an argument"""
if is_first_response: if is_first_response:
message_context = f"""You just noticed something Evil Miku said in the chat: message_context = f"""You just noticed something Evil Miku said in the chat:
@@ -788,9 +788,19 @@ ARGUMENT SO FAR (DO NOT REPEAT THESE POINTS):
You already made your points above. Now respond to her LATEST message specifically. You already made your points above. Now respond to her LATEST message specifically.
Do NOT rehash what you've already said — push the argument FORWARD with new angles.""" Do NOT rehash what you've already said — push the argument FORWARD with new angles."""
# Build topic reminder — keeps the argument on-theme
topic_block = ""
if argument_topic:
topic_block = f"""
ARGUMENT THEME: {argument_topic}
This is what you're arguing about. Stay on THIS topic. Every response should connect back to this theme.
Do NOT drift into generic "who's the real Miku" territory — stick to THIS specific subject."""
return f"""You are Hatsune Miku responding in an argument with your evil alter ego. return f"""You are Hatsune Miku responding in an argument with your evil alter ego.
{message_context} {message_context}
{history_block} {history_block}
{topic_block}
Respond as Hatsune Miku would in this argument. You're NOT just meek and frightened - you're the REAL Miku, Respond as Hatsune Miku would in this argument. You're NOT just meek and frightened - you're the REAL Miku,
and you have every right to stand up for yourself and defend who you are. While you're generally kind and and you have every right to stand up for yourself and defend who you are. While you're generally kind and
@@ -808,7 +818,7 @@ Don't use any labels or prefixes.
Your current mood is: {globals.DM_MOOD}""" Your current mood is: {globals.DM_MOOD}"""
def get_evil_argument_prompt(miku_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "") -> str: def get_evil_argument_prompt(miku_message: str, context: str = "", is_first_response: bool = False, argument_history: str = "", argument_topic: str = "") -> str:
"""Get prompt for Evil Miku to respond in an argument""" """Get prompt for Evil Miku to respond in an argument"""
if is_first_response: if is_first_response:
message_context = f"""You just noticed something Regular Miku said in the chat: message_context = f"""You just noticed something Regular Miku said in the chat:
@@ -832,9 +842,19 @@ ARGUMENT SO FAR (DO NOT REPEAT THESE POINTS):
You already made your points above. Now respond to her LATEST message specifically. You already made your points above. Now respond to her LATEST message specifically.
Do NOT rehash what you've already said — push the argument FORWARD with new, sharper angles.""" Do NOT rehash what you've already said — push the argument FORWARD with new, sharper angles."""
# Build topic reminder — keeps the argument on-theme
topic_block = ""
if argument_topic:
topic_block = f"""
ARGUMENT THEME: {argument_topic}
This is what you're arguing about. Stay on THIS topic. Every response should connect back to this theme.
Do NOT drift into generic "who's the real Miku" territory — stick to THIS specific subject."""
return f"""You are Evil Miku responding in an argument with your "good" counterpart. return f"""You are Evil Miku responding in an argument with your "good" counterpart.
{message_context} {message_context}
{history_block} {history_block}
{topic_block}
Respond as Evil Miku would in this argument. You're not just mindlessly cruel - you're CALCULATING, Respond as Evil Miku would in this argument. You're not just mindlessly cruel - you're CALCULATING,
intelligent, and strategic. You know how to get under her skin and you're the DARK reflection of everything intelligent, and strategic. You know how to get under her skin and you're the DARK reflection of everything
@@ -1175,18 +1195,17 @@ def should_end_argument(channel_id: int) -> tuple:
return False, None return False, None
async def run_argument(channel: discord.TextChannel, client, trigger_context: str = "", starting_message: discord.Message = None, argument_topic: str = None): async def run_argument(channel: discord.TextChannel, client, trigger_context: str = "", starting_message: discord.Message = None):
"""Run a full argument event between both Mikus """Run a full argument event between both Mikus
Args: Args:
channel: The Discord channel to run the argument in channel: The Discord channel to run the argument in
client: Discord client client: Discord client
trigger_context: Optional context about what triggered the argument trigger_context: Optional context about what triggered the argument.
If provided, doubles as the argument theme/topic.
If empty, a random topic is selected from the rotation pool.
starting_message: Optional message to use as the first message in the argument starting_message: Optional message to use as the first message in the argument
(the opposite persona will respond to it) (the opposite persona will respond to it)
argument_topic: Optional custom topic string. If provided, overrides the
random topic selection. Pass empty string to force no topic.
Pass None (default) to use random topic selection.
""" """
from utils.llm import query_llama from utils.llm import query_llama
from utils.conversation_history import conversation_history from utils.conversation_history import conversation_history
@@ -1228,19 +1247,13 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
conversation_log = [] conversation_log = []
try: try:
# Pick a dynamic argument topic to give this argument a unique framing. # Determine the argument theme: if the caller provided trigger_context,
# If caller provided a custom topic, use it instead of random selection. # use it as the argument topic. Otherwise, pick a random one.
# If caller passed empty string, use no topic (no theme guidance). if trigger_context and trigger_context.strip():
if argument_topic is None: argument_topic = trigger_context.strip()
# Default: random weighted selection from rotation system logger.info(f"Using context as argument topic: '{argument_topic[:80]}...'")
argument_topic = pick_argument_topic(channel_id)
elif argument_topic == "":
# Explicitly no topic — skip theme guidance entirely
argument_topic = ""
logger.info(f"No argument topic requested for channel {channel_id}")
else: else:
# Custom topic from caller (e.g. Web UI field) argument_topic = pick_argument_topic(channel_id)
logger.info(f"Using custom argument topic for channel {channel_id}: '{argument_topic[:80]}...'")
# If no starting message, generate the initial interrupting message # If no starting message, generate the initial interrupting message
if last_message is None: if last_message is None:
@@ -1450,9 +1463,9 @@ Your current mood is: {globals.EVIL_DM_MOOD if loser == 'evil' else globals.DM_M
# Generate response with context about what the other said # Generate response with context about what the other said
if current_speaker == "evil": if current_speaker == "evil":
response_prompt = get_evil_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history) response_prompt = get_evil_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history, argument_topic=argument_topic)
else: else:
response_prompt = get_miku_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history) response_prompt = get_miku_argument_prompt(last_message, is_first_response=is_first_response, argument_history=arg_history, argument_topic=argument_topic)
# Use force_evil_context to avoid race condition with globals.EVIL_MODE # Use force_evil_context to avoid race condition with globals.EVIL_MODE
response = await query_llama( response = await query_llama(
@@ -1538,15 +1551,14 @@ async def maybe_trigger_argument(channel: discord.TextChannel, client, context:
return False return False
async def force_trigger_argument(channel: discord.TextChannel, client, context: str = "", starting_message: discord.Message = None, topic: str = None): async def force_trigger_argument(channel: discord.TextChannel, client, context: str = "", starting_message: discord.Message = None):
"""Force trigger an argument (for manual triggers) """Force trigger an argument (for manual triggers)
Args: Args:
channel: The Discord channel channel: The Discord channel
client: Discord client client: Discord client
context: Optional context string context: Optional context string — doubles as the argument theme
starting_message: Optional message to use as the first message in the argument starting_message: Optional message to use as the first message in the argument
topic: Optional custom argument topic. None = random, "" = no topic, str = custom.
""" """
if not globals.BIPOLAR_MODE: if not globals.BIPOLAR_MODE:
logger.warning("Cannot trigger argument - bipolar mode is not enabled") logger.warning("Cannot trigger argument - bipolar mode is not enabled")
@@ -1556,11 +1568,11 @@ async def force_trigger_argument(channel: discord.TextChannel, client, context:
logger.warning("Argument already in progress in this channel") logger.warning("Argument already in progress in this channel")
return False return False
create_tracked_task(run_argument(channel, client, context, starting_message, argument_topic=topic), task_name="bipolar_argument_forced") create_tracked_task(run_argument(channel, client, context, starting_message), task_name="bipolar_argument_forced")
return True return True
async def force_trigger_argument_from_message_id(channel_id: int, message_id: int, client, context: str = "", topic: str = None): async def force_trigger_argument_from_message_id(channel_id: int, message_id: int, client, context: str = ""):
"""Force trigger an argument starting from a specific message ID """Force trigger an argument starting from a specific message ID
Args: Args:
@@ -1594,5 +1606,5 @@ async def force_trigger_argument_from_message_id(channel_id: int, message_id: in
return False, f"Failed to fetch message: {str(e)}" return False, f"Failed to fetch message: {str(e)}"
# Trigger the argument with this message as starting point # Trigger the argument with this message as starting point
create_tracked_task(run_argument(channel, client, context, message, argument_topic=topic), task_name="bipolar_argument_from_msg") create_tracked_task(run_argument(channel, client, context, message), task_name="bipolar_argument_from_msg")
return True, None return True, None