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: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
66f8c6390b
commit
daecdbbbc8
1 file changed
+210
-4
@@ -4,7 +4,7 @@ import os
|
||||
import json
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -15,6 +15,7 @@ logging.basicConfig(
|
||||
log = logging.getLogger("dadbot")
|
||||
|
||||
import discord
|
||||
from discord.ext import tasks
|
||||
from openai import AsyncOpenAI
|
||||
from dotenv import load_dotenv
|
||||
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():
|
||||
global personality_notes
|
||||
if PERSONALITY_FILE.exists():
|
||||
@@ -83,11 +105,14 @@ def load_personality():
|
||||
personality_notes = ""
|
||||
|
||||
|
||||
def save_personality(text: str):
|
||||
def save_personality(text: str, reasoning: str = ""):
|
||||
global personality_notes
|
||||
old = personality_notes
|
||||
personality_notes = text.strip()
|
||||
PERSONALITY_FILE.write_text(personality_notes)
|
||||
log.info(f"Personality updated ({len(personality_notes)} chars)")
|
||||
if reasoning:
|
||||
append_personality_changelog(old, personality_notes, reasoning)
|
||||
|
||||
|
||||
def load_dossiers():
|
||||
@@ -104,6 +129,33 @@ def save_dossiers():
|
||||
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"
|
||||
COMPACTION_THRESHOLD = 15
|
||||
|
||||
@@ -115,6 +167,13 @@ BACKGROUND_COOLDOWN = 120 # seconds
|
||||
BACKGROUND_LLM_TIMEOUT = 90 # seconds
|
||||
_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:
|
||||
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:
|
||||
"""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}
|
||||
|
||||
iteration = 0
|
||||
@@ -396,6 +455,9 @@ async def run_agent(messages: list) -> str:
|
||||
if tc.function.name == "update_personality":
|
||||
save_personality(args["personality"])
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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})")
|
||||
load_dossiers()
|
||||
load_personality()
|
||||
@@ -419,18 +614,28 @@ async def on_ready():
|
||||
name = entry.get("name", key)
|
||||
log.info(f"Scheduling startup compaction for '{key}'")
|
||||
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
|
||||
async def on_message(message: discord.Message):
|
||||
if message.author.bot:
|
||||
return
|
||||
global _last_message_time
|
||||
_last_message_time = time.monotonic()
|
||||
|
||||
history = channel_history[message.channel.id]
|
||||
|
||||
content = message.content
|
||||
for mention in (f"<@{bot.user.id}>", f"<@!{bot.user.id}>"):
|
||||
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()
|
||||
|
||||
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
|
||||
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\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}"
|
||||
if dossier_context:
|
||||
system_content = f"{system_content}\n\n{dossier_context}"
|
||||
|
||||
Reference in new issue
Block a user