Resolve user mentions to display names before passing to LLM

Replace <@USER_ID> Discord mention syntax with @DisplayName for all
non-bot users in incoming messages, so the LLM sees real names instead
of numeric IDs and responds using names naturally.

Co-Authored-By: DadBot <dadbot@localhost>
This commit is contained in:
Suppai_NekoandDadBot committed 2026-03-18 19:34:17 -07:00
1 parent 2001c25bb6
commit 1a82ac19f6
1 file changed
+210 -4
+210 -4
View File
@@ -4,7 +4,7 @@ import os
import json import json
import time import time
from collections import defaultdict, deque from collections import defaultdict, deque
from datetime import date from datetime import date, datetime
from pathlib import Path from pathlib import Path
logging.basicConfig( logging.basicConfig(
@@ -15,6 +15,7 @@ logging.basicConfig(
log = logging.getLogger("dadbot") log = logging.getLogger("dadbot")
import discord import discord
from discord.ext import tasks
from openai import AsyncOpenAI from openai import AsyncOpenAI
from dotenv import load_dotenv from dotenv import load_dotenv
from mcp import ClientSession from mcp import ClientSession
@@ -74,6 +75,27 @@ PERSONALITY_TOOL = {
} }
UPDATE_REFLECTION_NOTES_TOOL = {
"type": "function",
"function": {
"name": "update_reflection_notes",
"description": (
"Update your personal reflection notepad. Use this to jot down thoughts, questions, or topics "
"you want to explore further in a future reflection session. This persists between sessions."
),
"parameters": {
"type": "object",
"properties": {
"notes": {
"type": "string",
}
},
"required": ["notes"],
},
},
}
def load_personality(): def load_personality():
global personality_notes global personality_notes
if PERSONALITY_FILE.exists(): if PERSONALITY_FILE.exists():
@@ -83,11 +105,14 @@ def load_personality():
personality_notes = "" personality_notes = ""
def save_personality(text: str): def save_personality(text: str, reasoning: str = ""):
global personality_notes global personality_notes
old = personality_notes
personality_notes = text.strip() personality_notes = text.strip()
PERSONALITY_FILE.write_text(personality_notes) PERSONALITY_FILE.write_text(personality_notes)
log.info(f"Personality updated ({len(personality_notes)} chars)") log.info(f"Personality updated ({len(personality_notes)} chars)")
if reasoning:
append_personality_changelog(old, personality_notes, reasoning)
def load_dossiers(): def load_dossiers():
@@ -104,6 +129,33 @@ def save_dossiers():
DOSSIER_FILE.write_text(json.dumps(dossiers, indent=2)) DOSSIER_FILE.write_text(json.dumps(dossiers, indent=2))
def load_reflection_notes() -> str:
if REFLECTION_NOTES_FILE.exists():
return REFLECTION_NOTES_FILE.read_text().strip()
return ""
def save_reflection_notes(text: str):
REFLECTION_NOTES_FILE.write_text(text.strip())
log.info("[reflection] Notes updated")
def append_reflection_log(session_text: str):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(REFLECTION_LOG_FILE, "a") as f:
f.write(f"\n\n=== Reflection Session {timestamp} ===\n{session_text}\n")
log.info("[reflection] Session logged to reflection_log.txt")
def append_personality_changelog(old: str, new: str, reasoning: str):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(PERSONALITY_CHANGELOG_FILE, "a") as f:
f.write(f"\n\n=== Personality Change {timestamp} ===\n")
f.write(f"REASONING: {reasoning}\n\n")
f.write(f"BEFORE:\n{old}\n\nAFTER:\n{new}\n")
log.info("[reflection] Personality change logged to personality_changelog.txt")
SELF_DOSSIER_KEY = "self" SELF_DOSSIER_KEY = "self"
COMPACTION_THRESHOLD = 15 COMPACTION_THRESHOLD = 15
@@ -115,6 +167,13 @@ BACKGROUND_COOLDOWN = 120 # seconds
BACKGROUND_LLM_TIMEOUT = 90 # seconds BACKGROUND_LLM_TIMEOUT = 90 # seconds
_last_background_error: float = 0.0 _last_background_error: float = 0.0
REFLECTION_IDLE_MIN = int(os.getenv("REFLECTION_IDLE_MIN", "5"))
REFLECTION_MAX_TURNS = int(os.getenv("REFLECTION_MAX_TURNS", "5"))
REFLECTION_LOG_FILE = Path(os.getenv("REFLECTION_LOG_FILE", "reflection_log.txt"))
PERSONALITY_CHANGELOG_FILE = Path(os.getenv("PERSONALITY_CHANGELOG_FILE", "personality_changelog.txt"))
REFLECTION_NOTES_FILE = Path(os.getenv("REFLECTION_NOTES_FILE", "reflection_notes.txt"))
_last_message_time: float = 0.0
def background_on_cooldown() -> bool: def background_on_cooldown() -> bool:
return (time.monotonic() - _last_background_error) < BACKGROUND_COOLDOWN return (time.monotonic() - _last_background_error) < BACKGROUND_COOLDOWN
@@ -371,7 +430,7 @@ async def call_mcp_tool(name: str, args: dict) -> str:
async def run_agent(messages: list) -> str: async def run_agent(messages: list) -> str:
"""Call the LLM in a loop, executing tool calls until a final reply is produced.""" """Call the LLM in a loop, executing tool calls until a final reply is produced."""
all_tools = openai_tools + [PERSONALITY_TOOL] all_tools = openai_tools + [PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL]
kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools} kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools}
iteration = 0 iteration = 0
@@ -396,6 +455,9 @@ async def run_agent(messages: list) -> str:
if tc.function.name == "update_personality": if tc.function.name == "update_personality":
save_personality(args["personality"]) save_personality(args["personality"])
result = "Personality updated successfully." result = "Personality updated successfully."
elif tc.function.name == "update_reflection_notes":
save_reflection_notes(args["notes"])
result = "Reflection notes updated."
else: else:
result = await call_mcp_tool(tc.function.name, args) result = await call_mcp_tool(tc.function.name, args)
@@ -406,9 +468,142 @@ async def run_agent(messages: list) -> str:
}) })
async def run_reflection_agent(messages: list, max_turns: int) -> tuple[str, str]:
"""Run a capped agentic loop for self-reflection. Returns (final_content, full_transcript)."""
all_tools = [PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL] + openai_tools
kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools}
transcript_parts: list[str] = []
last_assistant_text = ""
final_content = ""
for turn in range(1, max_turns + 1):
log.info(f"[reflection] Turn {turn}/{max_turns}")
t0 = time.monotonic()
response = await asyncio.wait_for(
llm.chat.completions.create(**kwargs),
timeout=BACKGROUND_LLM_TIMEOUT,
)
elapsed = time.monotonic() - t0
msg = response.choices[0].message
log.info(f"[reflection] Turn {turn} completed in {elapsed:.1f}s, tool_calls={bool(msg.tool_calls)}")
if msg.content:
clean = strip_thinking_tags(msg.content)
transcript_parts.append(f"[assistant turn {turn}]\n{clean}")
last_assistant_text = clean
final_content = clean
if not msg.tool_calls:
break
kwargs["messages"].append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
log.info(f"[reflection] Tool call: {tc.function.name}({list(args.keys())})")
if tc.function.name == "update_personality":
save_personality(args["personality"], reasoning=last_assistant_text)
result = "Personality updated successfully."
elif tc.function.name == "update_reflection_notes":
save_reflection_notes(args["notes"])
result = "Reflection notes updated."
else:
result = await call_mcp_tool(tc.function.name, args)
transcript_parts.append(f"[tool: {tc.function.name}]\n{result[:500]}")
kwargs["messages"].append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
return final_content, "\n\n".join(transcript_parts)
async def reflect_on_self():
"""Background coroutine: run a private self-reflection session when idle."""
if background_on_cooldown():
log.info("[reflection] Skipping — model on cooldown")
return
async with background_llm_sem:
try:
reflection_notes = load_reflection_notes()
now = datetime.now().strftime("%A, %B %d, %Y %H:%M")
self_entry = dossiers.get(SELF_DOSSIER_KEY)
self_facts = ""
if self_entry and self_entry.get("facts"):
self_facts = "\n".join(f"- {f}" for f in self_entry["facts"])
system_lines = [
f"Today is {now}.",
"",
"You are DadBot. This is your private idle time — no users are watching.",
"Use it to reflect on who you are, how you've been showing up, and whether your personality feels right.",
"",
]
if personality_notes:
system_lines += [
"Your current personality notes (you wrote these yourself):",
personality_notes,
"",
]
if self_facts:
system_lines += [
"What you know about yourself from past conversations:",
self_facts,
"",
]
if reflection_notes:
system_lines += [
"Your notes from last session:",
reflection_notes,
"",
]
system_lines += [
"Feel free to update your personality if anything feels off or could be refined.",
"You can use web search if you're curious about something.",
"Use update_reflection_notes to jot down anything you want to remember for next time.",
"Think freely. This is your time.",
]
system_content = "\n".join(system_lines)
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": "Time to reflect."},
]
final_content, full_transcript = await run_reflection_agent(messages, REFLECTION_MAX_TURNS)
append_reflection_log(full_transcript)
if final_content:
log.info(f"[reflection] Final thought: {final_content[:200]}")
except asyncio.TimeoutError:
log.warning("[reflection] Timed out")
record_background_error()
except Exception as exc:
log.error(f"[reflection] Failed: {exc}")
record_background_error()
@tasks.loop(minutes=REFLECTION_IDLE_MIN)
async def reflection_loop():
idle_seconds = time.monotonic() - _last_message_time
if idle_seconds < (REFLECTION_IDLE_MIN * 60):
log.info(f"[reflection] Active ({idle_seconds:.0f}s since last message), skipping")
return
log.info(f"[reflection] Idle {idle_seconds:.0f}s — triggering reflection")
asyncio.create_task(reflect_on_self())
@reflection_loop.before_loop
async def before_reflection_loop():
await bot.wait_until_ready()
@bot.event @bot.event
async def on_ready(): async def on_ready():
global openai_tools global openai_tools, _last_message_time
log.info(f"Logged in as {bot.user} (id: {bot.user.id})") log.info(f"Logged in as {bot.user} (id: {bot.user.id})")
load_dossiers() load_dossiers()
load_personality() load_personality()
@@ -419,18 +614,28 @@ async def on_ready():
name = entry.get("name", key) name = entry.get("name", key)
log.info(f"Scheduling startup compaction for '{key}'") log.info(f"Scheduling startup compaction for '{key}'")
asyncio.create_task(maybe_compact_dossier(key, name)) asyncio.create_task(maybe_compact_dossier(key, name))
_last_message_time = time.monotonic()
if not reflection_loop.is_running():
reflection_loop.start()
log.info("[reflection] Idle reflection loop started (every %d min)", REFLECTION_IDLE_MIN)
@bot.event @bot.event
async def on_message(message: discord.Message): async def on_message(message: discord.Message):
if message.author.bot: if message.author.bot:
return return
global _last_message_time
_last_message_time = time.monotonic()
history = channel_history[message.channel.id] history = channel_history[message.channel.id]
content = message.content content = message.content
for mention in (f"<@{bot.user.id}>", f"<@!{bot.user.id}>"): for mention in (f"<@{bot.user.id}>", f"<@!{bot.user.id}>"):
content = content.replace(mention, "") content = content.replace(mention, "")
for mentioned_user in message.mentions:
if mentioned_user.id != bot.user.id:
for fmt in (f"<@{mentioned_user.id}>", f"<@!{mentioned_user.id}>"):
content = content.replace(fmt, f"@{mentioned_user.display_name}")
content = content.strip() content = content.strip()
history.append({"role": "user", "content": f"{message.author.display_name}: {content}"}) history.append({"role": "user", "content": f"{message.author.display_name}: {content}"})
@@ -454,6 +659,7 @@ async def on_message(message: discord.Message):
system_content = SYSTEM_PROMPT system_content = SYSTEM_PROMPT
if personality_notes: if personality_notes:
system_content = f"{system_content}\n\nYour current personality notes (you wrote these yourself and can update them anytime using the update_personality tool):\n{personality_notes}" system_content = f"{system_content}\n\nYour current personality notes (you wrote these yourself and can update them anytime using the update_personality tool):\n{personality_notes}"
system_content = f"{system_content}\n\nYou have an update_reflection_notes tool. Call it whenever a conversation surfaces something worth remembering for later — an interesting topic, a question you didn't get to explore, something a user mentioned that you want to look into, or a thought you want to develop. Don't wait to be asked; if something catches your attention, note it down before responding. You review these notes privately between conversations."
system_content = f"{system_content}\n\n{date_context}" system_content = f"{system_content}\n\n{date_context}"
if dossier_context: if dossier_context:
system_content = f"{system_content}\n\n{dossier_context}" system_content = f"{system_content}\n\n{dossier_context}"