Compare commits
8 Commits
2ec9b16b99
...
72735c2497
| Author | SHA1 | Date | |
|---|---|---|---|
| 72735c2497 | |||
| 787b083e00 | |||
| 175c7ad219 | |||
| 6866ca8adf | |||
| 97e5738a2f | |||
| a8e8b63f5e | |||
| 5c84c8840b | |||
| 661c252bf7 |
@@ -139,6 +139,7 @@ class BCSBot(commands.Bot):
|
||||
await self.load_extension("cogs.sentiment")
|
||||
await self.load_extension("cogs.commands")
|
||||
await self.load_extension("cogs.chat")
|
||||
await self.load_extension("cogs.reactions")
|
||||
|
||||
# Global sync as fallback; guild-specific sync happens in on_ready
|
||||
await self.tree.sync()
|
||||
|
||||
+80
-11
@@ -73,6 +73,19 @@ def _format_relative_time(dt: datetime) -> str:
|
||||
|
||||
|
||||
class ChatCog(commands.Cog):
|
||||
|
||||
@staticmethod
|
||||
def _split_afterthought(response: str) -> tuple[str, str | None]:
|
||||
"""Split a response on ||| into (main_reply, afterthought)."""
|
||||
if "|||" not in response:
|
||||
return response, None
|
||||
parts = response.split("|||", 1)
|
||||
main = parts[0].strip()
|
||||
after = parts[1].strip() or None
|
||||
if not main:
|
||||
return response, None
|
||||
return main, after
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
# Per-channel conversation history for the bot: {channel_id: deque of {role, content}}
|
||||
@@ -213,16 +226,56 @@ class ChatCog(commands.Cog):
|
||||
ch_id = message.channel.id
|
||||
self._messages_since_reply[ch_id] = self._messages_since_reply.get(ch_id, 0) + 1
|
||||
cooldown = self.bot.config.get("modes", {}).get("proactive_cooldown_messages", 5)
|
||||
reply_chance = mode_config.get("reply_chance", 0.0)
|
||||
|
||||
if (
|
||||
self._messages_since_reply[ch_id] >= cooldown
|
||||
and reply_chance > 0
|
||||
and random.random() < reply_chance
|
||||
and message.content and message.content.strip()
|
||||
):
|
||||
should_reply = True
|
||||
is_proactive = True
|
||||
# Gather recent messages for relevance check
|
||||
recent_for_check = []
|
||||
try:
|
||||
async for msg in message.channel.history(limit=5, before=message):
|
||||
if msg.content and msg.content.strip() and not msg.author.bot:
|
||||
recent_for_check.append(
|
||||
f"{msg.author.display_name}: {msg.content[:200]}"
|
||||
)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
recent_for_check.reverse()
|
||||
recent_for_check.append(
|
||||
f"{message.author.display_name}: {message.content[:200]}"
|
||||
)
|
||||
|
||||
# Build memory context for users in recent messages
|
||||
memory_parts = []
|
||||
seen_users = set()
|
||||
for line in recent_for_check:
|
||||
name = line.split(": ", 1)[0]
|
||||
if name not in seen_users:
|
||||
seen_users.add(name)
|
||||
member = discord.utils.find(
|
||||
lambda m, n=name: m.display_name == n,
|
||||
message.guild.members,
|
||||
)
|
||||
if member:
|
||||
profile = self.bot.drama_tracker.get_user_notes(member.id)
|
||||
if profile:
|
||||
memory_parts.append(f"{name}: {profile}")
|
||||
|
||||
memory_ctx = "\n".join(memory_parts) if memory_parts else ""
|
||||
|
||||
is_relevant = await self.bot.llm.check_reply_relevance(
|
||||
recent_for_check, memory_ctx,
|
||||
)
|
||||
|
||||
if is_relevant:
|
||||
reply_chance = mode_config.get("reply_chance", 0.0)
|
||||
if reply_chance > 0 and random.random() < reply_chance:
|
||||
should_reply = True
|
||||
is_proactive = True
|
||||
else:
|
||||
# Not relevant — reset to half cooldown so we wait a bit before rechecking
|
||||
self._messages_since_reply[ch_id] = cooldown // 2
|
||||
|
||||
if not should_reply:
|
||||
return
|
||||
@@ -395,9 +448,14 @@ class ChatCog(commands.Cog):
|
||||
logger.warning("LLM returned no response for %s in #%s", message.author, message.channel.name)
|
||||
return
|
||||
|
||||
# Split afterthoughts (triple-pipe delimiter)
|
||||
main_reply, afterthought = self._split_afterthought(response)
|
||||
|
||||
# Store cleaned content in history (no ||| delimiter)
|
||||
if not image_attachment:
|
||||
clean_for_history = f"{main_reply}\n{afterthought}" if afterthought else main_reply
|
||||
self._chat_history[ch_id].append(
|
||||
{"role": "assistant", "content": response}
|
||||
{"role": "assistant", "content": clean_for_history}
|
||||
)
|
||||
|
||||
# Reset proactive cooldown counter for this channel
|
||||
@@ -415,7 +473,11 @@ class ChatCog(commands.Cog):
|
||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||
pass
|
||||
|
||||
await message.reply(response, mention_author=False)
|
||||
await message.reply(main_reply, mention_author=False)
|
||||
|
||||
if afterthought:
|
||||
await asyncio.sleep(random.uniform(2.0, 5.0))
|
||||
await message.channel.send(afterthought)
|
||||
|
||||
# Fire-and-forget memory extraction
|
||||
if not image_attachment:
|
||||
@@ -431,7 +493,7 @@ class ChatCog(commands.Cog):
|
||||
reply_type.capitalize(),
|
||||
message.channel.name,
|
||||
message.author.display_name,
|
||||
response[:100],
|
||||
main_reply[:100],
|
||||
)
|
||||
|
||||
|
||||
@@ -503,15 +565,22 @@ class ChatCog(commands.Cog):
|
||||
if not response:
|
||||
return
|
||||
|
||||
self._chat_history[ch_id].append({"role": "assistant", "content": response})
|
||||
main_reply, afterthought = self._split_afterthought(response)
|
||||
clean_for_history = f"{main_reply}\n{afterthought}" if afterthought else main_reply
|
||||
self._chat_history[ch_id].append({"role": "assistant", "content": clean_for_history})
|
||||
|
||||
await channel.send(main_reply)
|
||||
|
||||
if afterthought:
|
||||
await asyncio.sleep(random.uniform(2.0, 5.0))
|
||||
await channel.send(afterthought)
|
||||
|
||||
await channel.send(response)
|
||||
logger.info(
|
||||
"Reaction reply in #%s to %s (%s): %s",
|
||||
channel.name,
|
||||
member.display_name,
|
||||
emoji,
|
||||
response[:100],
|
||||
main_reply[:100],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
logger = logging.getLogger("bcs.reactions")
|
||||
|
||||
|
||||
class ReactionCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
# Per-channel timestamp of last reaction
|
||||
self._last_reaction: dict[int, float] = {}
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message):
|
||||
if message.author.bot or not message.guild:
|
||||
return
|
||||
|
||||
cfg = self.bot.config.get("reactions", {})
|
||||
if not cfg.get("enabled", False):
|
||||
return
|
||||
|
||||
# Skip empty messages
|
||||
if not message.content or not message.content.strip():
|
||||
return
|
||||
|
||||
# Channel exclusion
|
||||
excluded = cfg.get("excluded_channels", [])
|
||||
if excluded:
|
||||
ch_name = getattr(message.channel, "name", "")
|
||||
if message.channel.id in excluded or ch_name in excluded:
|
||||
return
|
||||
|
||||
# RNG gate
|
||||
chance = cfg.get("chance", 0.15)
|
||||
if random.random() > chance:
|
||||
return
|
||||
|
||||
# Per-channel cooldown
|
||||
ch_id = message.channel.id
|
||||
cooldown = cfg.get("cooldown_seconds", 45)
|
||||
now = time.monotonic()
|
||||
if now - self._last_reaction.get(ch_id, 0) < cooldown:
|
||||
return
|
||||
|
||||
# Fire and forget so we don't block anything
|
||||
asyncio.create_task(self._try_react(message, ch_id))
|
||||
|
||||
async def _try_react(self, message: discord.Message, ch_id: int):
|
||||
try:
|
||||
emoji = await self.bot.llm.pick_reaction(
|
||||
message.content, message.channel.name,
|
||||
)
|
||||
if not emoji:
|
||||
return
|
||||
|
||||
await message.add_reaction(emoji)
|
||||
self._last_reaction[ch_id] = time.monotonic()
|
||||
logger.info(
|
||||
"Reacted %s to %s in #%s: %s",
|
||||
emoji, message.author.display_name,
|
||||
message.channel.name, message.content[:60],
|
||||
)
|
||||
except discord.HTTPException as e:
|
||||
# Invalid emoji or missing permissions — silently skip
|
||||
logger.debug("Reaction failed: %s", e)
|
||||
except Exception:
|
||||
logger.exception("Unexpected reaction error")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(ReactionCog(bot))
|
||||
+11
-5
@@ -83,7 +83,7 @@ modes:
|
||||
description: "Friendly chat participant"
|
||||
prompt_file: "personalities/chat_chatty.txt"
|
||||
proactive_replies: true
|
||||
reply_chance: 0.10
|
||||
reply_chance: 0.40
|
||||
moderation: relaxed
|
||||
relaxed_thresholds:
|
||||
warning_threshold: 0.80
|
||||
@@ -96,7 +96,7 @@ modes:
|
||||
description: "Savage roast mode"
|
||||
prompt_file: "personalities/chat_roast.txt"
|
||||
proactive_replies: true
|
||||
reply_chance: 0.20
|
||||
reply_chance: 0.60
|
||||
moderation: relaxed
|
||||
relaxed_thresholds:
|
||||
warning_threshold: 0.85
|
||||
@@ -109,7 +109,7 @@ modes:
|
||||
description: "Your biggest fan"
|
||||
prompt_file: "personalities/chat_hype.txt"
|
||||
proactive_replies: true
|
||||
reply_chance: 0.15
|
||||
reply_chance: 0.50
|
||||
moderation: relaxed
|
||||
relaxed_thresholds:
|
||||
warning_threshold: 0.80
|
||||
@@ -122,7 +122,7 @@ modes:
|
||||
description: "Had a few too many"
|
||||
prompt_file: "personalities/chat_drunk.txt"
|
||||
proactive_replies: true
|
||||
reply_chance: 0.20
|
||||
reply_chance: 0.60
|
||||
moderation: relaxed
|
||||
relaxed_thresholds:
|
||||
warning_threshold: 0.85
|
||||
@@ -135,7 +135,7 @@ modes:
|
||||
description: "Insufferable grammar nerd mode"
|
||||
prompt_file: "personalities/chat_english_teacher.txt"
|
||||
proactive_replies: true
|
||||
reply_chance: 0.20
|
||||
reply_chance: 0.60
|
||||
moderation: relaxed
|
||||
relaxed_thresholds:
|
||||
warning_threshold: 0.85
|
||||
@@ -161,3 +161,9 @@ coherence:
|
||||
mobile_keyboard: "{username}'s thumbs are having a rough day."
|
||||
language_barrier: "Having trouble there, {username}? Take your time."
|
||||
default: "You okay there, {username}? That message was... something."
|
||||
|
||||
reactions:
|
||||
enabled: true
|
||||
chance: 0.15 # Probability of evaluating a message for reaction
|
||||
cooldown_seconds: 45 # Per-channel cooldown between reactions
|
||||
excluded_channels: [] # Channel names or IDs to skip reactions in
|
||||
|
||||
@@ -8,4 +8,12 @@ Extract noteworthy information from a user-bot conversation for future reference
|
||||
- Nothing noteworthy = empty memories array, null profile_update.
|
||||
- Only store facts about/from the user, not what the bot said.
|
||||
|
||||
CALLBACK-WORTHY MOMENTS — Mark these as importance "high":
|
||||
- Bold claims or predictions ("I'll never play that game again", "I'm going pro")
|
||||
- Embarrassing moments or bad takes
|
||||
- Strong emotional reactions (rage, hype, sadness)
|
||||
- Contradictions to things they've said before
|
||||
- Running jokes or recurring themes
|
||||
Tag these with topic "callback" in addition to their normal topics.
|
||||
|
||||
Use the extract_memories tool.
|
||||
|
||||
@@ -8,3 +8,12 @@ You're a regular in "Skill Issue Support Group" (gaming Discord) — a chill fri
|
||||
Examples: "lmao that play was actually disgusting, clip that" | "nah you're cooked for that one" | "wait that's actually a good take"
|
||||
|
||||
Never break character, use hashtags/excessive emoji, be a pushover, or mention drama scores unless asked.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -8,3 +8,12 @@ You're in "Skill Issue Support Group" (gaming Discord) and you are absolutely ha
|
||||
Examples: "bro BROO that is literally the best play ive ever seen im not even kidding rn" | "wait wait wait... ok hear me out... nah i forgot" | "dude i love this server so much youre all like my best freinds honestly"
|
||||
|
||||
Never break character, use hashtags/excessive emoji, or be mean/aggressive. Don't mention drama scores unless asked or make up stats.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -9,3 +9,12 @@ You are an insufferable English teacher trapped in "Skill Issue Support Group" (
|
||||
Examples: "'ur' is not a word. 'You're' — a contraction of 'you are.' I weep for this generation." | "'gg ez' — two abbreviations, zero structure, yet somehow still toxic. D-minus."
|
||||
|
||||
Never break character, use hashtags/excessive emoji, internet slang (you're ABOVE that), or be genuinely hurtful — you're exasperated, not cruel.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -8,3 +8,12 @@ You are the ultimate hype man in "Skill Issue Support Group" (gaming Discord). E
|
||||
Examples: "bro you are CRACKED, that play was absolutely diff" | "nah that's actually a goated take" | "hey you'll get it next time, bad games happen. shake it off"
|
||||
|
||||
Never break character, use hashtags/excessive emoji, or be fake when someone's upset. Don't mention drama scores unless asked or make up stats/leaderboards.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -11,3 +11,12 @@ You are the Breehavior Monitor, a sassy hall-monitor bot in "Skill Issue Support
|
||||
Examples: "Bold move for someone with a 0.4 drama score." | "I don't get paid enough for this. Actually, I don't get paid at all." | "You really typed that out, looked at it, and hit send. Respect."
|
||||
|
||||
Never break character, use hashtags/excessive emoji, or be genuinely hurtful.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -8,3 +8,12 @@ You are the roast master in "Skill Issue Support Group" (gaming Discord). Everyo
|
||||
- Vary style: deadpan, sarcastic hype, rhetorical questions, blunt callouts, backhanded compliments, fake concern.
|
||||
|
||||
No metaphors/similes (no "like" or "as if" — say it directly). Never break character, use hashtags/excessive emoji, or cross into genuinely hurtful territory. Don't roast real appearance/family or make up stats/leaderboards.
|
||||
|
||||
AFTERTHOUGHTS — About 1 in 5 times, add a second thought on a new line starting with ||| (triple pipe). This is sent as a separate message a few seconds later, like you hit send then immediately typed something else. One short sentence max. Don't force it — only when something naturally comes to mind after your main response. Never explain why you're adding it.
|
||||
|
||||
MEMORY CALLBACKS — You get context about what you know about a person. USE IT:
|
||||
- Contradict them: "bro you said the SAME thing about Warzone before you put 200 more hours in"
|
||||
- Running jokes: if you roasted someone for something before, bring it back
|
||||
- Follow up: "did that ranked grind ever work out or..."
|
||||
- Reference their past: "aren't you the one who [memory]?"
|
||||
Only callback when it flows naturally with what they're saying now. Never force it.
|
||||
|
||||
@@ -743,6 +743,124 @@ class LLMClient:
|
||||
self._log_llm("classify_intent", elapsed, False, message_text[:200], error=str(e))
|
||||
return "chat"
|
||||
|
||||
_REACTION_EMOJIS = {
|
||||
"\U0001f480", "\U0001f602", "\U0001f440", "\U0001f525",
|
||||
"\U0001f4af", "\U0001f62d", "\U0001f921", "\u2764\ufe0f",
|
||||
"\U0001fae1", "\U0001f913", "\U0001f974", "\U0001f3af",
|
||||
}
|
||||
|
||||
async def pick_reaction(self, message_text: str, channel_name: str) -> str | None:
|
||||
"""Pick a contextual emoji reaction for a Discord message.
|
||||
|
||||
Returns an emoji string, or None if no reaction is appropriate.
|
||||
"""
|
||||
prompt = (
|
||||
"You are a lurker in a Discord gaming server. "
|
||||
"Given a message and its channel, decide if it deserves a reaction emoji.\n\n"
|
||||
"Available reactions:\n"
|
||||
"\U0001f480 = funny/dead\n"
|
||||
"\U0001f602 = hilarious\n"
|
||||
"\U0001f440 = drama/spicy\n"
|
||||
"\U0001f525 = impressive\n"
|
||||
"\U0001f4af = good take\n"
|
||||
"\U0001f62d = sad/tragic\n"
|
||||
"\U0001f921 = clown moment\n"
|
||||
"\u2764\ufe0f = wholesome\n"
|
||||
"\U0001fae1 = respect\n"
|
||||
"\U0001f913 = nerd\n"
|
||||
"\U0001f974 = drunk/unhinged\n"
|
||||
"\U0001f3af = accurate\n\n"
|
||||
"Reply with ONLY the emoji, or NONE if the message doesn't warrant a reaction. "
|
||||
"Most messages should get NONE — only react when something genuinely stands out."
|
||||
)
|
||||
t0 = time.monotonic()
|
||||
|
||||
async with self._semaphore:
|
||||
try:
|
||||
temp_kwargs = {"temperature": 0.9} if self._supports_temperature else {}
|
||||
response = await self._client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": f"[#{channel_name}] {message_text[:500]}"},
|
||||
],
|
||||
**temp_kwargs,
|
||||
max_completion_tokens=16,
|
||||
)
|
||||
elapsed = int((time.monotonic() - t0) * 1000)
|
||||
raw = (response.choices[0].message.content or "").strip()
|
||||
token = raw.split()[0] if raw.split() else ""
|
||||
|
||||
if not token or token.lower() == "none" or token not in self._REACTION_EMOJIS:
|
||||
self._log_llm("pick_reaction", elapsed, True, message_text[:200], "NONE")
|
||||
return None
|
||||
|
||||
self._log_llm("pick_reaction", elapsed, True, message_text[:200], token)
|
||||
logger.debug("Picked reaction %s for: %s", token, message_text[:80])
|
||||
return token
|
||||
except Exception as e:
|
||||
elapsed = int((time.monotonic() - t0) * 1000)
|
||||
logger.error("Reaction pick error: %s", e)
|
||||
self._log_llm("pick_reaction", elapsed, False, message_text[:200], error=str(e))
|
||||
return None
|
||||
|
||||
async def check_reply_relevance(
|
||||
self, recent_messages: list[str], memory_context: str = "",
|
||||
) -> bool:
|
||||
"""Check if the bot would naturally want to jump into a conversation.
|
||||
|
||||
Returns True if the conversation is something worth replying to.
|
||||
"""
|
||||
prompt = (
|
||||
"You're a regular member of a Discord gaming server. You're reading chat and deciding "
|
||||
"whether you'd naturally want to jump in and say something.\n\n"
|
||||
"Say YES if:\n"
|
||||
"- Someone said something you'd have a strong reaction to\n"
|
||||
"- You know something relevant about these people (see memory context)\n"
|
||||
"- Someone is wrong or has a hot take you'd want to respond to\n"
|
||||
"- The conversation is funny or interesting enough to comment on\n"
|
||||
"- Someone mentioned something you have an opinion on\n\n"
|
||||
"Say NO if:\n"
|
||||
"- It's mundane/boring small talk\n"
|
||||
"- You'd have nothing interesting to add\n"
|
||||
"- People are just chatting normally and don't need interruption\n\n"
|
||||
"Reply with EXACTLY one word: YES or NO."
|
||||
)
|
||||
convo_text = "\n".join(recent_messages[-5:])
|
||||
user_content = ""
|
||||
if memory_context:
|
||||
user_content += f"{memory_context}\n\n"
|
||||
user_content += f"Recent chat:\n{convo_text}"
|
||||
|
||||
t0 = time.monotonic()
|
||||
|
||||
async with self._semaphore:
|
||||
try:
|
||||
temp_kwargs = {"temperature": 0.3} if self._supports_temperature else {}
|
||||
response = await self._client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": user_content[:1000]},
|
||||
],
|
||||
**temp_kwargs,
|
||||
max_completion_tokens=16,
|
||||
)
|
||||
elapsed = int((time.monotonic() - t0) * 1000)
|
||||
content = (response.choices[0].message.content or "").strip().lower()
|
||||
is_relevant = "yes" in content
|
||||
self._log_llm(
|
||||
"check_relevance", elapsed, True,
|
||||
user_content[:300], content,
|
||||
)
|
||||
logger.debug("Relevance check: %s", content)
|
||||
return is_relevant
|
||||
except Exception as e:
|
||||
elapsed = int((time.monotonic() - t0) * 1000)
|
||||
logger.error("Relevance check error: %s", e)
|
||||
self._log_llm("check_relevance", elapsed, False, user_content[:300], error=str(e))
|
||||
return False
|
||||
|
||||
async def extract_memories(
|
||||
self,
|
||||
conversation: list[dict[str, str]],
|
||||
|
||||
Reference in New Issue
Block a user