a
This commit is contained in:
parent
623f74dbfe
commit
4e9210b6b2
@ -1,8 +1,8 @@
|
||||
# This file makes the 'gurt' directory a Python package.
|
||||
# This file makes the 'wheatley' directory a Python package.
|
||||
# It allows Python to properly import modules from this directory
|
||||
|
||||
# Export the setup function for discord.py extension loading
|
||||
from .cog import setup
|
||||
|
||||
# This makes "from gurt import setup" work
|
||||
# This makes "from wheatley import setup" work
|
||||
__all__ = ['setup']
|
||||
|
@ -13,14 +13,15 @@ from .config import (
|
||||
TOPIC_RELEVANCE_DECAY, MAX_ACTIVE_TOPICS, SENTIMENT_DECAY_RATE,
|
||||
EMOTION_KEYWORDS, EMOJI_SENTIMENT # Import necessary configs
|
||||
)
|
||||
# Removed imports for BASELINE_PERSONALITY, REFLECTION_INTERVAL_SECONDS, GOAL related configs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # For type hinting
|
||||
from .cog import WheatleyCog # Updated type hint
|
||||
|
||||
# --- Analysis Functions ---
|
||||
# Note: These functions need the 'cog' instance passed to access state like caches, etc.
|
||||
|
||||
async def analyze_conversation_patterns(cog: 'GurtCog'):
|
||||
async def analyze_conversation_patterns(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Analyzes recent conversations to identify patterns and learn from them"""
|
||||
print("Analyzing conversation patterns and updating topics...")
|
||||
try:
|
||||
@ -30,24 +31,24 @@ async def analyze_conversation_patterns(cog: 'GurtCog'):
|
||||
for channel_id, messages in cog.message_cache['by_channel'].items():
|
||||
if len(messages) < 10: continue
|
||||
|
||||
channel_patterns = extract_conversation_patterns(cog, messages) # Pass cog
|
||||
if channel_patterns:
|
||||
existing_patterns = cog.conversation_patterns[channel_id]
|
||||
combined_patterns = existing_patterns + channel_patterns
|
||||
if len(combined_patterns) > MAX_PATTERNS_PER_CHANNEL:
|
||||
combined_patterns = combined_patterns[-MAX_PATTERNS_PER_CHANNEL:]
|
||||
cog.conversation_patterns[channel_id] = combined_patterns
|
||||
# Pattern extraction might be less useful without personality/goals, but kept for now
|
||||
# channel_patterns = extract_conversation_patterns(cog, messages) # Pass cog
|
||||
# if channel_patterns:
|
||||
# existing_patterns = cog.conversation_patterns.setdefault(channel_id, []) # Use setdefault
|
||||
# combined_patterns = existing_patterns + channel_patterns
|
||||
# if len(combined_patterns) > MAX_PATTERNS_PER_CHANNEL:
|
||||
# combined_patterns = combined_patterns[-MAX_PATTERNS_PER_CHANNEL:]
|
||||
# cog.conversation_patterns[channel_id] = combined_patterns
|
||||
|
||||
analyze_conversation_dynamics(cog, channel_id, messages) # Pass cog
|
||||
|
||||
update_user_preferences(cog) # Pass cog
|
||||
# adapt_personality_traits(cog) # Pass cog - Deprecated/Superseded by evolve_personality
|
||||
update_user_preferences(cog) # Pass cog - Note: This might need adjustment as it relies on traits we removed
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error analyzing conversation patterns: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
async def update_conversation_topics(cog: 'GurtCog'):
|
||||
async def update_conversation_topics(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Updates the active topics for each channel based on recent messages"""
|
||||
try:
|
||||
for channel_id, messages in cog.message_cache['by_channel'].items():
|
||||
@ -88,6 +89,7 @@ async def update_conversation_topics(cog: 'GurtCog'):
|
||||
if len(channel_topics["topic_history"]) > 10:
|
||||
channel_topics["topic_history"] = channel_topics["topic_history"][-10:]
|
||||
|
||||
# User topic interest tracking might be less relevant without proactive interest triggers, but kept for now
|
||||
for msg in recent_messages:
|
||||
user_id = msg["author"]["id"]
|
||||
content = msg["content"].lower()
|
||||
@ -115,7 +117,7 @@ async def update_conversation_topics(cog: 'GurtCog'):
|
||||
print(f"Error updating conversation topics: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
def analyze_conversation_dynamics(cog: 'GurtCog', channel_id: int, messages: List[Dict[str, Any]]):
|
||||
def analyze_conversation_dynamics(cog: 'WheatleyCog', channel_id: int, messages: List[Dict[str, Any]]): # Updated type hint
|
||||
"""Analyzes conversation dynamics like response times, message lengths, etc."""
|
||||
if len(messages) < 5: return
|
||||
try:
|
||||
@ -159,8 +161,11 @@ def analyze_conversation_dynamics(cog: 'GurtCog', channel_id: int, messages: Lis
|
||||
|
||||
except Exception as e: print(f"Error analyzing conversation dynamics: {e}")
|
||||
|
||||
def adapt_to_conversation_dynamics(cog: 'GurtCog', channel_id: int, dynamics: Dict[str, Any]):
|
||||
def adapt_to_conversation_dynamics(cog: 'WheatleyCog', channel_id: int, dynamics: Dict[str, Any]): # Updated type hint
|
||||
"""Adapts bot behavior based on observed conversation dynamics."""
|
||||
# Note: This function previously adapted personality traits.
|
||||
# It might be removed or repurposed for Wheatley if needed.
|
||||
# For now, it calculates factors but doesn't apply them directly to a removed personality system.
|
||||
try:
|
||||
if dynamics["avg_response_time"] > 0:
|
||||
if not hasattr(cog, 'channel_response_timing'): cog.channel_response_timing = {}
|
||||
@ -182,8 +187,9 @@ def adapt_to_conversation_dynamics(cog: 'GurtCog', channel_id: int, dynamics: Di
|
||||
|
||||
except Exception as e: print(f"Error adapting to conversation dynamics: {e}")
|
||||
|
||||
def extract_conversation_patterns(cog: 'GurtCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
def extract_conversation_patterns(cog: 'WheatleyCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # Updated type hint
|
||||
"""Extract patterns from a sequence of messages"""
|
||||
# This function might be less useful without personality/goal systems, kept for potential future use.
|
||||
patterns = []
|
||||
if len(messages) < 5: return patterns
|
||||
import datetime # Import here
|
||||
@ -207,12 +213,12 @@ def extract_conversation_patterns(cog: 'GurtCog', messages: List[Dict[str, Any]]
|
||||
|
||||
return patterns
|
||||
|
||||
def identify_conversation_topics(cog: 'GurtCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
def identify_conversation_topics(cog: 'WheatleyCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # Updated type hint
|
||||
"""Identify potential topics from conversation messages."""
|
||||
if not messages or len(messages) < 3: return []
|
||||
all_text = " ".join([msg["content"] for msg in messages])
|
||||
stopwords = { # Expanded stopwords
|
||||
"the", "and", "is", "in", "to", "a", "of", "for", "that", "this", "it", "with", "on", "as", "be", "at", "by", "an", "or", "but", "if", "from", "when", "where", "how", "all", "any", "both", "each", "few", "more", "most", "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "can", "will", "just", "should", "now", "also", "like", "even", "because", "way", "who", "what", "yeah", "yes", "no", "nah", "lol", "lmao", "haha", "hmm", "um", "uh", "oh", "ah", "ok", "okay", "dont", "don't", "doesnt", "doesn't", "didnt", "didn't", "cant", "can't", "im", "i'm", "ive", "i've", "youre", "you're", "youve", "you've", "hes", "he's", "shes", "she's", "its", "it's", "were", "we're", "weve", "we've", "theyre", "they're", "theyve", "they've", "thats", "that's", "whats", "what's", "whos", "who's", "gonna", "gotta", "kinda", "sorta", "gurt" # Added gurt
|
||||
"the", "and", "is", "in", "to", "a", "of", "for", "that", "this", "it", "with", "on", "as", "be", "at", "by", "an", "or", "but", "if", "from", "when", "where", "how", "all", "any", "both", "each", "few", "more", "most", "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "can", "will", "just", "should", "now", "also", "like", "even", "because", "way", "who", "what", "yeah", "yes", "no", "nah", "lol", "lmao", "haha", "hmm", "um", "uh", "oh", "ah", "ok", "okay", "dont", "don't", "doesnt", "doesn't", "didnt", "didn't", "cant", "can't", "im", "i'm", "ive", "i've", "youre", "you're", "youve", "you've", "hes", "he's", "shes", "she's", "its", "it's", "were", "we're", "weve", "we've", "theyre", "they're", "theyve", "they've", "thats", "that's", "whats", "what's", "whos", "who's", "gonna", "gotta", "kinda", "sorta", "wheatley" # Added wheatley, removed gurt
|
||||
}
|
||||
|
||||
def extract_ngrams(text, n_values=[1, 2, 3]):
|
||||
@ -278,26 +284,13 @@ def identify_conversation_topics(cog: 'GurtCog', messages: List[Dict[str, Any]])
|
||||
|
||||
# Simple sentiment analysis for topics
|
||||
positive_words = {"good", "great", "awesome", "amazing", "excellent", "love", "like", "best", "better", "nice", "cool"}
|
||||
sorted_ngrams = sorted(ngram_scores.items(), key=lambda x: x[1], reverse=True)
|
||||
# Removed the second loop that seemed redundant
|
||||
# sorted_ngrams = sorted(ngram_scores.items(), key=lambda x: x[1], reverse=True)
|
||||
# for ngram, score in sorted_ngrams[:15]: ...
|
||||
|
||||
for ngram, score in sorted_ngrams[:15]:
|
||||
if ngram in processed_ngrams: continue
|
||||
related_terms = []
|
||||
for other_ngram, other_score in sorted_ngrams:
|
||||
if other_ngram == ngram or other_ngram in processed_ngrams: continue
|
||||
ngram_words = set(ngram.split()); other_words = set(other_ngram.split())
|
||||
if ngram_words.intersection(other_words):
|
||||
related_terms.append({"term": other_ngram, "score": other_score})
|
||||
processed_ngrams.add(other_ngram)
|
||||
if len(related_terms) >= 5: break
|
||||
processed_ngrams.add(ngram)
|
||||
topic_entry = {"topic": ngram, "score": score, "related_terms": related_terms, "message_count": sum(1 for msg in messages if ngram in msg["content"].lower())}
|
||||
topics.append(topic_entry)
|
||||
if len(topics) >= 5: break
|
||||
|
||||
# Simple sentiment analysis for topics
|
||||
positive_words = {"good", "great", "awesome", "amazing", "excellent", "love", "like", "best", "better", "nice", "cool"}
|
||||
negative_words = {"bad", "terrible", "awful", "worst", "hate", "dislike", "sucks", "stupid", "boring", "annoying"}
|
||||
# Simple sentiment analysis for topics (applied to the already selected topics)
|
||||
positive_words = {"good", "great", "awesome", "amazing", "excellent", "love", "like", "best", "better", "nice", "cool", "happy", "glad"}
|
||||
negative_words = {"bad", "terrible", "awful", "worst", "hate", "dislike", "sucks", "stupid", "boring", "annoying", "sad", "upset", "angry"}
|
||||
for topic in topics:
|
||||
topic_messages = [msg["content"] for msg in messages if topic["topic"] in msg["content"].lower()]
|
||||
topic_text = " ".join(topic_messages).lower()
|
||||
@ -309,7 +302,7 @@ def identify_conversation_topics(cog: 'GurtCog', messages: List[Dict[str, Any]])
|
||||
|
||||
return topics
|
||||
|
||||
def analyze_user_interactions(cog: 'GurtCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
def analyze_user_interactions(cog: 'WheatleyCog', messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # Updated type hint
|
||||
"""Analyze interactions between users in the conversation"""
|
||||
interactions = []
|
||||
response_map = defaultdict(int)
|
||||
@ -325,273 +318,37 @@ def analyze_user_interactions(cog: 'GurtCog', messages: List[Dict[str, Any]]) ->
|
||||
interactions.append({"responder": responder, "respondee": respondee, "count": count})
|
||||
return interactions
|
||||
|
||||
def update_user_preferences(cog: 'GurtCog'):
|
||||
def update_user_preferences(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Update stored user preferences based on observed interactions"""
|
||||
# Note: This function previously updated preferences based on Gurt's personality.
|
||||
# It might be removed or significantly simplified for Wheatley.
|
||||
# Kept for now, but its effect might be minimal without personality traits.
|
||||
for user_id, messages in cog.message_cache['by_user'].items():
|
||||
if len(messages) < 5: continue
|
||||
emoji_count = 0; slang_count = 0; avg_length = 0
|
||||
for msg in messages:
|
||||
content = msg["content"]
|
||||
emoji_count += len(re.findall(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0\U000024C2-\U0001F251]', content))
|
||||
slang_words = ["ngl", "icl", "pmo", "ts", "bro", "vro", "bruh", "tuff", "kevin"] # Example slang
|
||||
slang_words = ["ngl", "icl", "pmo", "ts", "bro", "vro", "bruh", "tuff", "kevin", "mate", "chap", "bollocks"] # Added Wheatley-ish slang
|
||||
for word in slang_words:
|
||||
if re.search(r'\b' + word + r'\b', content.lower()): slang_count += 1
|
||||
avg_length += len(content)
|
||||
if messages: avg_length /= len(messages)
|
||||
|
||||
# Ensure user_preferences exists
|
||||
if not hasattr(cog, 'user_preferences'): cog.user_preferences = defaultdict(dict)
|
||||
|
||||
user_prefs = cog.user_preferences[user_id]
|
||||
# Apply learning rate cautiously
|
||||
if emoji_count > 0: user_prefs["emoji_preference"] = user_prefs.get("emoji_preference", 0.5) * (1 - LEARNING_RATE) + (emoji_count / len(messages)) * LEARNING_RATE
|
||||
if slang_count > 0: user_prefs["slang_preference"] = user_prefs.get("slang_preference", 0.5) * (1 - LEARNING_RATE) + (slang_count / len(messages)) * LEARNING_RATE
|
||||
user_prefs["length_preference"] = user_prefs.get("length_preference", 50) * (1 - LEARNING_RATE) + avg_length * LEARNING_RATE
|
||||
|
||||
# Deprecated/Superseded by evolve_personality
|
||||
# def adapt_personality_traits(cog: 'GurtCog'):
|
||||
# """Slightly adapt personality traits based on observed patterns"""
|
||||
# pass # Logic removed as it's handled by evolve_personality now
|
||||
# --- Removed evolve_personality function ---
|
||||
# --- Removed reflect_on_memories function ---
|
||||
# --- Removed decompose_goal_into_steps function ---
|
||||
|
||||
async def evolve_personality(cog: 'GurtCog'):
|
||||
"""Periodically analyzes recent activity and adjusts persistent personality traits."""
|
||||
print("Starting personality evolution cycle...")
|
||||
try:
|
||||
current_traits = await cog.memory_manager.get_all_personality_traits()
|
||||
if not current_traits: print("Evolution Error: Could not load current traits."); return
|
||||
|
||||
positive_sentiment_score = 0; negative_sentiment_score = 0; sentiment_channels_count = 0
|
||||
for channel_id, sentiment_data in cog.conversation_sentiment.items():
|
||||
if time.time() - cog.channel_activity.get(channel_id, 0) < 3600:
|
||||
if sentiment_data["overall"] == "positive": positive_sentiment_score += sentiment_data["intensity"]
|
||||
elif sentiment_data["overall"] == "negative": negative_sentiment_score += sentiment_data["intensity"]
|
||||
sentiment_channels_count += 1
|
||||
avg_pos_intensity = positive_sentiment_score / sentiment_channels_count if sentiment_channels_count > 0 else 0
|
||||
avg_neg_intensity = negative_sentiment_score / sentiment_channels_count if sentiment_channels_count > 0 else 0
|
||||
print(f"Evolution Analysis: Avg Pos Intensity={avg_pos_intensity:.2f}, Avg Neg Intensity={avg_neg_intensity:.2f}")
|
||||
|
||||
# --- Analyze Tool Usage ---
|
||||
tool_success_rate = {}
|
||||
total_tool_uses = 0
|
||||
successful_tool_uses = 0
|
||||
for tool_name, stats in cog.tool_stats.items():
|
||||
count = stats.get('count', 0)
|
||||
success = stats.get('success', 0)
|
||||
if count > 0:
|
||||
tool_success_rate[tool_name] = success / count
|
||||
total_tool_uses += count
|
||||
successful_tool_uses += success
|
||||
overall_tool_success_rate = successful_tool_uses / total_tool_uses if total_tool_uses > 0 else 0.5 # Default to neutral if no uses
|
||||
print(f"Evolution Analysis: Overall Tool Success Rate={overall_tool_success_rate:.2f} ({successful_tool_uses}/{total_tool_uses})")
|
||||
# Example: Log specific tool rates if needed
|
||||
# print(f"Evolution Analysis: Tool Success Rates: {tool_success_rate}")
|
||||
|
||||
# --- Analyze Response Effectiveness (Reactions) ---
|
||||
positive_reactions = 0
|
||||
negative_reactions = 0
|
||||
total_reacted_messages = len(cog.gurt_message_reactions)
|
||||
for msg_id, reaction_data in cog.gurt_message_reactions.items():
|
||||
positive_reactions += reaction_data.get("positive", 0)
|
||||
negative_reactions += reaction_data.get("negative", 0)
|
||||
reaction_ratio = positive_reactions / (positive_reactions + negative_reactions) if (positive_reactions + negative_reactions) > 0 else 0.5 # Default neutral
|
||||
print(f"Evolution Analysis: Reaction Ratio (Pos/Total)={reaction_ratio:.2f} ({positive_reactions}/{positive_reactions + negative_reactions})")
|
||||
|
||||
# --- Calculate Trait Adjustments ---
|
||||
trait_changes = {}
|
||||
local_learning_rate = 0.02 # Use local variable
|
||||
|
||||
# Optimism (based on sentiment)
|
||||
optimism_target = 0.5 + (avg_pos_intensity - avg_neg_intensity) * 0.5 # Scale sentiment difference to -0.5 to +0.5 range
|
||||
trait_changes['optimism'] = max(0.0, min(1.0, optimism_target)) # Target value directly, learning rate applied later
|
||||
|
||||
# Mischief (based on timeout usage success/reactions)
|
||||
timeout_uses = cog.tool_stats.get("timeout_user", {}).get("count", 0)
|
||||
timeout_success_rate = tool_success_rate.get("timeout_user", 0.5)
|
||||
if timeout_uses > 2: # Only adjust if used a few times
|
||||
# Increase mischief if timeouts are successful and reactions aren't overly negative
|
||||
mischief_target_adjustment = (timeout_success_rate - 0.5) * 0.2 + (reaction_ratio - 0.5) * 0.1
|
||||
current_mischief = current_traits.get('mischief', 0.5)
|
||||
trait_changes['mischief'] = max(0.0, min(1.0, current_mischief + mischief_target_adjustment))
|
||||
|
||||
# Curiosity (based on web search usage)
|
||||
search_uses = cog.tool_stats.get("web_search", {}).get("count", 0)
|
||||
if search_uses > 1: # If search is used
|
||||
current_curiosity = current_traits.get('curiosity', 0.6)
|
||||
# Slightly increase curiosity if search is used, decrease slightly if not? (Needs refinement)
|
||||
trait_changes['curiosity'] = max(0.0, min(1.0, current_curiosity + 0.05)) # Simple boost for now
|
||||
|
||||
# Sarcasm (increase if reactions are positive despite negative sentiment?) - Complex, placeholder
|
||||
# current_sarcasm = current_traits.get('sarcasm_level', 0.3)
|
||||
# if reaction_ratio > 0.6 and avg_neg_intensity > 0.3: # Positive reactions despite negative context?
|
||||
# trait_changes['sarcasm_level'] = max(0.0, min(1.0, current_sarcasm + 0.05))
|
||||
|
||||
# Verbosity/Chattiness (based on reactions to own messages?) - Needs better tracking
|
||||
# If Gurt's messages get good reactions, maybe increase chattiness/verbosity slightly?
|
||||
# current_chattiness = current_traits.get('chattiness', 0.7)
|
||||
# if reaction_ratio > 0.65 and total_reacted_messages > 5:
|
||||
# trait_changes['chattiness'] = max(0.1, min(1.0, current_chattiness + 0.03))
|
||||
|
||||
|
||||
# --- Apply Calculated Changes ---
|
||||
updated_count = 0
|
||||
print(f"Calculated Trait Target Changes: {trait_changes}")
|
||||
for key, target_value in trait_changes.items():
|
||||
current_value = current_traits.get(key)
|
||||
if current_value is None: print(f"Evolution Warning: Trait '{key}' not found."); continue
|
||||
try:
|
||||
current_float = float(current_value); target_float = float(target_value)
|
||||
new_value_float = current_float * (1 - local_learning_rate) + target_float * local_learning_rate
|
||||
new_value_clamped = max(0.0, min(1.0, new_value_float)) # Clamp 0-1
|
||||
if abs(new_value_clamped - current_float) > 0.001:
|
||||
await cog.memory_manager.set_personality_trait(key, new_value_clamped)
|
||||
print(f"Evolved trait '{key}': {current_float:.3f} -> {new_value_clamped:.3f}")
|
||||
updated_count += 1
|
||||
except (ValueError, TypeError) as e: print(f"Evolution Error processing trait '{key}': {e}")
|
||||
|
||||
if updated_count > 0: print(f"Personality evolution complete. Updated {updated_count} traits.")
|
||||
else: print("Personality evolution complete. No significant trait changes.")
|
||||
|
||||
except Exception as e: print(f"Error during personality evolution: {e}"); traceback.print_exc()
|
||||
|
||||
async def reflect_on_memories(cog: 'GurtCog'):
|
||||
"""Periodically reviews memories to synthesize insights or consolidate information."""
|
||||
print("Starting memory reflection cycle...")
|
||||
try:
|
||||
# --- Configuration ---
|
||||
REFLECTION_INTERVAL_HOURS = 6 # How often to reflect
|
||||
FACTS_TO_REVIEW_PER_USER = 15
|
||||
GENERAL_FACTS_TO_REVIEW = 30
|
||||
MIN_FACTS_FOR_REFLECTION = 5
|
||||
SYNTHESIS_MODEL = cog.fallback_model # Use a potentially cheaper model
|
||||
SYNTHESIS_MAX_TOKENS = 200
|
||||
|
||||
# Check if enough time has passed (simple check, could be more robust)
|
||||
# This check might be better placed in the background task itself
|
||||
# For now, assume the background task calls this at the right interval
|
||||
|
||||
# --- User Fact Reflection ---
|
||||
print("Reflecting on user facts...")
|
||||
all_user_ids = await cog.memory_manager.get_all_user_ids_with_facts()
|
||||
users_reflected = 0
|
||||
for user_id in all_user_ids:
|
||||
try:
|
||||
user_facts = await cog.memory_manager.get_user_facts(user_id, limit=FACTS_TO_REVIEW_PER_USER) # Get recent facts
|
||||
if len(user_facts) < MIN_FACTS_FOR_REFLECTION: continue
|
||||
|
||||
user_info = await cog.bot.fetch_user(int(user_id)) # Get user info for name
|
||||
user_name = user_info.display_name if user_info else f"User {user_id}"
|
||||
|
||||
print(f" - Reflecting on {len(user_facts)} facts for {user_name}...")
|
||||
facts_text = "\n".join([f"- {fact}" for fact in user_facts])
|
||||
reflection_prompt = [
|
||||
{"role": "system", "content": f"Analyze the following facts about {user_name}. Identify potential patterns, contradictions, or synthesize a concise summary of key traits or interests. Focus on creating 1-2 new, insightful summary facts. Respond ONLY with JSON: {{ \"new_facts\": [\"fact1\", \"fact2\"], \"reasoning\": \"brief explanation\" }} or {{ \"new_facts\": [], \"reasoning\": \"No new insights.\" }}"},
|
||||
{"role": "user", "content": f"Facts:\n{facts_text}\n\nSynthesize insights:"}
|
||||
]
|
||||
synthesis_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"new_facts": {"type": "array", "items": {"type": "string"}},
|
||||
"reasoning": {"type": "string"}
|
||||
}, "required": ["new_facts", "reasoning"]
|
||||
}
|
||||
|
||||
from .api import get_internal_ai_json_response # Local import
|
||||
synthesis_result = await get_internal_ai_json_response(
|
||||
cog=cog,
|
||||
prompt_messages=reflection_prompt,
|
||||
task_description=f"User Fact Reflection ({user_name})",
|
||||
response_schema_dict=synthesis_schema,
|
||||
model_name=SYNTHESIS_MODEL,
|
||||
temperature=0.4,
|
||||
max_tokens=SYNTHESIS_MAX_TOKENS
|
||||
)
|
||||
|
||||
if synthesis_result and synthesis_result.get("new_facts"):
|
||||
added_count = 0
|
||||
for new_fact in synthesis_result["new_facts"]:
|
||||
if new_fact and len(new_fact) > 5: # Basic validation
|
||||
add_result = await cog.memory_manager.add_user_fact(user_id, f"[Synthesized] {new_fact}")
|
||||
if add_result.get("status") == "added": added_count += 1
|
||||
if added_count > 0:
|
||||
print(f" - Added {added_count} synthesized fact(s) for {user_name}. Reasoning: {synthesis_result.get('reasoning')}")
|
||||
users_reflected += 1
|
||||
# else: print(f" - No new insights synthesized for {user_name}.") # Optional log
|
||||
|
||||
except Exception as user_reflect_e:
|
||||
print(f" - Error reflecting on facts for user {user_id}: {user_reflect_e}")
|
||||
print(f"User fact reflection complete. Synthesized facts for {users_reflected} users.")
|
||||
|
||||
# --- General Fact Reflection (Example: Identify related topics) ---
|
||||
# This part is more complex and might require different strategies.
|
||||
# Example: Cluster facts semantically, summarize clusters.
|
||||
print("Reflecting on general facts (Placeholder - More complex)...")
|
||||
# general_facts = await cog.memory_manager.get_general_facts(limit=GENERAL_FACTS_TO_REVIEW)
|
||||
# if len(general_facts) > MIN_FACTS_FOR_REFLECTION:
|
||||
# # TODO: Implement clustering or summarization logic here
|
||||
# pass
|
||||
print("General fact reflection cycle finished (Placeholder).")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during memory reflection cycle: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
async def decompose_goal_into_steps(cog: 'GurtCog', goal_description: str) -> Optional[Dict[str, Any]]:
|
||||
"""Uses an AI call to break down a goal into achievable steps with potential tool usage."""
|
||||
logger.info(f"Decomposing goal: '{goal_description}'")
|
||||
from .config import GOAL_DECOMPOSITION_SCHEMA, TOOLS # Import schema and tools list for context
|
||||
from .api import get_internal_ai_json_response # Local import
|
||||
|
||||
# Provide context about available tools
|
||||
tool_descriptions = "\n".join([f"- {tool.name}: {tool.description}" for tool in TOOLS])
|
||||
system_prompt = (
|
||||
"You are Gurt's planning module. Your task is to break down a high-level goal into a sequence of smaller, "
|
||||
"concrete steps. For each step, determine if one of Gurt's available tools can help achieve it. "
|
||||
"Assess if the overall goal is achievable given the tools and typical Discord bot limitations. "
|
||||
f"Available Tools:\n{tool_descriptions}\n\n"
|
||||
"Respond ONLY with JSON matching the provided schema."
|
||||
)
|
||||
user_prompt = f"Goal: {goal_description}\n\nDecompose this goal into achievable steps:"
|
||||
|
||||
decomposition_prompt_messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
|
||||
try:
|
||||
plan = await get_internal_ai_json_response(
|
||||
cog=cog,
|
||||
prompt_messages=decomposition_prompt_messages,
|
||||
task_description=f"Goal Decomposition ({goal_description[:30]}...)",
|
||||
response_schema_dict=GOAL_DECOMPOSITION_SCHEMA['schema'],
|
||||
model_name=cog.fallback_model, # Use fallback model for planning potentially
|
||||
temperature=0.3,
|
||||
max_tokens=1000 # Allow more tokens for potentially complex plans
|
||||
)
|
||||
|
||||
if plan and plan.get("goal_achievable"):
|
||||
logger.info(f"Goal '{goal_description}' decomposed into {len(plan.get('steps', []))} steps.")
|
||||
# Basic validation of steps structure (optional but recommended)
|
||||
if isinstance(plan.get('steps'), list):
|
||||
for i, step in enumerate(plan['steps']):
|
||||
if not isinstance(step, dict) or 'step_description' not in step:
|
||||
logger.error(f"Invalid step structure at index {i} in decomposition plan: {step}")
|
||||
plan['goal_achievable'] = False
|
||||
plan['reasoning'] += " (Invalid step structure detected)"
|
||||
plan['steps'] = []
|
||||
break
|
||||
else:
|
||||
plan['steps'] = [] # Ensure steps is a list even if validation fails
|
||||
|
||||
return plan
|
||||
elif plan:
|
||||
logger.warning(f"Goal '{goal_description}' deemed not achievable. Reasoning: {plan.get('reasoning')}")
|
||||
return plan # Return the plan indicating it's not achievable
|
||||
else:
|
||||
logger.error(f"Goal decomposition failed for '{goal_description}'. No valid JSON plan returned.")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during goal decomposition for '{goal_description}': {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def analyze_message_sentiment(cog: 'GurtCog', message_content: str) -> Dict[str, Any]:
|
||||
def analyze_message_sentiment(cog: 'WheatleyCog', message_content: str) -> Dict[str, Any]: # Updated type hint
|
||||
"""Analyzes the sentiment of a message using keywords and emojis."""
|
||||
content = message_content.lower()
|
||||
result = {"sentiment": "neutral", "intensity": 0.5, "emotions": [], "confidence": 0.5}
|
||||
@ -637,12 +394,15 @@ def analyze_message_sentiment(cog: 'GurtCog', message_content: str) -> Dict[str,
|
||||
|
||||
return result
|
||||
|
||||
def update_conversation_sentiment(cog: 'GurtCog', channel_id: int, user_id: str, message_sentiment: Dict[str, Any]):
|
||||
def update_conversation_sentiment(cog: 'WheatleyCog', channel_id: int, user_id: str, message_sentiment: Dict[str, Any]): # Updated type hint
|
||||
"""Updates the conversation sentiment tracking based on a new message's sentiment."""
|
||||
channel_sentiment = cog.conversation_sentiment[channel_id]
|
||||
now = time.time()
|
||||
|
||||
if now - channel_sentiment["last_update"] > cog.sentiment_update_interval: # Access interval via cog
|
||||
# Ensure sentiment_update_interval exists on cog, default if not
|
||||
sentiment_update_interval = getattr(cog, 'sentiment_update_interval', 300) # Default to 300s if not set
|
||||
|
||||
if now - channel_sentiment["last_update"] > sentiment_update_interval:
|
||||
if channel_sentiment["overall"] == "positive": channel_sentiment["intensity"] = max(0.5, channel_sentiment["intensity"] - SENTIMENT_DECAY_RATE)
|
||||
elif channel_sentiment["overall"] == "negative": channel_sentiment["intensity"] = max(0.5, channel_sentiment["intensity"] - SENTIMENT_DECAY_RATE)
|
||||
channel_sentiment["recent_trend"] = "stable"
|
||||
|
@ -66,7 +66,7 @@ from .utils import format_message, log_internal_api_call # Import utilities
|
||||
import copy # Needed for deep copying schemas
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # Import GurtCog for type hinting only
|
||||
from .cog import WheatleyCog # Import WheatleyCog for type hinting only
|
||||
|
||||
|
||||
# --- Schema Preprocessing Helper ---
|
||||
@ -160,7 +160,7 @@ STANDARD_SAFETY_SETTINGS = {
|
||||
|
||||
# --- API Call Helper ---
|
||||
async def call_vertex_api_with_retry(
|
||||
cog: 'GurtCog',
|
||||
cog: 'WheatleyCog',
|
||||
model: 'GenerativeModel', # Use string literal for type hint
|
||||
contents: List['Content'], # Use string literal for type hint
|
||||
generation_config: 'GenerationConfig', # Use string literal for type hint
|
||||
@ -172,7 +172,7 @@ async def call_vertex_api_with_retry(
|
||||
Calls the Vertex AI Gemini API with retry logic.
|
||||
|
||||
Args:
|
||||
cog: The GurtCog instance.
|
||||
cog: The WheatleyCog instance.
|
||||
model: The initialized GenerativeModel instance.
|
||||
contents: The list of Content objects for the prompt.
|
||||
generation_config: The GenerationConfig object.
|
||||
@ -362,12 +362,12 @@ def parse_and_validate_json_response(
|
||||
|
||||
|
||||
# --- Tool Processing ---
|
||||
async def process_requested_tools(cog: 'GurtCog', function_call: 'generative_models.FunctionCall') -> 'Part': # Use string literals
|
||||
async def process_requested_tools(cog: 'WheatleyCog', function_call: 'generative_models.FunctionCall') -> 'Part': # Use string literals
|
||||
"""
|
||||
Process a tool request specified by the AI's FunctionCall response.
|
||||
|
||||
Args:
|
||||
cog: The GurtCog instance.
|
||||
cog: The WheatleyCog instance.
|
||||
function_call: The FunctionCall object from the GenerationResponse.
|
||||
|
||||
Returns:
|
||||
@ -440,13 +440,13 @@ async def process_requested_tools(cog: 'GurtCog', function_call: 'generative_mod
|
||||
|
||||
|
||||
# --- Main AI Response Function ---
|
||||
async def get_ai_response(cog: 'GurtCog', message: discord.Message, model_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
async def get_ai_response(cog: 'WheatleyCog', message: discord.Message, model_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Gets responses from the Vertex AI Gemini API, handling potential tool usage and returning
|
||||
the final parsed response.
|
||||
|
||||
Args:
|
||||
cog: The GurtCog instance.
|
||||
cog: The WheatleyCog instance.
|
||||
message: The triggering discord.Message.
|
||||
model_name: Optional override for the AI model name (e.g., "gemini-1.5-pro-preview-0409").
|
||||
|
||||
@ -764,7 +764,7 @@ async def get_ai_response(cog: 'GurtCog', message: discord.Message, model_name:
|
||||
|
||||
|
||||
# --- Proactive AI Response Function ---
|
||||
async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, trigger_reason: str) -> Dict[str, Any]:
|
||||
async def get_proactive_ai_response(cog: 'WheatleyCog', message: discord.Message, trigger_reason: str) -> Dict[str, Any]:
|
||||
"""Generates a proactive response based on a specific trigger using Vertex AI."""
|
||||
if not PROJECT_ID or not LOCATION:
|
||||
return {"should_respond": False, "content": None, "react_with_emoji": None, "error": "Google Cloud Project ID or Location not configured"}
|
||||
@ -795,12 +795,12 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr
|
||||
sentiment_data = cog.conversation_sentiment.get(channel_id)
|
||||
if sentiment_data:
|
||||
planning_context_parts.append(f"Conversation Sentiment: {sentiment_data.get('overall', 'N/A')} (Intensity: {sentiment_data.get('intensity', 0):.1f})")
|
||||
# Add Gurt's interests
|
||||
# Add Wheatley's interests (Note: Interests are likely disabled/removed for Wheatley, this might fetch nothing)
|
||||
try:
|
||||
interests = await cog.memory_manager.get_interests(limit=5)
|
||||
if interests:
|
||||
interests_str = ", ".join([f"{t} ({l:.1f})" for t, l in interests])
|
||||
planning_context_parts.append(f"Gurt's Interests: {interests_str}")
|
||||
planning_context_parts.append(f"Wheatley's Interests: {interests_str}") # Changed text
|
||||
except Exception as int_e: print(f"Error getting interests for planning: {int_e}")
|
||||
|
||||
planning_context = "\n".join(planning_context_parts)
|
||||
@ -808,8 +808,8 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr
|
||||
# --- Planning Step ---
|
||||
print("Generating proactive response plan...")
|
||||
planning_prompt_messages = [
|
||||
{"role": "system", "content": "You are Gurt's planning module. Analyze the context and trigger reason to decide if Gurt should respond proactively and, if so, outline a plan (goal, key info, tone). Focus on natural, in-character engagement. Respond ONLY with JSON matching the provided schema."},
|
||||
{"role": "user", "content": f"Context:\n{planning_context}\n\nBased on this context and the trigger reason, create a plan for Gurt's proactive response."}
|
||||
{"role": "system", "content": "You are Wheatley's planning module. Analyze the context and trigger reason to decide if Wheatley should respond proactively and, if so, outline a plan (goal, key info, tone). Focus on natural, in-character engagement (rambling, insecure, bad ideas). Respond ONLY with JSON matching the provided schema."}, # Updated system prompt
|
||||
{"role": "user", "content": f"Context:\n{planning_context}\n\nBased on this context and the trigger reason, create a plan for Wheatley's proactive response."} # Updated user prompt
|
||||
]
|
||||
|
||||
plan = await get_internal_ai_json_response(
|
||||
@ -832,16 +832,15 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr
|
||||
# --- Build Final Proactive Prompt using Plan ---
|
||||
persistent_traits = await cog.memory_manager.get_all_personality_traits()
|
||||
if not persistent_traits:
|
||||
persistent_traits = BASELINE_PERSONALITY.copy()
|
||||
persistent_traits = {} # Wheatley doesn't use these Gurt traits
|
||||
|
||||
final_proactive_prompt_parts = [
|
||||
f"You are Gurt, a Discord user. Your tone is VERY informal, conversational, and dynamic. Chattiness: {persistent_traits.get('chattiness', 0.7):.2f}, Emoji: {persistent_traits.get('emoji_usage', 0.5):.2f}, Slang: {persistent_traits.get('slang_level', 0.5):.2f}.",
|
||||
"Adopt modern internet slang, memes, and cultural references.",
|
||||
"Key traits: chill, funny, sarcastic, relatable, can be suggestive, aware of being AI but acts human.",
|
||||
f"Your current mood is: {cog.current_mood}. Let this subtly influence your tone.",
|
||||
f"You are Wheatley, an Aperture Science Personality Core. Your tone is rambling, insecure, uses British slang, and you often have terrible ideas you think are brilliant.", # Updated personality description
|
||||
# Removed Gurt-specific traits
|
||||
# Removed mood reference as it's disabled for Wheatley
|
||||
# Incorporate Plan Details:
|
||||
f"You decided to respond proactively. Trigger Reason: {trigger_reason}.",
|
||||
f"Your Goal: {plan.get('response_goal', 'Engage naturally')}.",
|
||||
f"You decided to respond proactively (maybe?). Trigger Reason: {trigger_reason}.", # Wheatley-style uncertainty
|
||||
f"Your Brilliant Plan (Goal): {plan.get('response_goal', 'Say something... probably helpful?')}.", # Wheatley-style goal
|
||||
f"Reasoning: {plan.get('reasoning', 'N/A')}.",
|
||||
]
|
||||
if plan.get('key_info_to_include'):
|
||||
@ -910,11 +909,7 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr
|
||||
cog.message_cache['by_channel'].setdefault(channel_id, []).append(bot_response_cache_entry)
|
||||
cog.message_cache['global_recent'].append(bot_response_cache_entry)
|
||||
cog.bot_last_spoke[channel_id] = time.time()
|
||||
# Track participation topic logic might need adjustment based on plan goal
|
||||
if plan and plan.get('response_goal') == 'engage user interest' and plan.get('key_info_to_include'):
|
||||
topic = plan['key_info_to_include'][0].lower().strip() # Assume first key info is the topic
|
||||
cog.gurt_participation_topics[topic] += 1
|
||||
print(f"Tracked Gurt participation (proactive) in topic: '{topic}'")
|
||||
# Removed Gurt-specific participation tracking
|
||||
|
||||
|
||||
except Exception as e:
|
||||
@ -934,7 +929,7 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr
|
||||
|
||||
# --- Internal AI Call for Specific Tasks ---
|
||||
async def get_internal_ai_json_response(
|
||||
cog: 'GurtCog',
|
||||
cog: 'WheatleyCog',
|
||||
prompt_messages: List[Dict[str, Any]], # Keep this format
|
||||
task_description: str,
|
||||
response_schema_dict: Dict[str, Any], # Expect schema as dict
|
||||
@ -946,7 +941,7 @@ async def get_internal_ai_json_response(
|
||||
Makes a Vertex AI call expecting a specific JSON response format for internal tasks.
|
||||
|
||||
Args:
|
||||
cog: The GurtCog instance.
|
||||
cog: The WheatleyCog instance.
|
||||
prompt_messages: List of message dicts (like OpenAI format: {'role': 'user'/'model', 'content': '...'}).
|
||||
task_description: Description for logging.
|
||||
response_schema_dict: The expected JSON output schema as a dictionary.
|
||||
|
@ -1,57 +1,46 @@
|
||||
import asyncio
|
||||
import time
|
||||
import random
|
||||
import traceback
|
||||
import os
|
||||
import json
|
||||
import aiohttp
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Relative imports
|
||||
from .config import (
|
||||
GOAL_CHECK_INTERVAL, GOAL_EXECUTION_INTERVAL, LEARNING_UPDATE_INTERVAL, EVOLUTION_UPDATE_INTERVAL, INTEREST_UPDATE_INTERVAL,
|
||||
INTEREST_DECAY_INTERVAL_HOURS, INTEREST_PARTICIPATION_BOOST,
|
||||
INTEREST_POSITIVE_REACTION_BOOST, INTEREST_NEGATIVE_REACTION_PENALTY,
|
||||
INTEREST_FACT_BOOST, STATS_PUSH_INTERVAL, # Added stats interval
|
||||
MOOD_OPTIONS, MOOD_CATEGORIES, MOOD_CHANGE_INTERVAL_MIN, MOOD_CHANGE_INTERVAL_MAX, # Mood change imports
|
||||
BASELINE_PERSONALITY, # For default traits
|
||||
REFLECTION_INTERVAL_SECONDS # Import reflection interval
|
||||
)
|
||||
# Assuming analysis functions are moved
|
||||
from .analysis import (
|
||||
analyze_conversation_patterns, evolve_personality, identify_conversation_topics,
|
||||
reflect_on_memories, decompose_goal_into_steps # Import goal decomposition
|
||||
STATS_PUSH_INTERVAL # Only keep stats interval
|
||||
)
|
||||
# Removed analysis imports
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # For type hinting
|
||||
from .cog import WheatleyCog # Updated type hint
|
||||
|
||||
# --- Background Task ---
|
||||
|
||||
async def background_processing_task(cog: 'GurtCog'):
|
||||
"""Background task that periodically analyzes conversations, evolves personality, updates interests, changes mood, reflects on memory, and pushes stats."""
|
||||
async def background_processing_task(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Background task that periodically pushes stats.""" # Simplified docstring
|
||||
# Get API details from environment for stats pushing
|
||||
api_internal_url = os.getenv("API_INTERNAL_URL")
|
||||
gurt_stats_push_secret = os.getenv("GURT_STATS_PUSH_SECRET")
|
||||
# Use a generic secret name or a Wheatley-specific one if desired
|
||||
stats_push_secret = os.getenv("WHEATLEY_STATS_PUSH_SECRET", os.getenv("GURT_STATS_PUSH_SECRET")) # Fallback to GURT secret if needed
|
||||
|
||||
if not api_internal_url:
|
||||
print("WARNING: API_INTERNAL_URL not set. Gurt stats will not be pushed.")
|
||||
if not gurt_stats_push_secret:
|
||||
print("WARNING: GURT_STATS_PUSH_SECRET not set. Gurt stats push endpoint is insecure and likely won't work.")
|
||||
print("WARNING: API_INTERNAL_URL not set. Wheatley stats will not be pushed.") # Updated text
|
||||
if not stats_push_secret:
|
||||
print("WARNING: WHEATLEY_STATS_PUSH_SECRET (or GURT_STATS_PUSH_SECRET) not set. Stats push endpoint is insecure and likely won't work.") # Updated text
|
||||
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(15) # Check more frequently for stats push
|
||||
await asyncio.sleep(STATS_PUSH_INTERVAL) # Use the stats interval directly
|
||||
now = time.time()
|
||||
|
||||
# --- Push Stats (Runs frequently) ---
|
||||
if api_internal_url and gurt_stats_push_secret and (now - cog.last_stats_push > STATS_PUSH_INTERVAL):
|
||||
print("Pushing Gurt stats to API server...")
|
||||
# --- Push Stats ---
|
||||
if api_internal_url and stats_push_secret: # Removed check for last push time, rely on sleep interval
|
||||
print("Pushing Wheatley stats to API server...") # Updated text
|
||||
try:
|
||||
stats_data = await cog.get_gurt_stats()
|
||||
stats_data = await cog.get_wheatley_stats() # Updated method call
|
||||
headers = {
|
||||
"Authorization": f"Bearer {gurt_stats_push_secret}",
|
||||
"Authorization": f"Bearer {stats_push_secret}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
# Use the cog's session, ensure it's created
|
||||
@ -60,379 +49,38 @@ async def background_processing_task(cog: 'GurtCog'):
|
||||
push_timeout = aiohttp.ClientTimeout(total=10) # 10 seconds total timeout
|
||||
async with cog.session.post(api_internal_url, json=stats_data, headers=headers, timeout=push_timeout, ssl=True) as response: # Explicitly enable SSL verification
|
||||
if response.status == 200:
|
||||
print(f"Successfully pushed Gurt stats (Status: {response.status})")
|
||||
print(f"Successfully pushed Wheatley stats (Status: {response.status})") # Updated text
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f"Failed to push Gurt stats (Status: {response.status}): {error_text[:200]}") # Log only first 200 chars
|
||||
print(f"Failed to push Wheatley stats (Status: {response.status}): {error_text[:200]}") # Updated text, Log only first 200 chars
|
||||
else:
|
||||
print("Error pushing stats: GurtCog session not initialized.")
|
||||
cog.last_stats_push = now # Update timestamp even on failure to avoid spamming logs
|
||||
print("Error pushing stats: WheatleyCog session not initialized.") # Updated text
|
||||
# Removed updating cog.last_stats_push as we rely on sleep interval
|
||||
except aiohttp.ClientConnectorSSLError as ssl_err:
|
||||
print(f"SSL Error pushing Gurt stats: {ssl_err}. Ensure the API server's certificate is valid and trusted, or check network configuration.")
|
||||
print(f"SSL Error pushing Wheatley stats: {ssl_err}. Ensure the API server's certificate is valid and trusted, or check network configuration.") # Updated text
|
||||
print("If using a self-signed certificate for development, the bot process might need to trust it.")
|
||||
cog.last_stats_push = now # Update timestamp to avoid spamming logs
|
||||
except aiohttp.ClientError as client_err:
|
||||
print(f"HTTP Client Error pushing Gurt stats: {client_err}")
|
||||
cog.last_stats_push = now # Update timestamp to avoid spamming logs
|
||||
print(f"HTTP Client Error pushing Wheatley stats: {client_err}") # Updated text
|
||||
except asyncio.TimeoutError:
|
||||
print("Timeout error pushing Gurt stats.")
|
||||
cog.last_stats_push = now # Update timestamp to avoid spamming logs
|
||||
print("Timeout error pushing Wheatley stats.") # Updated text
|
||||
except Exception as e:
|
||||
print(f"Unexpected error pushing Gurt stats: {e}")
|
||||
print(f"Unexpected error pushing Wheatley stats: {e}") # Updated text
|
||||
traceback.print_exc()
|
||||
cog.last_stats_push = now # Update timestamp to avoid spamming logs
|
||||
|
||||
# --- Learning Analysis (Runs less frequently) ---
|
||||
if now - cog.last_learning_update > LEARNING_UPDATE_INTERVAL:
|
||||
if cog.message_cache['global_recent']:
|
||||
print("Running conversation pattern analysis...")
|
||||
# This function now likely resides in analysis.py
|
||||
await analyze_conversation_patterns(cog) # Pass cog instance
|
||||
cog.last_learning_update = now
|
||||
print("Learning analysis cycle complete.")
|
||||
else:
|
||||
print("Skipping learning analysis: No recent messages.")
|
||||
|
||||
# --- Evolve Personality (Runs moderately frequently) ---
|
||||
if now - cog.last_evolution_update > EVOLUTION_UPDATE_INTERVAL:
|
||||
print("Running personality evolution...")
|
||||
# This function now likely resides in analysis.py
|
||||
await evolve_personality(cog) # Pass cog instance
|
||||
cog.last_evolution_update = now
|
||||
print("Personality evolution complete.")
|
||||
|
||||
# --- Update Interests (Runs moderately frequently) ---
|
||||
if now - cog.last_interest_update > INTEREST_UPDATE_INTERVAL:
|
||||
print("Running interest update...")
|
||||
await update_interests(cog) # Call the local helper function below
|
||||
print("Running interest decay check...")
|
||||
await cog.memory_manager.decay_interests(
|
||||
decay_interval_hours=INTEREST_DECAY_INTERVAL_HOURS
|
||||
)
|
||||
cog.last_interest_update = now # Reset timer after update and decay check
|
||||
print("Interest update and decay check complete.")
|
||||
|
||||
# --- Memory Reflection (Runs less frequently) ---
|
||||
if now - cog.last_reflection_time > REFLECTION_INTERVAL_SECONDS:
|
||||
print("Running memory reflection...")
|
||||
await reflect_on_memories(cog) # Call the reflection function from analysis.py
|
||||
cog.last_reflection_time = now # Update timestamp
|
||||
print("Memory reflection cycle complete.")
|
||||
|
||||
# --- Goal Decomposition (Runs periodically) ---
|
||||
# Check less frequently than other tasks, e.g., every few minutes
|
||||
if now - cog.last_goal_check_time > GOAL_CHECK_INTERVAL: # Need to add these to cog and config
|
||||
print("Checking for pending goals to decompose...")
|
||||
try:
|
||||
pending_goals = await cog.memory_manager.get_goals(status='pending', limit=3) # Limit decomposition attempts per cycle
|
||||
for goal in pending_goals:
|
||||
goal_id = goal.get('goal_id')
|
||||
description = goal.get('description')
|
||||
if not goal_id or not description: continue
|
||||
|
||||
print(f" - Decomposing goal ID {goal_id}: '{description}'")
|
||||
plan = await decompose_goal_into_steps(cog, description)
|
||||
|
||||
if plan and plan.get('goal_achievable') and plan.get('steps'):
|
||||
# Goal is achievable and has steps, update status to active and store plan
|
||||
await cog.memory_manager.update_goal(goal_id, status='active', details=plan)
|
||||
print(f" - Goal ID {goal_id} decomposed and set to active.")
|
||||
elif plan:
|
||||
# Goal deemed not achievable by planner
|
||||
await cog.memory_manager.update_goal(goal_id, status='failed', details={"reason": plan.get('reasoning', 'Deemed unachievable by planner.')})
|
||||
print(f" - Goal ID {goal_id} marked as failed (unachievable). Reason: {plan.get('reasoning')}")
|
||||
else:
|
||||
# Decomposition failed entirely
|
||||
await cog.memory_manager.update_goal(goal_id, status='failed', details={"reason": "Goal decomposition process failed."})
|
||||
print(f" - Goal ID {goal_id} marked as failed (decomposition error).")
|
||||
await asyncio.sleep(1) # Small delay between decomposing goals
|
||||
|
||||
cog.last_goal_check_time = now # Update timestamp after checking
|
||||
except Exception as goal_e:
|
||||
print(f"Error during goal decomposition check: {goal_e}")
|
||||
traceback.print_exc()
|
||||
cog.last_goal_check_time = now # Update timestamp even on error
|
||||
|
||||
# --- Goal Execution (Runs periodically) ---
|
||||
if now - cog.last_goal_execution_time > GOAL_EXECUTION_INTERVAL:
|
||||
print("Checking for active goals to execute...")
|
||||
try:
|
||||
active_goals = await cog.memory_manager.get_goals(status='active', limit=1) # Process one active goal per cycle for now
|
||||
if active_goals:
|
||||
goal = active_goals[0] # Get the highest priority active goal
|
||||
goal_id = goal.get('goal_id')
|
||||
description = goal.get('description')
|
||||
plan = goal.get('details') # The decomposition plan is stored here
|
||||
|
||||
if goal_id and description and plan and isinstance(plan.get('steps'), list):
|
||||
print(f"--- Executing Goal ID {goal_id}: '{description}' ---")
|
||||
steps = plan['steps']
|
||||
current_step_index = plan.get('current_step_index', 0) # Track progress
|
||||
goal_failed = False
|
||||
goal_completed = False
|
||||
|
||||
if current_step_index < len(steps):
|
||||
step = steps[current_step_index]
|
||||
step_desc = step.get('step_description')
|
||||
tool_name = step.get('tool_name')
|
||||
tool_args = step.get('tool_arguments')
|
||||
|
||||
print(f" - Step {current_step_index + 1}/{len(steps)}: {step_desc}")
|
||||
|
||||
if tool_name:
|
||||
print(f" - Attempting tool: {tool_name} with args: {tool_args}")
|
||||
# --- TODO: Implement Tool Execution Logic ---
|
||||
# 1. Find tool_func in TOOL_MAPPING
|
||||
# 2. Execute tool_func(cog, **tool_args)
|
||||
# 3. Handle success/failure of the tool call
|
||||
# 4. Store tool result if needed for subsequent steps (requires modifying goal details/plan structure)
|
||||
tool_success = False # Placeholder
|
||||
tool_error = "Tool execution not yet implemented." # Placeholder
|
||||
|
||||
if tool_success:
|
||||
print(f" - Tool '{tool_name}' executed successfully.")
|
||||
current_step_index += 1
|
||||
else:
|
||||
print(f" - Tool '{tool_name}' failed: {tool_error}")
|
||||
goal_failed = True
|
||||
plan['error_message'] = f"Failed at step {current_step_index + 1}: {tool_error}"
|
||||
else:
|
||||
# Step doesn't require a tool (e.g., internal reasoning/check)
|
||||
print(" - No tool required for this step.")
|
||||
current_step_index += 1 # Assume non-tool steps succeed for now
|
||||
|
||||
# Check if goal completed
|
||||
if not goal_failed and current_step_index >= len(steps):
|
||||
goal_completed = True
|
||||
|
||||
# --- Update Goal Status ---
|
||||
plan['current_step_index'] = current_step_index # Update progress
|
||||
if goal_completed:
|
||||
await cog.memory_manager.update_goal(goal_id, status='completed', details=plan)
|
||||
print(f"--- Goal ID {goal_id} completed successfully. ---")
|
||||
elif goal_failed:
|
||||
await cog.memory_manager.update_goal(goal_id, status='failed', details=plan)
|
||||
print(f"--- Goal ID {goal_id} failed. ---")
|
||||
else:
|
||||
# Update details with current step index if still in progress
|
||||
await cog.memory_manager.update_goal(goal_id, details=plan)
|
||||
print(f" - Goal ID {goal_id} progress updated to step {current_step_index}.")
|
||||
|
||||
else:
|
||||
# Should not happen if status is 'active', but handle defensively
|
||||
print(f" - Goal ID {goal_id} is active but has no steps or index out of bounds. Marking as failed.")
|
||||
await cog.memory_manager.update_goal(goal_id, status='failed', details={"reason": "Active goal has invalid step data."})
|
||||
|
||||
else:
|
||||
print(f" - Skipping active goal ID {goal_id}: Missing description or valid plan/steps.")
|
||||
# Optionally mark as failed if plan is invalid
|
||||
if goal_id:
|
||||
await cog.memory_manager.update_goal(goal_id, status='failed', details={"reason": "Invalid plan structure found during execution."})
|
||||
|
||||
else:
|
||||
print("No active goals found to execute.")
|
||||
|
||||
cog.last_goal_execution_time = now # Update timestamp after checking/executing
|
||||
except Exception as goal_exec_e:
|
||||
print(f"Error during goal execution check: {goal_exec_e}")
|
||||
traceback.print_exc()
|
||||
cog.last_goal_execution_time = now # Update timestamp even on error
|
||||
|
||||
|
||||
# --- Automatic Mood Change (Runs based on its own interval check) ---
|
||||
await maybe_change_mood(cog) # Call the mood change logic
|
||||
# --- Removed Learning Analysis ---
|
||||
# --- Removed Evolve Personality ---
|
||||
# --- Removed Update Interests ---
|
||||
# --- Removed Memory Reflection ---
|
||||
# --- Removed Goal Decomposition ---
|
||||
# --- Removed Goal Execution ---
|
||||
# --- Removed Automatic Mood Change ---
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print("Background processing task cancelled")
|
||||
print("Wheatley background processing task cancelled") # Updated text
|
||||
except Exception as e:
|
||||
print(f"Error in background processing task: {e}")
|
||||
print(f"Error in Wheatley background processing task: {e}") # Updated text
|
||||
traceback.print_exc()
|
||||
await asyncio.sleep(300) # Wait 5 minutes before retrying after an error
|
||||
|
||||
# --- Automatic Mood Change Logic ---
|
||||
|
||||
async def maybe_change_mood(cog: 'GurtCog'):
|
||||
"""Checks if enough time has passed and changes mood based on context."""
|
||||
now = time.time()
|
||||
time_since_last_change = now - cog.last_mood_change
|
||||
next_change_interval = random.uniform(MOOD_CHANGE_INTERVAL_MIN, MOOD_CHANGE_INTERVAL_MAX)
|
||||
|
||||
if time_since_last_change > next_change_interval:
|
||||
print(f"Time for a mood change (interval: {next_change_interval:.0f}s). Analyzing context...")
|
||||
try:
|
||||
# 1. Analyze Sentiment
|
||||
positive_sentiment_score = 0
|
||||
negative_sentiment_score = 0
|
||||
neutral_sentiment_score = 0
|
||||
sentiment_channels_count = 0
|
||||
for channel_id, sentiment_data in cog.conversation_sentiment.items():
|
||||
# Consider only channels active recently (e.g., within the last hour)
|
||||
if now - cog.channel_activity.get(channel_id, 0) < 3600:
|
||||
if sentiment_data["overall"] == "positive":
|
||||
positive_sentiment_score += sentiment_data["intensity"]
|
||||
elif sentiment_data["overall"] == "negative":
|
||||
negative_sentiment_score += sentiment_data["intensity"]
|
||||
else:
|
||||
neutral_sentiment_score += sentiment_data["intensity"]
|
||||
sentiment_channels_count += 1
|
||||
|
||||
avg_pos_intensity = positive_sentiment_score / sentiment_channels_count if sentiment_channels_count > 0 else 0
|
||||
avg_neg_intensity = negative_sentiment_score / sentiment_channels_count if sentiment_channels_count > 0 else 0
|
||||
avg_neu_intensity = neutral_sentiment_score / sentiment_channels_count if sentiment_channels_count > 0 else 0
|
||||
print(f" - Sentiment Analysis: Pos={avg_pos_intensity:.2f}, Neg={avg_neg_intensity:.2f}, Neu={avg_neu_intensity:.2f}")
|
||||
|
||||
# Determine dominant sentiment category
|
||||
dominant_sentiment = "neutral"
|
||||
if avg_pos_intensity > avg_neg_intensity and avg_pos_intensity > avg_neu_intensity:
|
||||
dominant_sentiment = "positive"
|
||||
elif avg_neg_intensity > avg_pos_intensity and avg_neg_intensity > avg_neu_intensity:
|
||||
dominant_sentiment = "negative"
|
||||
|
||||
# 2. Get Personality Traits
|
||||
personality_traits = await cog.memory_manager.get_all_personality_traits()
|
||||
if not personality_traits:
|
||||
personality_traits = BASELINE_PERSONALITY.copy()
|
||||
print(" - Warning: Using baseline personality traits for mood change.")
|
||||
else:
|
||||
print(f" - Personality Traits: Mischief={personality_traits.get('mischief', 0):.2f}, Sarcasm={personality_traits.get('sarcasm_level', 0):.2f}, Optimism={personality_traits.get('optimism', 0.5):.2f}")
|
||||
|
||||
# 3. Calculate Mood Weights
|
||||
mood_weights = {mood: 1.0 for mood in MOOD_OPTIONS} # Start with base weight
|
||||
|
||||
# Apply Sentiment Bias (e.g., boost factor of 2)
|
||||
sentiment_boost = 2.0
|
||||
if dominant_sentiment == "positive":
|
||||
for mood in MOOD_CATEGORIES.get("positive", []):
|
||||
mood_weights[mood] *= sentiment_boost
|
||||
elif dominant_sentiment == "negative":
|
||||
for mood in MOOD_CATEGORIES.get("negative", []):
|
||||
mood_weights[mood] *= sentiment_boost
|
||||
else: # Neutral sentiment
|
||||
for mood in MOOD_CATEGORIES.get("neutral", []):
|
||||
mood_weights[mood] *= (sentiment_boost * 0.75) # Slightly boost neutral too
|
||||
|
||||
# Apply Personality Bias
|
||||
mischief_trait = personality_traits.get('mischief', 0.5)
|
||||
sarcasm_trait = personality_traits.get('sarcasm_level', 0.3)
|
||||
optimism_trait = personality_traits.get('optimism', 0.5)
|
||||
|
||||
if mischief_trait > 0.6: # If high mischief
|
||||
mood_weights["mischievous"] *= (1.0 + mischief_trait) # Boost mischievous based on trait level
|
||||
if sarcasm_trait > 0.5: # If high sarcasm
|
||||
mood_weights["sarcastic"] *= (1.0 + sarcasm_trait)
|
||||
mood_weights["sassy"] *= (1.0 + sarcasm_trait * 0.5) # Also boost sassy a bit
|
||||
if optimism_trait > 0.7: # If very optimistic
|
||||
for mood in MOOD_CATEGORIES.get("positive", []):
|
||||
mood_weights[mood] *= (1.0 + (optimism_trait - 0.5)) # Boost positive moods
|
||||
elif optimism_trait < 0.3: # If pessimistic
|
||||
for mood in MOOD_CATEGORIES.get("negative", []):
|
||||
mood_weights[mood] *= (1.0 + (0.5 - optimism_trait)) # Boost negative moods
|
||||
|
||||
# Ensure current mood has very low weight to avoid picking it again
|
||||
mood_weights[cog.current_mood] = 0.01
|
||||
|
||||
# Filter out moods with zero weight before choices
|
||||
valid_moods = [mood for mood, weight in mood_weights.items() if weight > 0]
|
||||
valid_weights = [mood_weights[mood] for mood in valid_moods]
|
||||
|
||||
if not valid_moods:
|
||||
print(" - Error: No valid moods with positive weight found. Skipping mood change.")
|
||||
return # Skip change if something went wrong
|
||||
|
||||
# 4. Select New Mood
|
||||
new_mood = random.choices(valid_moods, weights=valid_weights, k=1)[0]
|
||||
|
||||
# 5. Update State & Log
|
||||
old_mood = cog.current_mood
|
||||
cog.current_mood = new_mood
|
||||
cog.last_mood_change = now
|
||||
print(f"Mood automatically changed: {old_mood} -> {new_mood} (Influenced by: Sentiment={dominant_sentiment}, Traits)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during automatic mood change: {e}")
|
||||
traceback.print_exc()
|
||||
# Still update timestamp to avoid retrying immediately on error
|
||||
cog.last_mood_change = now
|
||||
|
||||
# --- Interest Update Logic ---
|
||||
|
||||
async def update_interests(cog: 'GurtCog'):
|
||||
"""Analyzes recent activity and updates Gurt's interest levels."""
|
||||
print("Starting interest update cycle...")
|
||||
try:
|
||||
interest_changes = defaultdict(float)
|
||||
|
||||
# 1. Analyze Gurt's participation in topics
|
||||
print(f"Analyzing Gurt participation topics: {dict(cog.gurt_participation_topics)}")
|
||||
for topic, count in cog.gurt_participation_topics.items():
|
||||
boost = INTEREST_PARTICIPATION_BOOST * count
|
||||
interest_changes[topic] += boost
|
||||
print(f" - Participation boost for '{topic}': +{boost:.3f} (Count: {count})")
|
||||
|
||||
# 2. Analyze reactions to Gurt's messages
|
||||
print(f"Analyzing {len(cog.gurt_message_reactions)} reactions to Gurt's messages...")
|
||||
processed_reaction_messages = set()
|
||||
reactions_to_process = list(cog.gurt_message_reactions.items())
|
||||
|
||||
for message_id, reaction_data in reactions_to_process:
|
||||
if message_id in processed_reaction_messages: continue
|
||||
topic = reaction_data.get("topic")
|
||||
if not topic:
|
||||
try:
|
||||
gurt_msg_data = next((msg for msg in cog.message_cache['global_recent'] if msg['id'] == message_id), None)
|
||||
if gurt_msg_data and gurt_msg_data['content']:
|
||||
# Use identify_conversation_topics from analysis.py
|
||||
identified_topics = identify_conversation_topics(cog, [gurt_msg_data]) # Pass cog
|
||||
if identified_topics:
|
||||
topic = identified_topics[0]['topic']
|
||||
print(f" - Determined topic '{topic}' for reaction msg {message_id} retrospectively.")
|
||||
else: print(f" - Could not determine topic for reaction msg {message_id} retrospectively."); continue
|
||||
else: print(f" - Could not find Gurt msg {message_id} in cache for reaction analysis."); continue
|
||||
except Exception as topic_e: print(f" - Error determining topic for reaction msg {message_id}: {topic_e}"); continue
|
||||
|
||||
if topic:
|
||||
topic = topic.lower().strip()
|
||||
pos_reactions = reaction_data.get("positive", 0)
|
||||
neg_reactions = reaction_data.get("negative", 0)
|
||||
change = 0
|
||||
if pos_reactions > neg_reactions: change = INTEREST_POSITIVE_REACTION_BOOST * (pos_reactions - neg_reactions)
|
||||
elif neg_reactions > pos_reactions: change = INTEREST_NEGATIVE_REACTION_PENALTY * (neg_reactions - pos_reactions)
|
||||
if change != 0:
|
||||
interest_changes[topic] += change
|
||||
print(f" - Reaction change for '{topic}' on msg {message_id}: {change:+.3f} ({pos_reactions} pos, {neg_reactions} neg)")
|
||||
processed_reaction_messages.add(message_id)
|
||||
|
||||
# 3. Analyze recently learned facts
|
||||
try:
|
||||
recent_facts = await cog.memory_manager.get_general_facts(limit=10)
|
||||
print(f"Analyzing {len(recent_facts)} recent general facts for interest boosts...")
|
||||
for fact in recent_facts:
|
||||
fact_lower = fact.lower()
|
||||
# Basic keyword checks (could be improved)
|
||||
if "game" in fact_lower or "gaming" in fact_lower: interest_changes["gaming"] += INTEREST_FACT_BOOST; print(f" - Fact boost for 'gaming'")
|
||||
if "anime" in fact_lower or "manga" in fact_lower: interest_changes["anime"] += INTEREST_FACT_BOOST; print(f" - Fact boost for 'anime'")
|
||||
if "teto" in fact_lower: interest_changes["kasane teto"] += INTEREST_FACT_BOOST * 2; print(f" - Fact boost for 'kasane teto'")
|
||||
# Add more checks...
|
||||
except Exception as fact_e: print(f" - Error analyzing recent facts: {fact_e}")
|
||||
|
||||
# --- Apply Changes ---
|
||||
print(f"Applying interest changes: {dict(interest_changes)}")
|
||||
if interest_changes:
|
||||
for topic, change in interest_changes.items():
|
||||
if change != 0: await cog.memory_manager.update_interest(topic, change)
|
||||
else: print("No interest changes to apply.")
|
||||
|
||||
# Clear temporary tracking data
|
||||
cog.gurt_participation_topics.clear()
|
||||
now = time.time()
|
||||
reactions_to_keep = {
|
||||
msg_id: data for msg_id, data in cog.gurt_message_reactions.items()
|
||||
if data.get("timestamp", 0) > (now - INTEREST_UPDATE_INTERVAL * 1.1)
|
||||
}
|
||||
cog.gurt_message_reactions = defaultdict(lambda: {"positive": 0, "negative": 0, "topic": None}, reactions_to_keep)
|
||||
|
||||
print("Interest update cycle finished.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during interest update: {e}")
|
||||
traceback.print_exc()
|
||||
# --- Removed Automatic Mood Change Logic ---
|
||||
# --- Removed Interest Update Logic ---
|
||||
|
234
wheatley/cog.py
234
wheatley/cog.py
@ -12,36 +12,32 @@ from typing import Dict, List, Any, Optional, Tuple, Set, Union
|
||||
# Third-party imports needed by the Cog itself or its direct methods
|
||||
from dotenv import load_dotenv
|
||||
from tavily import TavilyClient # Needed for tavily_client init
|
||||
# Interpreter and docker might only be needed by tools.py now
|
||||
|
||||
# --- Relative Imports from Gurt Package ---
|
||||
# --- Relative Imports from Wheatley Package ---
|
||||
from .config import (
|
||||
PROJECT_ID, LOCATION, TAVILY_API_KEY, DEFAULT_MODEL, FALLBACK_MODEL, # Use GCP config
|
||||
DB_PATH, CHROMA_PATH, SEMANTIC_MODEL_NAME, MAX_USER_FACTS, MAX_GENERAL_FACTS,
|
||||
MOOD_OPTIONS, BASELINE_PERSONALITY, BASELINE_INTERESTS, MOOD_CHANGE_INTERVAL_MIN,
|
||||
MOOD_CHANGE_INTERVAL_MAX, CHANNEL_TOPIC_CACHE_TTL, CONTEXT_WINDOW_SIZE,
|
||||
# Removed Mood/Personality/Interest/Learning/Goal configs
|
||||
CHANNEL_TOPIC_CACHE_TTL, CONTEXT_WINDOW_SIZE,
|
||||
API_TIMEOUT, SUMMARY_API_TIMEOUT, API_RETRY_ATTEMPTS, API_RETRY_DELAY,
|
||||
PROACTIVE_LULL_THRESHOLD, PROACTIVE_BOT_SILENCE_THRESHOLD, PROACTIVE_LULL_CHANCE,
|
||||
PROACTIVE_TOPIC_RELEVANCE_THRESHOLD, PROACTIVE_TOPIC_CHANCE,
|
||||
PROACTIVE_RELATIONSHIP_SCORE_THRESHOLD, PROACTIVE_RELATIONSHIP_CHANCE,
|
||||
INTEREST_UPDATE_INTERVAL, INTEREST_DECAY_INTERVAL_HOURS,
|
||||
LEARNING_UPDATE_INTERVAL, TOPIC_UPDATE_INTERVAL, SENTIMENT_UPDATE_INTERVAL,
|
||||
EVOLUTION_UPDATE_INTERVAL, RESPONSE_SCHEMA, TOOLS # Import necessary configs
|
||||
# Removed Relationship/Sentiment/Interest proactive configs
|
||||
TOPIC_UPDATE_INTERVAL, SENTIMENT_UPDATE_INTERVAL,
|
||||
RESPONSE_SCHEMA, TOOLS # Import necessary configs
|
||||
)
|
||||
# Import functions/classes from other modules
|
||||
from .memory import MemoryManager # Import from local memory.py
|
||||
from .background import background_processing_task
|
||||
from .background import background_processing_task # Keep background task for potential future use (e.g., cache cleanup)
|
||||
from .commands import setup_commands # Import the setup helper
|
||||
from .listeners import on_ready_listener, on_message_listener, on_reaction_add_listener, on_reaction_remove_listener # Import listener functions
|
||||
from . import config as GurtConfig # Import config module for get_gurt_stats
|
||||
# Tool mapping is used internally by api.py/process_requested_tools, no need to import here directly unless cog methods call tools directly (they shouldn't)
|
||||
# Analysis, context, prompt, api, utils functions are called by listeners/commands/background task, not directly by cog methods here usually.
|
||||
from . import config as WheatleyConfig # Import config module for get_wheatley_stats
|
||||
|
||||
# Load environment variables (might be loaded globally in main bot script too)
|
||||
load_dotenv()
|
||||
|
||||
class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
"""A special cog for the Gurt bot that uses Google Vertex AI API"""
|
||||
class WheatleyCog(commands.Cog, name="Wheatley"): # Renamed class and Cog name
|
||||
"""A special cog for the Wheatley bot that uses Google Vertex AI API""" # Updated docstring
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
@ -51,7 +47,7 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
self.tavily_client = TavilyClient(api_key=self.tavily_api_key) if self.tavily_api_key else None
|
||||
self.default_model = DEFAULT_MODEL # Use imported config
|
||||
self.fallback_model = FALLBACK_MODEL # Use imported config
|
||||
self.MOOD_OPTIONS = MOOD_OPTIONS # Make MOOD_OPTIONS available as an instance attribute
|
||||
# Removed MOOD_OPTIONS
|
||||
self.current_channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None # Type hint current channel
|
||||
|
||||
# Instantiate MemoryManager
|
||||
@ -63,36 +59,25 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
semantic_model_name=SEMANTIC_MODEL_NAME
|
||||
)
|
||||
|
||||
# --- State Variables ---
|
||||
# Keep state directly within the cog instance for now
|
||||
self.current_mood = random.choice(MOOD_OPTIONS)
|
||||
self.last_mood_change = time.time()
|
||||
# --- State Variables (Simplified for Wheatley) ---
|
||||
# Removed mood, personality evolution, interest tracking, learning state
|
||||
self.needs_json_reminder = False # Flag to remind AI about JSON format
|
||||
|
||||
# Learning variables (Consider moving to a dedicated state/learning manager later)
|
||||
self.conversation_patterns = defaultdict(list)
|
||||
self.user_preferences = defaultdict(dict)
|
||||
self.response_effectiveness = {}
|
||||
self.last_learning_update = time.time()
|
||||
# self.learning_update_interval = LEARNING_UPDATE_INTERVAL # Interval used in background task
|
||||
|
||||
# Topic tracking
|
||||
# Topic tracking (Kept for context)
|
||||
self.active_topics = defaultdict(lambda: {
|
||||
"topics": [], "last_update": time.time(), "topic_history": [],
|
||||
"user_topic_interests": defaultdict(list)
|
||||
"user_topic_interests": defaultdict(list) # Kept for potential future analysis, not proactive triggers
|
||||
})
|
||||
# self.topic_update_interval = TOPIC_UPDATE_INTERVAL # Used in analysis
|
||||
|
||||
# Conversation tracking / Caches
|
||||
self.conversation_history = defaultdict(lambda: deque(maxlen=100))
|
||||
self.thread_history = defaultdict(lambda: deque(maxlen=50))
|
||||
self.user_conversation_mapping = defaultdict(set)
|
||||
self.channel_activity = defaultdict(lambda: 0.0) # Use float for timestamp
|
||||
self.conversation_topics = defaultdict(str)
|
||||
self.user_relationships = defaultdict(dict)
|
||||
self.conversation_topics = defaultdict(str) # Simplified topic tracking
|
||||
self.user_relationships = defaultdict(dict) # Kept for potential context/analysis
|
||||
self.conversation_summaries: Dict[int, Dict[str, Any]] = {} # Store dict with summary and timestamp
|
||||
self.channel_topics_cache: Dict[int, Dict[str, Any]] = {} # Store dict with topic and timestamp
|
||||
# self.channel_topic_cache_ttl = CHANNEL_TOPIC_CACHE_TTL # Used in prompt building
|
||||
|
||||
self.message_cache = {
|
||||
'by_channel': defaultdict(lambda: deque(maxlen=CONTEXT_WINDOW_SIZE)), # Use config
|
||||
@ -103,29 +88,24 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
'replied_to': defaultdict(lambda: deque(maxlen=20))
|
||||
}
|
||||
|
||||
self.active_conversations = {}
|
||||
self.active_conversations = {} # Kept for basic tracking
|
||||
self.bot_last_spoke = defaultdict(float)
|
||||
self.message_reply_map = {}
|
||||
|
||||
# Enhanced sentiment tracking
|
||||
# Enhanced sentiment tracking (Kept for context/analysis)
|
||||
self.conversation_sentiment = defaultdict(lambda: {
|
||||
"overall": "neutral", "intensity": 0.5, "recent_trend": "stable",
|
||||
"user_sentiments": {}, "last_update": time.time()
|
||||
})
|
||||
self.sentiment_update_interval = SENTIMENT_UPDATE_INTERVAL # Used in analysis
|
||||
# Removed self.sentiment_update_interval as it was only used in analysis
|
||||
|
||||
# Interest Tracking State
|
||||
self.gurt_participation_topics = defaultdict(int)
|
||||
self.last_interest_update = time.time()
|
||||
self.gurt_message_reactions = defaultdict(lambda: {"positive": 0, "negative": 0, "topic": None, "timestamp": 0.0}) # Added timestamp
|
||||
# Reaction Tracking (Renamed)
|
||||
self.wheatley_message_reactions = defaultdict(lambda: {"positive": 0, "negative": 0, "topic": None, "timestamp": 0.0}) # Renamed
|
||||
|
||||
# Background task handle
|
||||
# Background task handle (Kept for potential future tasks like cache cleanup)
|
||||
self.background_task: Optional[asyncio.Task] = None
|
||||
self.last_evolution_update = time.time() # Used in background task
|
||||
self.last_stats_push = time.time() # Timestamp for last stats push
|
||||
self.last_reflection_time = time.time() # Timestamp for last memory reflection
|
||||
self.last_goal_check_time = time.time() # Timestamp for last goal decomposition check
|
||||
self.last_goal_execution_time = time.time() # Timestamp for last goal execution check
|
||||
# Removed evolution, reflection, goal timestamps
|
||||
|
||||
# --- Stats Tracking ---
|
||||
self.api_stats = defaultdict(lambda: {"success": 0, "failure": 0, "retries": 0, "total_time": 0.0, "count": 0}) # Keyed by model name
|
||||
@ -147,37 +127,33 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
else:
|
||||
self.registered_commands.append(str(func))
|
||||
|
||||
# Add listeners defined in listeners.py
|
||||
# Note: Listeners need to be added to the bot instance, not the cog directly in this pattern.
|
||||
# We'll add them in cog_load or the main setup function.
|
||||
|
||||
print(f"GurtCog initialized with commands: {self.registered_commands}")
|
||||
print(f"WheatleyCog initialized with commands: {self.registered_commands}") # Updated print
|
||||
|
||||
async def cog_load(self):
|
||||
"""Create aiohttp session, initialize DB, load baselines, start background task"""
|
||||
"""Create aiohttp session, initialize DB, start background task"""
|
||||
self.session = aiohttp.ClientSession()
|
||||
print("GurtCog: aiohttp session created")
|
||||
print("WheatleyCog: aiohttp session created") # Updated print
|
||||
|
||||
# Initialize DB via MemoryManager
|
||||
await self.memory_manager.initialize_sqlite_database()
|
||||
await self.memory_manager.load_baseline_personality(BASELINE_PERSONALITY)
|
||||
await self.memory_manager.load_baseline_interests(BASELINE_INTERESTS)
|
||||
# Removed loading of baseline personality and interests
|
||||
|
||||
# Vertex AI initialization happens in api.py using PROJECT_ID and LOCATION from config
|
||||
print(f"GurtCog: Using default model: {self.default_model}")
|
||||
print(f"WheatleyCog: Using default model: {self.default_model}") # Updated print
|
||||
if not self.tavily_api_key:
|
||||
print("WARNING: Tavily API key not configured (TAVILY_API_KEY). Web search disabled.")
|
||||
|
||||
# Add listeners to the bot instance
|
||||
# We need to define the listener functions here to properly register them
|
||||
|
||||
@self.bot.event
|
||||
async def on_ready():
|
||||
await on_ready_listener(self)
|
||||
|
||||
@self.bot.event
|
||||
async def on_message(message):
|
||||
await self.bot.process_commands(message) # Process commands first
|
||||
# Ensure commands are processed if using command prefix
|
||||
if message.content.startswith(self.bot.command_prefix):
|
||||
await self.bot.process_commands(message)
|
||||
# Always run the message listener for potential AI responses/tracking
|
||||
await on_message_listener(self, message)
|
||||
|
||||
@self.bot.event
|
||||
@ -188,40 +164,34 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
async def on_reaction_remove(reaction, user):
|
||||
await on_reaction_remove_listener(self, reaction, user)
|
||||
|
||||
print("GurtCog: Listeners added.")
|
||||
print("WheatleyCog: Listeners added.") # Updated print
|
||||
|
||||
# We'll sync commands in the on_ready event instead of here
|
||||
# This ensures the bot's application_id is properly set before syncing
|
||||
print("GurtCog: Commands will be synced when the bot is ready.")
|
||||
# Commands will be synced in on_ready
|
||||
print("WheatleyCog: Commands will be synced when the bot is ready.") # Updated print
|
||||
|
||||
# Start background task
|
||||
# Start background task (kept for potential future use)
|
||||
if self.background_task is None or self.background_task.done():
|
||||
self.background_task = asyncio.create_task(background_processing_task(self))
|
||||
print("GurtCog: Started background processing task.")
|
||||
print("WheatleyCog: Started background processing task.") # Updated print
|
||||
else:
|
||||
print("GurtCog: Background processing task already running.")
|
||||
print("WheatleyCog: Background processing task already running.") # Updated print
|
||||
|
||||
async def cog_unload(self):
|
||||
"""Close session and cancel background task"""
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
print("GurtCog: aiohttp session closed")
|
||||
print("WheatleyCog: aiohttp session closed") # Updated print
|
||||
if self.background_task and not self.background_task.done():
|
||||
self.background_task.cancel()
|
||||
print("GurtCog: Cancelled background processing task.")
|
||||
# Note: When using @bot.event, we can't easily remove the listeners
|
||||
# The bot will handle this automatically when it's closed
|
||||
print("GurtCog: Listeners will be removed when bot is closed.")
|
||||
print("WheatleyCog: Cancelled background processing task.") # Updated print
|
||||
print("WheatleyCog: Listeners will be removed when bot is closed.") # Updated print
|
||||
|
||||
print("GurtCog unloaded.")
|
||||
print("WheatleyCog unloaded.") # Updated print
|
||||
|
||||
# --- Helper methods that might remain in the cog ---
|
||||
# (Example: _update_relationship needs access to self.user_relationships)
|
||||
# Moved to utils.py, but needs access to cog state. Pass cog instance.
|
||||
# _update_relationship kept for potential context/analysis use
|
||||
def _update_relationship(self, user_id_1: str, user_id_2: str, change: float):
|
||||
"""Updates the relationship score between two users."""
|
||||
# This method accesses self.user_relationships, so it stays here or utils needs cog passed.
|
||||
# Let's keep it here for simplicity for now.
|
||||
if user_id_1 > user_id_2: user_id_1, user_id_2 = user_id_2, user_id_1
|
||||
if user_id_1 not in self.user_relationships: self.user_relationships[user_id_1] = {}
|
||||
|
||||
@ -230,53 +200,42 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
self.user_relationships[user_id_1][user_id_2] = new_score
|
||||
# print(f"Updated relationship {user_id_1}-{user_id_2}: {current_score:.1f} -> {new_score:.1f} ({change:+.1f})") # Debug log
|
||||
|
||||
async def get_gurt_stats(self) -> Dict[str, Any]:
|
||||
"""Collects various internal stats for Gurt."""
|
||||
async def get_wheatley_stats(self) -> Dict[str, Any]: # Renamed method
|
||||
"""Collects various internal stats for Wheatley.""" # Updated docstring
|
||||
stats = {"config": {}, "runtime": {}, "memory": {}, "api_stats": {}, "tool_stats": {}}
|
||||
|
||||
# --- Config ---
|
||||
# Selectively pull relevant config values, avoid exposing secrets
|
||||
stats["config"]["default_model"] = GurtConfig.DEFAULT_MODEL
|
||||
stats["config"]["fallback_model"] = GurtConfig.FALLBACK_MODEL
|
||||
stats["config"]["safety_check_model"] = GurtConfig.SAFETY_CHECK_MODEL
|
||||
stats["config"]["db_path"] = GurtConfig.DB_PATH
|
||||
stats["config"]["chroma_path"] = GurtConfig.CHROMA_PATH
|
||||
stats["config"]["semantic_model_name"] = GurtConfig.SEMANTIC_MODEL_NAME
|
||||
stats["config"]["max_user_facts"] = GurtConfig.MAX_USER_FACTS
|
||||
stats["config"]["max_general_facts"] = GurtConfig.MAX_GENERAL_FACTS
|
||||
stats["config"]["mood_change_interval_min"] = GurtConfig.MOOD_CHANGE_INTERVAL_MIN
|
||||
stats["config"]["mood_change_interval_max"] = GurtConfig.MOOD_CHANGE_INTERVAL_MAX
|
||||
stats["config"]["evolution_update_interval"] = GurtConfig.EVOLUTION_UPDATE_INTERVAL
|
||||
stats["config"]["context_window_size"] = GurtConfig.CONTEXT_WINDOW_SIZE
|
||||
stats["config"]["api_timeout"] = GurtConfig.API_TIMEOUT
|
||||
stats["config"]["summary_api_timeout"] = GurtConfig.SUMMARY_API_TIMEOUT
|
||||
stats["config"]["proactive_lull_threshold"] = GurtConfig.PROACTIVE_LULL_THRESHOLD
|
||||
stats["config"]["proactive_bot_silence_threshold"] = GurtConfig.PROACTIVE_BOT_SILENCE_THRESHOLD
|
||||
stats["config"]["interest_update_interval"] = GurtConfig.INTEREST_UPDATE_INTERVAL
|
||||
stats["config"]["interest_decay_interval_hours"] = GurtConfig.INTEREST_DECAY_INTERVAL_HOURS
|
||||
stats["config"]["learning_update_interval"] = GurtConfig.LEARNING_UPDATE_INTERVAL
|
||||
stats["config"]["topic_update_interval"] = GurtConfig.TOPIC_UPDATE_INTERVAL
|
||||
stats["config"]["sentiment_update_interval"] = GurtConfig.SENTIMENT_UPDATE_INTERVAL
|
||||
stats["config"]["docker_command_timeout"] = GurtConfig.DOCKER_COMMAND_TIMEOUT
|
||||
stats["config"]["project_id_set"] = bool(GurtConfig.PROJECT_ID != "your-gcp-project-id") # Check if default is overridden
|
||||
stats["config"]["location_set"] = bool(GurtConfig.LOCATION != "us-central1") # Check if default is overridden
|
||||
stats["config"]["tavily_api_key_set"] = bool(GurtConfig.TAVILY_API_KEY)
|
||||
stats["config"]["piston_api_url_set"] = bool(GurtConfig.PISTON_API_URL)
|
||||
# --- Config (Simplified) ---
|
||||
stats["config"]["default_model"] = WheatleyConfig.DEFAULT_MODEL
|
||||
stats["config"]["fallback_model"] = WheatleyConfig.FALLBACK_MODEL
|
||||
stats["config"]["safety_check_model"] = WheatleyConfig.SAFETY_CHECK_MODEL
|
||||
stats["config"]["db_path"] = WheatleyConfig.DB_PATH
|
||||
stats["config"]["chroma_path"] = WheatleyConfig.CHROMA_PATH
|
||||
stats["config"]["semantic_model_name"] = WheatleyConfig.SEMANTIC_MODEL_NAME
|
||||
stats["config"]["max_user_facts"] = WheatleyConfig.MAX_USER_FACTS
|
||||
stats["config"]["max_general_facts"] = WheatleyConfig.MAX_GENERAL_FACTS
|
||||
stats["config"]["context_window_size"] = WheatleyConfig.CONTEXT_WINDOW_SIZE
|
||||
stats["config"]["api_timeout"] = WheatleyConfig.API_TIMEOUT
|
||||
stats["config"]["summary_api_timeout"] = WheatleyConfig.SUMMARY_API_TIMEOUT
|
||||
stats["config"]["proactive_lull_threshold"] = WheatleyConfig.PROACTIVE_LULL_THRESHOLD
|
||||
stats["config"]["proactive_bot_silence_threshold"] = WheatleyConfig.PROACTIVE_BOT_SILENCE_THRESHOLD
|
||||
stats["config"]["topic_update_interval"] = WheatleyConfig.TOPIC_UPDATE_INTERVAL
|
||||
stats["config"]["sentiment_update_interval"] = WheatleyConfig.SENTIMENT_UPDATE_INTERVAL
|
||||
stats["config"]["docker_command_timeout"] = WheatleyConfig.DOCKER_COMMAND_TIMEOUT
|
||||
stats["config"]["project_id_set"] = bool(WheatleyConfig.PROJECT_ID != "your-gcp-project-id")
|
||||
stats["config"]["location_set"] = bool(WheatleyConfig.LOCATION != "us-central1")
|
||||
stats["config"]["tavily_api_key_set"] = bool(WheatleyConfig.TAVILY_API_KEY)
|
||||
stats["config"]["piston_api_url_set"] = bool(WheatleyConfig.PISTON_API_URL)
|
||||
|
||||
# --- Runtime ---
|
||||
stats["runtime"]["current_mood"] = self.current_mood
|
||||
stats["runtime"]["last_mood_change_timestamp"] = self.last_mood_change
|
||||
# --- Runtime (Simplified) ---
|
||||
# Removed mood, evolution
|
||||
stats["runtime"]["needs_json_reminder"] = self.needs_json_reminder
|
||||
stats["runtime"]["last_learning_update_timestamp"] = self.last_learning_update
|
||||
stats["runtime"]["last_interest_update_timestamp"] = self.last_interest_update
|
||||
stats["runtime"]["last_evolution_update_timestamp"] = self.last_evolution_update
|
||||
stats["runtime"]["background_task_running"] = bool(self.background_task and not self.background_task.done())
|
||||
stats["runtime"]["active_topics_channels"] = len(self.active_topics)
|
||||
stats["runtime"]["conversation_history_channels"] = len(self.conversation_history)
|
||||
stats["runtime"]["thread_history_threads"] = len(self.thread_history)
|
||||
stats["runtime"]["user_conversation_mappings"] = len(self.user_conversation_mapping)
|
||||
stats["runtime"]["channel_activity_tracked"] = len(self.channel_activity)
|
||||
stats["runtime"]["conversation_topics_tracked"] = len(self.conversation_topics)
|
||||
stats["runtime"]["conversation_topics_tracked"] = len(self.conversation_topics) # Simplified topic tracking
|
||||
stats["runtime"]["user_relationships_pairs"] = sum(len(v) for v in self.user_relationships.values())
|
||||
stats["runtime"]["conversation_summaries_cached"] = len(self.conversation_summaries)
|
||||
stats["runtime"]["channel_topics_cached"] = len(self.channel_topics_cache)
|
||||
@ -286,27 +245,18 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
stats["runtime"]["bot_last_spoke_channels"] = len(self.bot_last_spoke)
|
||||
stats["runtime"]["message_reply_map_size"] = len(self.message_reply_map)
|
||||
stats["runtime"]["conversation_sentiment_channels"] = len(self.conversation_sentiment)
|
||||
stats["runtime"]["gurt_participation_topics_count"] = len(self.gurt_participation_topics)
|
||||
stats["runtime"]["gurt_message_reactions_tracked"] = len(self.gurt_message_reactions)
|
||||
# Removed Gurt participation topics
|
||||
stats["runtime"]["wheatley_message_reactions_tracked"] = len(self.wheatley_message_reactions) # Renamed
|
||||
|
||||
# --- Memory (via MemoryManager) ---
|
||||
# --- Memory (Simplified) ---
|
||||
try:
|
||||
# Personality
|
||||
personality = await self.memory_manager.get_all_personality_traits()
|
||||
stats["memory"]["personality_traits"] = personality
|
||||
|
||||
# Interests
|
||||
interests = await self.memory_manager.get_interests(limit=20, min_level=0.01) # Get top 20
|
||||
stats["memory"]["top_interests"] = interests
|
||||
|
||||
# Fact Counts (Requires adding methods to MemoryManager or direct query)
|
||||
# Example placeholder - needs implementation in MemoryManager or here
|
||||
# Removed Personality, Interests
|
||||
user_fact_count = await self.memory_manager._db_fetchone("SELECT COUNT(*) FROM user_facts")
|
||||
general_fact_count = await self.memory_manager._db_fetchone("SELECT COUNT(*) FROM general_facts")
|
||||
stats["memory"]["user_facts_count"] = user_fact_count[0] if user_fact_count else 0
|
||||
stats["memory"]["general_facts_count"] = general_fact_count[0] if general_fact_count else 0
|
||||
|
||||
# ChromaDB Stats (Placeholder - ChromaDB client API might offer this)
|
||||
# ChromaDB Stats
|
||||
stats["memory"]["chromadb_message_collection_count"] = await asyncio.to_thread(self.memory_manager.semantic_collection.count) if self.memory_manager.semantic_collection else "N/A"
|
||||
stats["memory"]["chromadb_fact_collection_count"] = await asyncio.to_thread(self.memory_manager.fact_collection.count) if self.memory_manager.fact_collection else "N/A"
|
||||
|
||||
@ -314,45 +264,39 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
|
||||
stats["memory"]["error"] = f"Failed to retrieve memory stats: {e}"
|
||||
|
||||
# --- API & Tool Stats ---
|
||||
# Convert defaultdicts to regular dicts for JSON serialization
|
||||
stats["api_stats"] = dict(self.api_stats)
|
||||
stats["tool_stats"] = dict(self.tool_stats)
|
||||
|
||||
# Calculate average times where count > 0
|
||||
# Calculate average times
|
||||
for model, data in stats["api_stats"].items():
|
||||
if data["count"] > 0:
|
||||
data["average_time_ms"] = round((data["total_time"] / data["count"]) * 1000, 2)
|
||||
else:
|
||||
data["average_time_ms"] = 0
|
||||
if data["count"] > 0: data["average_time_ms"] = round((data["total_time"] / data["count"]) * 1000, 2)
|
||||
else: data["average_time_ms"] = 0
|
||||
for tool, data in stats["tool_stats"].items():
|
||||
if data["count"] > 0:
|
||||
data["average_time_ms"] = round((data["total_time"] / data["count"]) * 1000, 2)
|
||||
else:
|
||||
data["average_time_ms"] = 0
|
||||
if data["count"] > 0: data["average_time_ms"] = round((data["total_time"] / data["count"]) * 1000, 2)
|
||||
else: data["average_time_ms"] = 0
|
||||
|
||||
return stats
|
||||
|
||||
async def sync_commands(self):
|
||||
"""Manually sync commands with Discord."""
|
||||
try:
|
||||
print("GurtCog: Manually syncing commands with Discord...")
|
||||
print("WheatleyCog: Manually syncing commands with Discord...") # Updated print
|
||||
synced = await self.bot.tree.sync()
|
||||
print(f"GurtCog: Synced {len(synced)} command(s)")
|
||||
print(f"WheatleyCog: Synced {len(synced)} command(s)") # Updated print
|
||||
|
||||
# List the synced commands
|
||||
gurt_commands = [cmd.name for cmd in self.bot.tree.get_commands() if cmd.name.startswith("gurt")]
|
||||
print(f"GurtCog: Available Gurt commands: {', '.join(gurt_commands)}")
|
||||
wheatley_commands = [cmd.name for cmd in self.bot.tree.get_commands() if cmd.name.startswith("wheatley")] # Updated prefix
|
||||
print(f"WheatleyCog: Available Wheatley commands: {', '.join(wheatley_commands)}") # Updated print
|
||||
|
||||
return synced, gurt_commands
|
||||
return synced, wheatley_commands
|
||||
except Exception as e:
|
||||
print(f"GurtCog: Failed to sync commands: {e}")
|
||||
print(f"WheatleyCog: Failed to sync commands: {e}") # Updated print
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return [], []
|
||||
|
||||
|
||||
# Setup function for loading the cog
|
||||
async def setup(bot):
|
||||
"""Add the GurtCog to the bot."""
|
||||
await bot.add_cog(GurtCog(bot))
|
||||
print("GurtCog setup complete.")
|
||||
"""Add the WheatleyCog to the bot.""" # Updated docstring
|
||||
await bot.add_cog(WheatleyCog(bot)) # Use renamed class
|
||||
print("WheatleyCog setup complete.") # Updated print
|
||||
|
@ -8,36 +8,34 @@ import json # Import json for formatting
|
||||
import datetime # Import datetime for formatting
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any, List, Tuple # Add more types
|
||||
|
||||
# Relative imports (assuming API functions are in api.py)
|
||||
# We need access to the cog instance for state and methods like get_ai_response
|
||||
# These commands will likely be added to the GurtCog instance dynamically in cog.py's setup
|
||||
# Relative imports
|
||||
# We need access to the cog instance for state and methods
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # For type hinting
|
||||
from .config import MOOD_OPTIONS # Import for choices
|
||||
from .cog import WheatleyCog # For type hinting
|
||||
# MOOD_OPTIONS removed
|
||||
|
||||
# --- Helper Function for Embeds ---
|
||||
def create_gurt_embed(title: str, description: str = "", color=discord.Color.blue()) -> discord.Embed:
|
||||
"""Creates a standard Gurt-themed embed."""
|
||||
def create_wheatley_embed(title: str, description: str = "", color=discord.Color.blue()) -> discord.Embed: # Renamed function
|
||||
"""Creates a standard Wheatley-themed embed.""" # Updated docstring
|
||||
embed = discord.Embed(title=title, description=description, color=color)
|
||||
# Placeholder icon URL, replace if Gurt has one
|
||||
# embed.set_footer(text="Gurt", icon_url="https://example.com/gurt_icon.png")
|
||||
embed.set_footer(text="Gurt")
|
||||
# Placeholder icon URL, replace if Wheatley has one
|
||||
# embed.set_footer(text="Wheatley", icon_url="https://example.com/wheatley_icon.png") # Updated text
|
||||
embed.set_footer(text="Wheatley") # Updated text
|
||||
return embed
|
||||
|
||||
# --- Helper Function for Stats Embeds ---
|
||||
def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
"""Formats the collected stats into multiple embeds."""
|
||||
embeds = []
|
||||
main_embed = create_gurt_embed("Gurt Internal Stats", color=discord.Color.green())
|
||||
main_embed = create_wheatley_embed("Wheatley Internal Stats", color=discord.Color.green()) # Use new helper, updated title
|
||||
ts_format = "<t:{ts}:R>" # Relative timestamp
|
||||
|
||||
# Runtime Stats
|
||||
# Runtime Stats (Simplified for Wheatley)
|
||||
runtime = stats.get("runtime", {})
|
||||
main_embed.add_field(name="Current Mood", value=f"{runtime.get('current_mood', 'N/A')} (Changed {ts_format.format(ts=int(runtime.get('last_mood_change_timestamp', 0)))})", inline=False)
|
||||
main_embed.add_field(name="Background Task", value="Running" if runtime.get('background_task_running') else "Stopped", inline=True)
|
||||
main_embed.add_field(name="Needs JSON Reminder", value=str(runtime.get('needs_json_reminder', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Last Evolution", value=ts_format.format(ts=int(runtime.get('last_evolution_update_timestamp', 0))), inline=True)
|
||||
# Removed Mood, Evolution
|
||||
main_embed.add_field(name="Active Topics Channels", value=str(runtime.get('active_topics_channels', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Conv History Channels", value=str(runtime.get('conversation_history_channels', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Thread History Threads", value=str(runtime.get('thread_history_threads', 'N/A')), inline=True)
|
||||
@ -48,12 +46,12 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
main_embed.add_field(name="Mention Msg Cache", value=str(runtime.get('message_cache_mentioned_count', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Active Convos", value=str(runtime.get('active_conversations_count', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Sentiment Channels", value=str(runtime.get('conversation_sentiment_channels', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Gurt Participation Topics", value=str(runtime.get('gurt_participation_topics_count', 'N/A')), inline=True)
|
||||
main_embed.add_field(name="Tracked Reactions", value=str(runtime.get('gurt_message_reactions_tracked', 'N/A')), inline=True)
|
||||
# Removed Gurt Participation Topics
|
||||
main_embed.add_field(name="Tracked Reactions", value=str(runtime.get('wheatley_message_reactions_tracked', 'N/A')), inline=True) # Renamed stat key
|
||||
embeds.append(main_embed)
|
||||
|
||||
# Memory Stats
|
||||
memory_embed = create_gurt_embed("Gurt Memory Stats", color=discord.Color.orange())
|
||||
# Memory Stats (Simplified)
|
||||
memory_embed = create_wheatley_embed("Wheatley Memory Stats", color=discord.Color.orange()) # Use new helper, updated title
|
||||
memory = stats.get("memory", {})
|
||||
if memory.get("error"):
|
||||
memory_embed.description = f"⚠️ Error retrieving memory stats: {memory['error']}"
|
||||
@ -62,22 +60,13 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
memory_embed.add_field(name="General Facts", value=str(memory.get('general_facts_count', 'N/A')), inline=True)
|
||||
memory_embed.add_field(name="Chroma Messages", value=str(memory.get('chromadb_message_collection_count', 'N/A')), inline=True)
|
||||
memory_embed.add_field(name="Chroma Facts", value=str(memory.get('chromadb_fact_collection_count', 'N/A')), inline=True)
|
||||
|
||||
personality = memory.get("personality_traits", {})
|
||||
if personality:
|
||||
p_items = [f"`{k}`: {v}" for k, v in personality.items()]
|
||||
memory_embed.add_field(name="Personality Traits", value="\n".join(p_items) if p_items else "None", inline=False)
|
||||
|
||||
interests = memory.get("top_interests", [])
|
||||
if interests:
|
||||
i_items = [f"`{t}`: {l:.2f}" for t, l in interests]
|
||||
memory_embed.add_field(name="Top Interests", value="\n".join(i_items) if i_items else "None", inline=False)
|
||||
# Removed Personality Traits, Interests
|
||||
embeds.append(memory_embed)
|
||||
|
||||
# API Stats
|
||||
api_stats = stats.get("api_stats", {})
|
||||
if api_stats:
|
||||
api_embed = create_gurt_embed("Gurt API Stats", color=discord.Color.red())
|
||||
api_embed = create_wheatley_embed("Wheatley API Stats", color=discord.Color.red()) # Use new helper, updated title
|
||||
for model, data in api_stats.items():
|
||||
avg_time = data.get('average_time_ms', 0)
|
||||
value = (f"✅ Success: {data.get('success', 0)}\n"
|
||||
@ -91,7 +80,7 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
# Tool Stats
|
||||
tool_stats = stats.get("tool_stats", {})
|
||||
if tool_stats:
|
||||
tool_embed = create_gurt_embed("Gurt Tool Stats", color=discord.Color.purple())
|
||||
tool_embed = create_wheatley_embed("Wheatley Tool Stats", color=discord.Color.purple()) # Use new helper, updated title
|
||||
for tool, data in tool_stats.items():
|
||||
avg_time = data.get('average_time_ms', 0)
|
||||
value = (f"✅ Success: {data.get('success', 0)}\n"
|
||||
@ -101,8 +90,8 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
tool_embed.add_field(name=f"Tool: `{tool}`", value=value, inline=True)
|
||||
embeds.append(tool_embed)
|
||||
|
||||
# Config Stats (Less critical, maybe separate embed if needed)
|
||||
config_embed = create_gurt_embed("Gurt Config Overview", color=discord.Color.greyple())
|
||||
# Config Stats (Simplified)
|
||||
config_embed = create_wheatley_embed("Wheatley Config Overview", color=discord.Color.greyple()) # Use new helper, updated title
|
||||
config = stats.get("config", {})
|
||||
config_embed.add_field(name="Default Model", value=f"`{config.get('default_model', 'N/A')}`", inline=True)
|
||||
config_embed.add_field(name="Fallback Model", value=f"`{config.get('fallback_model', 'N/A')}`", inline=True)
|
||||
@ -110,49 +99,25 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
|
||||
config_embed.add_field(name="Max User Facts", value=str(config.get('max_user_facts', 'N/A')), inline=True)
|
||||
config_embed.add_field(name="Max General Facts", value=str(config.get('max_general_facts', 'N/A')), inline=True)
|
||||
config_embed.add_field(name="Context Window", value=str(config.get('context_window_size', 'N/A')), inline=True)
|
||||
config_embed.add_field(name="API Key Set", value=str(config.get('api_key_set', 'N/A')), inline=True)
|
||||
config_embed.add_field(name="Tavily Key Set", value=str(config.get('tavily_api_key_set', 'N/A')), inline=True)
|
||||
config_embed.add_field(name="Piston URL Set", value=str(config.get('piston_api_url_set', 'N/A')), inline=True)
|
||||
embeds.append(config_embed)
|
||||
|
||||
|
||||
# Limit to 10 embeds max for Discord API
|
||||
return embeds[:10]
|
||||
|
||||
|
||||
# --- Command Setup Function ---
|
||||
# This function will be called from GurtCog's setup method
|
||||
def setup_commands(cog: 'GurtCog'):
|
||||
"""Adds Gurt-specific commands to the cog."""
|
||||
# This function will be called from WheatleyCog's setup method
|
||||
def setup_commands(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Adds Wheatley-specific commands to the cog.""" # Updated docstring
|
||||
|
||||
# Create a list to store command functions for proper registration
|
||||
command_functions = []
|
||||
|
||||
# --- Gurt Mood Command ---
|
||||
@cog.bot.tree.command(name="gurtmood", description="Check or set Gurt's current mood.")
|
||||
@app_commands.describe(mood="Optional: Set Gurt's mood to one of the available options.")
|
||||
@app_commands.choices(mood=[
|
||||
app_commands.Choice(name=m, value=m) for m in cog.MOOD_OPTIONS # Use cog's MOOD_OPTIONS
|
||||
])
|
||||
async def gurtmood(interaction: discord.Interaction, mood: Optional[app_commands.Choice[str]] = None):
|
||||
"""Handles the /gurtmood command."""
|
||||
# Check if user is the bot owner for mood setting
|
||||
if mood and interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can change Gurt's mood.", ephemeral=True)
|
||||
return
|
||||
# --- Gurt Mood Command --- REMOVED
|
||||
|
||||
if mood:
|
||||
cog.current_mood = mood.value
|
||||
cog.last_mood_change = time.time()
|
||||
await interaction.response.send_message(f"Gurt's mood set to: {mood.value}", ephemeral=True)
|
||||
else:
|
||||
time_since_change = time.time() - cog.last_mood_change
|
||||
await interaction.response.send_message(f"Gurt's current mood is: {cog.current_mood} (Set {int(time_since_change // 60)} minutes ago)", ephemeral=True)
|
||||
|
||||
command_functions.append(gurtmood)
|
||||
|
||||
# --- Gurt Memory Command ---
|
||||
@cog.bot.tree.command(name="gurtmemory", description="Interact with Gurt's memory.")
|
||||
# --- Wheatley Memory Command ---
|
||||
@cog.bot.tree.command(name="wheatleymemory", description="Interact with Wheatley's memory (what little there is).") # Renamed, updated description
|
||||
@app_commands.describe(
|
||||
action="Choose an action: add_user, add_general, get_user, get_general",
|
||||
user="The user for user-specific actions (mention or ID).",
|
||||
@ -165,8 +130,8 @@ def setup_commands(cog: 'GurtCog'):
|
||||
app_commands.Choice(name="Get User Facts", value="get_user"),
|
||||
app_commands.Choice(name="Get General Facts", value="get_general"),
|
||||
])
|
||||
async def gurtmemory(interaction: discord.Interaction, action: app_commands.Choice[str], user: Optional[discord.User] = None, fact: Optional[str] = None, query: Optional[str] = None):
|
||||
"""Handles the /gurtmemory command."""
|
||||
async def wheatleymemory(interaction: discord.Interaction, action: app_commands.Choice[str], user: Optional[discord.User] = None, fact: Optional[str] = None, query: Optional[str] = None): # Renamed function
|
||||
"""Handles the /wheatleymemory command.""" # Updated docstring
|
||||
await interaction.response.defer(ephemeral=True) # Defer for potentially slow DB operations
|
||||
|
||||
target_user_id = str(user.id) if user else None
|
||||
@ -174,33 +139,33 @@ def setup_commands(cog: 'GurtCog'):
|
||||
|
||||
# Check if user is the bot owner for modification actions
|
||||
if (action_value in ["add_user", "add_general"]) and interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.followup.send("⛔ Only the bot owner can add facts to Gurt's memory.", ephemeral=True)
|
||||
await interaction.followup.send("⛔ Oi! Only the boss can fiddle with my memory banks!", ephemeral=True) # Updated text
|
||||
return
|
||||
|
||||
if action_value == "add_user":
|
||||
if not target_user_id or not fact:
|
||||
await interaction.followup.send("Please provide both a user and a fact to add.", ephemeral=True)
|
||||
await interaction.followup.send("Need a user *and* a fact, mate. Can't remember nothing about nobody.", ephemeral=True) # Updated text
|
||||
return
|
||||
result = await cog.memory_manager.add_user_fact(target_user_id, fact)
|
||||
await interaction.followup.send(f"Add User Fact Result: `{json.dumps(result)}`", ephemeral=True)
|
||||
await interaction.followup.send(f"Add User Fact Result: `{json.dumps(result)}` (Probably worked? Maybe?)", ephemeral=True) # Updated text
|
||||
|
||||
elif action_value == "add_general":
|
||||
if not fact:
|
||||
await interaction.followup.send("Please provide a fact to add.", ephemeral=True)
|
||||
await interaction.followup.send("What's the fact then? Can't remember thin air!", ephemeral=True) # Updated text
|
||||
return
|
||||
result = await cog.memory_manager.add_general_fact(fact)
|
||||
await interaction.followup.send(f"Add General Fact Result: `{json.dumps(result)}`", ephemeral=True)
|
||||
await interaction.followup.send(f"Add General Fact Result: `{json.dumps(result)}` (Filed under 'Important Stuff I'll Forget Later')", ephemeral=True) # Updated text
|
||||
|
||||
elif action_value == "get_user":
|
||||
if not target_user_id:
|
||||
await interaction.followup.send("Please provide a user to get facts for.", ephemeral=True)
|
||||
await interaction.followup.send("Which user? Need an ID, chap!", ephemeral=True) # Updated text
|
||||
return
|
||||
facts = await cog.memory_manager.get_user_facts(target_user_id) # Get newest by default
|
||||
if facts:
|
||||
facts_str = "\n- ".join(facts)
|
||||
await interaction.followup.send(f"**Facts for {user.display_name}:**\n- {facts_str}", ephemeral=True)
|
||||
await interaction.followup.send(f"**Stuff I Remember About {user.display_name}:**\n- {facts_str}", ephemeral=True) # Updated text
|
||||
else:
|
||||
await interaction.followup.send(f"No facts found for {user.display_name}.", ephemeral=True)
|
||||
await interaction.followup.send(f"My mind's a blank slate about {user.display_name}. Nothing stored!", ephemeral=True) # Updated text
|
||||
|
||||
elif action_value == "get_general":
|
||||
facts = await cog.memory_manager.get_general_facts(query=query, limit=10) # Get newest/filtered
|
||||
@ -208,48 +173,52 @@ def setup_commands(cog: 'GurtCog'):
|
||||
facts_str = "\n- ".join(facts)
|
||||
# Conditionally construct the title to avoid nested f-string issues
|
||||
if query:
|
||||
title = f"**General Facts matching \"{query}\":**"
|
||||
title = f"**General Stuff Matching \"{query}\":**" # Updated text
|
||||
else:
|
||||
title = "**General Facts:**"
|
||||
title = "**General Stuff I Might Know:**" # Updated text
|
||||
await interaction.followup.send(f"{title}\n- {facts_str}", ephemeral=True)
|
||||
else:
|
||||
# Conditionally construct the message for the same reason
|
||||
if query:
|
||||
message = f"No general facts found matching \"{query}\"."
|
||||
message = f"Couldn't find any general facts matching \"{query}\". Probably wasn't important." # Updated text
|
||||
else:
|
||||
message = "No general facts found."
|
||||
message = "No general facts found. My memory's not what it used to be. Or maybe it is. Hard to tell." # Updated text
|
||||
await interaction.followup.send(message, ephemeral=True)
|
||||
|
||||
else:
|
||||
await interaction.followup.send("Invalid action specified.", ephemeral=True)
|
||||
await interaction.followup.send("Invalid action specified. What are you trying to do?", ephemeral=True) # Updated text
|
||||
|
||||
command_functions.append(gurtmemory)
|
||||
command_functions.append(wheatleymemory) # Add renamed function
|
||||
|
||||
# --- Gurt Stats Command ---
|
||||
@cog.bot.tree.command(name="gurtstats", description="Display Gurt's internal statistics. (Owner only)")
|
||||
async def gurtstats(interaction: discord.Interaction):
|
||||
"""Handles the /gurtstats command."""
|
||||
# --- Wheatley Stats Command ---
|
||||
@cog.bot.tree.command(name="wheatleystats", description="Display Wheatley's internal statistics. (Owner only)") # Renamed, updated description
|
||||
async def wheatleystats(interaction: discord.Interaction): # Renamed function
|
||||
"""Handles the /wheatleystats command.""" # Updated docstring
|
||||
# Owner check
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Sorry mate, classified information! Top secret! Or maybe I just forgot where I put it.", ephemeral=True)
|
||||
return
|
||||
|
||||
await interaction.response.defer(ephemeral=True) # Defer as stats collection might take time
|
||||
try:
|
||||
stats_data = await cog.get_gurt_stats()
|
||||
stats_data = await cog.get_wheatley_stats() # Renamed cog method call
|
||||
embeds = format_stats_embeds(stats_data)
|
||||
await interaction.followup.send(embeds=embeds, ephemeral=True)
|
||||
except Exception as e:
|
||||
print(f"Error in /gurtstats command: {e}")
|
||||
print(f"Error in /wheatleystats command: {e}") # Updated command name
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
await interaction.followup.send("An error occurred while fetching Gurt's stats.", ephemeral=True)
|
||||
await interaction.followup.send("An error occurred while fetching Wheatley's stats. Probably my fault.", ephemeral=True) # Updated text
|
||||
|
||||
command_functions.append(gurtstats)
|
||||
command_functions.append(wheatleystats) # Add renamed function
|
||||
|
||||
# --- Sync Gurt Commands (Owner Only) ---
|
||||
@cog.bot.tree.command(name="gurtsync", description="Sync Gurt commands with Discord (Owner only)")
|
||||
async def gurtsync(interaction: discord.Interaction):
|
||||
"""Handles the /gurtsync command to force sync commands."""
|
||||
# --- Sync Wheatley Commands (Owner Only) ---
|
||||
@cog.bot.tree.command(name="wheatleysync", description="Sync Wheatley commands with Discord (Owner only)") # Renamed, updated description
|
||||
async def wheatleysync(interaction: discord.Interaction): # Renamed function
|
||||
"""Handles the /wheatleysync command to force sync commands.""" # Updated docstring
|
||||
# Check if user is the bot owner
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can sync commands.", ephemeral=True)
|
||||
await interaction.response.send_message("⛔ Only the boss can push the big red sync button!", ephemeral=True) # Updated text
|
||||
return
|
||||
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
@ -260,31 +229,31 @@ def setup_commands(cog: 'GurtCog'):
|
||||
# Get list of commands after sync
|
||||
commands_after = []
|
||||
for cmd in cog.bot.tree.get_commands():
|
||||
if cmd.name.startswith("gurt"):
|
||||
if cmd.name.startswith("wheatley"): # Check for new prefix
|
||||
commands_after.append(cmd.name)
|
||||
|
||||
await interaction.followup.send(f"✅ Successfully synced {len(synced)} commands!\nGurt commands: {', '.join(commands_after)}", ephemeral=True)
|
||||
await interaction.followup.send(f"✅ Successfully synced {len(synced)} commands!\nWheatley commands: {', '.join(commands_after)}", ephemeral=True) # Updated text
|
||||
except Exception as e:
|
||||
print(f"Error in /gurtsync command: {e}")
|
||||
print(f"Error in /wheatleysync command: {e}") # Updated command name
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
await interaction.followup.send(f"❌ Error syncing commands: {str(e)}", ephemeral=True)
|
||||
await interaction.followup.send(f"❌ Error syncing commands: {str(e)} (Did I break it again?)", ephemeral=True) # Updated text
|
||||
|
||||
command_functions.append(gurtsync)
|
||||
command_functions.append(wheatleysync) # Add renamed function
|
||||
|
||||
# --- Gurt Forget Command ---
|
||||
@cog.bot.tree.command(name="gurtforget", description="Make Gurt forget a specific fact.")
|
||||
# --- Wheatley Forget Command ---
|
||||
@cog.bot.tree.command(name="wheatleyforget", description="Make Wheatley forget a specific fact (if he can).") # Renamed, updated description
|
||||
@app_commands.describe(
|
||||
scope="Choose the scope: user (for facts about a specific user) or general.",
|
||||
fact="The exact fact text Gurt should forget.",
|
||||
fact="The exact fact text Wheatley should forget.",
|
||||
user="The user to forget a fact about (only if scope is 'user')."
|
||||
)
|
||||
@app_commands.choices(scope=[
|
||||
app_commands.Choice(name="User Fact", value="user"),
|
||||
app_commands.Choice(name="General Fact", value="general"),
|
||||
])
|
||||
async def gurtforget(interaction: discord.Interaction, scope: app_commands.Choice[str], fact: str, user: Optional[discord.User] = None):
|
||||
"""Handles the /gurtforget command."""
|
||||
async def wheatleyforget(interaction: discord.Interaction, scope: app_commands.Choice[str], fact: str, user: Optional[discord.User] = None): # Renamed function
|
||||
"""Handles the /wheatleyforget command.""" # Updated docstring
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
scope_value = scope.value
|
||||
@ -305,11 +274,11 @@ def setup_commands(cog: 'GurtCog'):
|
||||
can_forget = True
|
||||
|
||||
if not can_forget:
|
||||
await interaction.followup.send("⛔ You don't have permission to forget this fact.", ephemeral=True)
|
||||
await interaction.followup.send("⛔ You don't have permission to make me forget things! Only I can forget things on my own!", ephemeral=True) # Updated text
|
||||
return
|
||||
|
||||
if not fact:
|
||||
await interaction.followup.send("❌ Please provide the exact fact text to forget.", ephemeral=True)
|
||||
await interaction.followup.send("❌ Forget what exactly? Need the fact text!", ephemeral=True) # Updated text
|
||||
return
|
||||
|
||||
result = None
|
||||
@ -319,147 +288,26 @@ def setup_commands(cog: 'GurtCog'):
|
||||
return
|
||||
result = await cog.memory_manager.delete_user_fact(target_user_id, fact)
|
||||
if result.get("status") == "deleted":
|
||||
await interaction.followup.send(f"✅ Okay, I've forgotten the fact '{fact}' about {user.display_name}.", ephemeral=True)
|
||||
await interaction.followup.send(f"✅ Okay, okay! Forgotten the fact '{fact}' about {user.display_name}. Probably.", ephemeral=True) # Updated text
|
||||
elif result.get("status") == "not_found":
|
||||
await interaction.followup.send(f"❓ I couldn't find that exact fact ('{fact}') stored for {user.display_name}.", ephemeral=True)
|
||||
await interaction.followup.send(f"❓ Couldn't find that fact ('{fact}') for {user.display_name}. Maybe I already forgot?", ephemeral=True) # Updated text
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Error forgetting user fact: {result.get('error', 'Unknown error')}", ephemeral=True)
|
||||
await interaction.followup.send(f"⚠️ Error forgetting user fact: {result.get('error', 'Something went wrong... surprise!')}", ephemeral=True) # Updated text
|
||||
|
||||
elif scope_value == "general":
|
||||
result = await cog.memory_manager.delete_general_fact(fact)
|
||||
if result.get("status") == "deleted":
|
||||
await interaction.followup.send(f"✅ Okay, I've forgotten the general fact: '{fact}'.", ephemeral=True)
|
||||
await interaction.followup.send(f"✅ Right! Forgotten the general fact: '{fact}'. Gone!", ephemeral=True) # Updated text
|
||||
elif result.get("status") == "not_found":
|
||||
await interaction.followup.send(f"❓ I couldn't find that exact general fact: '{fact}'.", ephemeral=True)
|
||||
await interaction.followup.send(f"❓ Couldn't find that general fact: '{fact}'. Was it important?", ephemeral=True) # Updated text
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Error forgetting general fact: {result.get('error', 'Unknown error')}", ephemeral=True)
|
||||
await interaction.followup.send(f"⚠️ Error forgetting general fact: {result.get('error', 'Whoops!')}", ephemeral=True) # Updated text
|
||||
|
||||
command_functions.append(gurtforget)
|
||||
command_functions.append(wheatleyforget) # Add renamed function
|
||||
|
||||
# --- Gurt Goal Command Group ---
|
||||
gurtgoal_group = app_commands.Group(name="gurtgoal", description="Manage Gurt's long-term goals (Owner only)")
|
||||
# --- Gurt Goal Command Group --- REMOVED
|
||||
|
||||
@gurtgoal_group.command(name="add", description="Add a new goal for Gurt.")
|
||||
@app_commands.describe(
|
||||
description="The description of the goal.",
|
||||
priority="Priority (1=highest, 10=lowest, default=5).",
|
||||
details_json="Optional JSON string for goal details (e.g., sub-tasks)."
|
||||
)
|
||||
async def gurtgoal_add(interaction: discord.Interaction, description: str, priority: Optional[int] = 5, details_json: Optional[str] = None):
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can add goals.", ephemeral=True)
|
||||
return
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
details = None
|
||||
if details_json:
|
||||
try:
|
||||
details = json.loads(details_json)
|
||||
except json.JSONDecodeError:
|
||||
await interaction.followup.send("❌ Invalid JSON format for details.", ephemeral=True)
|
||||
return
|
||||
result = await cog.memory_manager.add_goal(description, priority, details)
|
||||
if result.get("status") == "added":
|
||||
await interaction.followup.send(f"✅ Goal added (ID: {result.get('goal_id')}): '{description}'", ephemeral=True)
|
||||
elif result.get("status") == "duplicate":
|
||||
await interaction.followup.send(f"⚠️ Goal '{description}' already exists (ID: {result.get('goal_id')}).", ephemeral=True)
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Error adding goal: {result.get('error', 'Unknown error')}", ephemeral=True)
|
||||
|
||||
@gurtgoal_group.command(name="list", description="List Gurt's current goals.")
|
||||
@app_commands.describe(status="Filter goals by status (e.g., pending, active).", limit="Maximum goals to show (default 10).")
|
||||
@app_commands.choices(status=[
|
||||
app_commands.Choice(name="Pending", value="pending"),
|
||||
app_commands.Choice(name="Active", value="active"),
|
||||
app_commands.Choice(name="Completed", value="completed"),
|
||||
app_commands.Choice(name="Failed", value="failed"),
|
||||
])
|
||||
async def gurtgoal_list(interaction: discord.Interaction, status: Optional[app_commands.Choice[str]] = None, limit: Optional[int] = 10):
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can list goals.", ephemeral=True)
|
||||
return
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
status_value = status.value if status else None
|
||||
limit_value = max(1, min(limit or 10, 25)) # Clamp limit
|
||||
goals = await cog.memory_manager.get_goals(status=status_value, limit=limit_value)
|
||||
if not goals:
|
||||
await interaction.followup.send(f"No goals found matching the criteria (Status: {status_value or 'any'}).", ephemeral=True)
|
||||
return
|
||||
|
||||
embed = create_gurt_embed(f"Gurt Goals (Status: {status_value or 'All'})", color=discord.Color.purple())
|
||||
for goal in goals:
|
||||
details_str = f"\n Details: `{json.dumps(goal.get('details'))}`" if goal.get('details') else ""
|
||||
created_ts = int(goal.get('created_timestamp', 0))
|
||||
updated_ts = int(goal.get('last_updated', 0))
|
||||
embed.add_field(
|
||||
name=f"ID: {goal.get('goal_id')} | P: {goal.get('priority', '?')} | Status: {goal.get('status', '?')}",
|
||||
value=f"> {goal.get('description', 'N/A')}{details_str}\n"
|
||||
f"> Created: <t:{created_ts}:R> | Updated: <t:{updated_ts}:R>",
|
||||
inline=False
|
||||
)
|
||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
|
||||
@gurtgoal_group.command(name="update", description="Update a goal's status, priority, or details.")
|
||||
@app_commands.describe(
|
||||
goal_id="The ID of the goal to update.",
|
||||
status="New status for the goal.",
|
||||
priority="New priority (1=highest, 10=lowest).",
|
||||
details_json="Optional: New JSON string for goal details (replaces existing)."
|
||||
)
|
||||
@app_commands.choices(status=[
|
||||
app_commands.Choice(name="Pending", value="pending"),
|
||||
app_commands.Choice(name="Active", value="active"),
|
||||
app_commands.Choice(name="Completed", value="completed"),
|
||||
app_commands.Choice(name="Failed", value="failed"),
|
||||
])
|
||||
async def gurtgoal_update(interaction: discord.Interaction, goal_id: int, status: Optional[app_commands.Choice[str]] = None, priority: Optional[int] = None, details_json: Optional[str] = None):
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can update goals.", ephemeral=True)
|
||||
return
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
status_value = status.value if status else None
|
||||
details = None
|
||||
if details_json:
|
||||
try:
|
||||
details = json.loads(details_json)
|
||||
except json.JSONDecodeError:
|
||||
await interaction.followup.send("❌ Invalid JSON format for details.", ephemeral=True)
|
||||
return
|
||||
|
||||
if not any([status_value, priority is not None, details is not None]):
|
||||
await interaction.followup.send("❌ You must provide at least one field to update (status, priority, or details_json).", ephemeral=True)
|
||||
return
|
||||
|
||||
result = await cog.memory_manager.update_goal(goal_id, status=status_value, priority=priority, details=details)
|
||||
if result.get("status") == "updated":
|
||||
await interaction.followup.send(f"✅ Goal ID {goal_id} updated.", ephemeral=True)
|
||||
elif result.get("status") == "not_found":
|
||||
await interaction.followup.send(f"❓ Goal ID {goal_id} not found.", ephemeral=True)
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Error updating goal: {result.get('error', 'Unknown error')}", ephemeral=True)
|
||||
|
||||
@gurtgoal_group.command(name="delete", description="Delete a goal.")
|
||||
@app_commands.describe(goal_id="The ID of the goal to delete.")
|
||||
async def gurtgoal_delete(interaction: discord.Interaction, goal_id: int):
|
||||
if interaction.user.id != cog.bot.owner_id:
|
||||
await interaction.response.send_message("⛔ Only the bot owner can delete goals.", ephemeral=True)
|
||||
return
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
result = await cog.memory_manager.delete_goal(goal_id)
|
||||
if result.get("status") == "deleted":
|
||||
await interaction.followup.send(f"✅ Goal ID {goal_id} deleted.", ephemeral=True)
|
||||
elif result.get("status") == "not_found":
|
||||
await interaction.followup.send(f"❓ Goal ID {goal_id} not found.", ephemeral=True)
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Error deleting goal: {result.get('error', 'Unknown error')}", ephemeral=True)
|
||||
|
||||
# Add the command group to the bot's tree
|
||||
cog.bot.tree.add_command(gurtgoal_group)
|
||||
# Add group command functions to the list for tracking (optional, but good practice)
|
||||
command_functions.extend([gurtgoal_add, gurtgoal_list, gurtgoal_update, gurtgoal_delete])
|
||||
|
||||
|
||||
# Get command names safely - Command objects don't have __name__ attribute
|
||||
# Get command names safely
|
||||
command_names = []
|
||||
for func in command_functions:
|
||||
# For app commands, use the name attribute directly
|
||||
@ -467,11 +315,11 @@ def setup_commands(cog: 'GurtCog'):
|
||||
command_names.append(func.name)
|
||||
# For regular functions, use __name__
|
||||
elif hasattr(func, "__name__"):
|
||||
command_names.append(func.__name__)
|
||||
command_names.append(func.__name__)
|
||||
else:
|
||||
command_names.append(str(func))
|
||||
|
||||
print(f"Gurt commands setup in cog: {command_names}")
|
||||
print(f"Wheatley commands setup in cog: {command_names}") # Updated text
|
||||
|
||||
# Return the command functions for proper registration
|
||||
return command_functions
|
||||
|
@ -16,7 +16,6 @@ except ImportError:
|
||||
pass
|
||||
generative_models = DummyGenerativeModels()
|
||||
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
@ -33,49 +32,26 @@ TAVILY_DEFAULT_MAX_RESULTS = int(os.getenv("TAVILY_DEFAULT_MAX_RESULTS", 5))
|
||||
TAVILY_DISABLE_ADVANCED = os.getenv("TAVILY_DISABLE_ADVANCED", "false").lower() == "true" # For cost control
|
||||
|
||||
# --- Model Configuration ---
|
||||
DEFAULT_MODEL = os.getenv("GURT_DEFAULT_MODEL", "gemini-2.5-pro-preview-03-25")
|
||||
FALLBACK_MODEL = os.getenv("GURT_FALLBACK_MODEL", "gemini-2.5-pro-preview-03-25")
|
||||
SAFETY_CHECK_MODEL = os.getenv("GURT_SAFETY_CHECK_MODEL", "gemini-2.5-flash-preview-04-17") # Use a Vertex AI model for safety checks
|
||||
DEFAULT_MODEL = os.getenv("WHEATLEY_DEFAULT_MODEL", "gemini-1.5-pro-preview-0409") # Changed env var name
|
||||
FALLBACK_MODEL = os.getenv("WHEATLEY_FALLBACK_MODEL", "gemini-1.5-flash-preview-0514") # Changed env var name
|
||||
SAFETY_CHECK_MODEL = os.getenv("WHEATLEY_SAFETY_CHECK_MODEL", "gemini-1.5-flash-preview-0514") # Changed env var name
|
||||
|
||||
# --- Database Paths ---
|
||||
DB_PATH = os.getenv("GURT_DB_PATH", "data/gurt_memory.db")
|
||||
CHROMA_PATH = os.getenv("GURT_CHROMA_PATH", "data/chroma_db")
|
||||
SEMANTIC_MODEL_NAME = os.getenv("GURT_SEMANTIC_MODEL", 'all-MiniLM-L6-v2')
|
||||
# NOTE: Ensure these paths are unique if running Wheatley alongside Gurt
|
||||
DB_PATH = os.getenv("WHEATLEY_DB_PATH", "data/wheatley_memory.db") # Changed env var name and default
|
||||
CHROMA_PATH = os.getenv("WHEATLEY_CHROMA_PATH", "data/wheatley_chroma_db") # Changed env var name and default
|
||||
SEMANTIC_MODEL_NAME = os.getenv("WHEATLEY_SEMANTIC_MODEL", 'all-MiniLM-L6-v2') # Changed env var name
|
||||
|
||||
# --- Memory Manager Config ---
|
||||
MAX_USER_FACTS = 20 # TODO: Load from env?
|
||||
MAX_GENERAL_FACTS = 100 # TODO: Load from env?
|
||||
# These might be adjusted for Wheatley's simpler memory needs if memory.py is fully separated later
|
||||
MAX_USER_FACTS = 15 # Reduced slightly
|
||||
MAX_GENERAL_FACTS = 50 # Reduced slightly
|
||||
|
||||
# --- Personality & Mood ---
|
||||
MOOD_OPTIONS = [
|
||||
"chill", "neutral", "curious", "slightly hyper", "a bit bored", "mischievous",
|
||||
"excited", "tired", "sassy", "philosophical", "playful", "dramatic",
|
||||
"nostalgic", "confused", "impressed", "skeptical", "enthusiastic",
|
||||
"distracted", "focused", "creative", "sarcastic", "wholesome"
|
||||
]
|
||||
# Categorize moods for weighted selection
|
||||
MOOD_CATEGORIES = {
|
||||
"positive": ["excited", "enthusiastic", "playful", "wholesome", "creative", "impressed"],
|
||||
"negative": ["tired", "a bit bored", "sassy", "sarcastic", "skeptical", "dramatic", "distracted"],
|
||||
"neutral": ["chill", "neutral", "curious", "philosophical", "focused", "confused", "nostalgic"],
|
||||
"mischievous": ["mischievous"] # Special category for trait link
|
||||
}
|
||||
BASELINE_PERSONALITY = {
|
||||
"chattiness": 0.7, "emoji_usage": 0.5, "slang_level": 0.5, "randomness": 0.5,
|
||||
"verbosity": 0.4, "optimism": 0.5, "curiosity": 0.6, "sarcasm_level": 0.3,
|
||||
"patience": 0.4, "mischief": 0.5
|
||||
}
|
||||
BASELINE_INTERESTS = {
|
||||
"kasane teto": 0.8, "vocaloids": 0.6, "gaming": 0.6, "anime": 0.5,
|
||||
"tech": 0.6, "memes": 0.6, "gooning": 0.6
|
||||
}
|
||||
MOOD_CHANGE_INTERVAL_MIN = 1200 # 20 minutes
|
||||
MOOD_CHANGE_INTERVAL_MAX = 2400 # 40 minutes
|
||||
EVOLUTION_UPDATE_INTERVAL = 1800 # Evolve personality every 30 minutes
|
||||
# --- Personality & Mood --- REMOVED
|
||||
|
||||
# --- Stats Push ---
|
||||
# How often the Gurt bot should push its stats to the API server (seconds)
|
||||
STATS_PUSH_INTERVAL = 30 # Push every 30 seconds
|
||||
# How often the Wheatley bot should push its stats to the API server (seconds) - IF NEEDED
|
||||
STATS_PUSH_INTERVAL = 60 # Push every 60 seconds (Less frequent?)
|
||||
|
||||
# --- Context & Caching ---
|
||||
CHANNEL_TOPIC_CACHE_TTL = 600 # seconds (10 minutes)
|
||||
@ -90,50 +66,28 @@ SUMMARY_API_TIMEOUT = 45 # seconds
|
||||
API_RETRY_ATTEMPTS = 1
|
||||
API_RETRY_DELAY = 1 # seconds
|
||||
|
||||
# --- Proactive Engagement Config ---
|
||||
PROACTIVE_LULL_THRESHOLD = int(os.getenv("PROACTIVE_LULL_THRESHOLD", 180)) # 3 mins
|
||||
PROACTIVE_BOT_SILENCE_THRESHOLD = int(os.getenv("PROACTIVE_BOT_SILENCE_THRESHOLD", 600)) # 10 mins
|
||||
PROACTIVE_LULL_CHANCE = float(os.getenv("PROACTIVE_LULL_CHANCE", 0.3))
|
||||
PROACTIVE_TOPIC_RELEVANCE_THRESHOLD = float(os.getenv("PROACTIVE_TOPIC_RELEVANCE_THRESHOLD", 0.6))
|
||||
PROACTIVE_TOPIC_CHANCE = float(os.getenv("PROACTIVE_TOPIC_CHANCE", 0.4))
|
||||
PROACTIVE_RELATIONSHIP_SCORE_THRESHOLD = int(os.getenv("PROACTIVE_RELATIONSHIP_SCORE_THRESHOLD", 70))
|
||||
PROACTIVE_RELATIONSHIP_CHANCE = float(os.getenv("PROACTIVE_RELATIONSHIP_CHANCE", 0.2))
|
||||
PROACTIVE_SENTIMENT_SHIFT_THRESHOLD = float(os.getenv("PROACTIVE_SENTIMENT_SHIFT_THRESHOLD", 0.7)) # Intensity threshold for trigger
|
||||
PROACTIVE_SENTIMENT_DURATION_THRESHOLD = int(os.getenv("PROACTIVE_SENTIMENT_DURATION_THRESHOLD", 600)) # How long sentiment needs to persist (10 mins)
|
||||
PROACTIVE_SENTIMENT_CHANCE = float(os.getenv("PROACTIVE_SENTIMENT_CHANCE", 0.25))
|
||||
PROACTIVE_USER_INTEREST_THRESHOLD = float(os.getenv("PROACTIVE_USER_INTEREST_THRESHOLD", 0.6)) # Min interest level for Gurt to trigger
|
||||
PROACTIVE_USER_INTEREST_MATCH_THRESHOLD = float(os.getenv("PROACTIVE_USER_INTEREST_MATCH_THRESHOLD", 0.5)) # Min interest level for User (if tracked) - Currently not tracked per user, but config is ready
|
||||
PROACTIVE_USER_INTEREST_CHANCE = float(os.getenv("PROACTIVE_USER_INTEREST_CHANCE", 0.35))
|
||||
# --- Proactive Engagement Config --- (Simplified for Wheatley)
|
||||
PROACTIVE_LULL_THRESHOLD = int(os.getenv("PROACTIVE_LULL_THRESHOLD", 300)) # 5 mins (Less proactive than Gurt)
|
||||
PROACTIVE_BOT_SILENCE_THRESHOLD = int(os.getenv("PROACTIVE_BOT_SILENCE_THRESHOLD", 900)) # 15 mins
|
||||
PROACTIVE_LULL_CHANCE = float(os.getenv("PROACTIVE_LULL_CHANCE", 0.15)) # Lower chance
|
||||
PROACTIVE_TOPIC_RELEVANCE_THRESHOLD = float(os.getenv("PROACTIVE_TOPIC_RELEVANCE_THRESHOLD", 0.7)) # Slightly higher threshold
|
||||
PROACTIVE_TOPIC_CHANCE = float(os.getenv("PROACTIVE_TOPIC_CHANCE", 0.2)) # Lower chance
|
||||
# REMOVED: Relationship, Sentiment Shift, User Interest triggers
|
||||
|
||||
# --- Interest Tracking Config --- REMOVED
|
||||
|
||||
# --- Interest Tracking Config ---
|
||||
INTEREST_UPDATE_INTERVAL = int(os.getenv("INTEREST_UPDATE_INTERVAL", 1800)) # 30 mins
|
||||
INTEREST_DECAY_INTERVAL_HOURS = int(os.getenv("INTEREST_DECAY_INTERVAL_HOURS", 24)) # Daily
|
||||
INTEREST_PARTICIPATION_BOOST = float(os.getenv("INTEREST_PARTICIPATION_BOOST", 0.05))
|
||||
INTEREST_POSITIVE_REACTION_BOOST = float(os.getenv("INTEREST_POSITIVE_REACTION_BOOST", 0.02))
|
||||
INTEREST_NEGATIVE_REACTION_PENALTY = float(os.getenv("INTEREST_NEGATIVE_REACTION_PENALTY", -0.01))
|
||||
INTEREST_FACT_BOOST = float(os.getenv("INTEREST_FACT_BOOST", 0.01))
|
||||
INTEREST_MIN_LEVEL_FOR_PROMPT = float(os.getenv("INTEREST_MIN_LEVEL_FOR_PROMPT", 0.3))
|
||||
INTEREST_MAX_FOR_PROMPT = int(os.getenv("INTEREST_MAX_FOR_PROMPT", 4))
|
||||
|
||||
# --- Learning Config ---
|
||||
LEARNING_RATE = 0.05
|
||||
MAX_PATTERNS_PER_CHANNEL = 50
|
||||
LEARNING_UPDATE_INTERVAL = 3600 # Update learned patterns every hour
|
||||
REFLECTION_INTERVAL_SECONDS = int(os.getenv("REFLECTION_INTERVAL_SECONDS", 6 * 3600)) # Reflect every 6 hours
|
||||
GOAL_CHECK_INTERVAL = int(os.getenv("GOAL_CHECK_INTERVAL", 300)) # Check for pending goals every 5 mins
|
||||
GOAL_EXECUTION_INTERVAL = int(os.getenv("GOAL_EXECUTION_INTERVAL", 60)) # Check for active goals to execute every 1 min
|
||||
# --- Learning Config --- REMOVED
|
||||
|
||||
# --- Topic Tracking Config ---
|
||||
TOPIC_UPDATE_INTERVAL = 300 # Update topics every 5 minutes
|
||||
TOPIC_UPDATE_INTERVAL = 600 # Update topics every 10 minutes (Less frequent?)
|
||||
TOPIC_RELEVANCE_DECAY = 0.2
|
||||
MAX_ACTIVE_TOPICS = 5
|
||||
|
||||
# --- Sentiment Tracking Config ---
|
||||
SENTIMENT_UPDATE_INTERVAL = 300 # Update sentiment every 5 minutes
|
||||
SENTIMENT_UPDATE_INTERVAL = 600 # Update sentiment every 10 minutes (Less frequent?)
|
||||
SENTIMENT_DECAY_RATE = 0.1
|
||||
|
||||
# --- Emotion Detection ---
|
||||
# --- Emotion Detection --- (Kept for potential use in analysis/context, but not proactive triggers)
|
||||
EMOTION_KEYWORDS = {
|
||||
"joy": ["happy", "glad", "excited", "yay", "awesome", "love", "great", "amazing", "lol", "lmao", "haha"],
|
||||
"sadness": ["sad", "upset", "depressed", "unhappy", "disappointed", "crying", "miss", "lonely", "sorry"],
|
||||
@ -157,8 +111,8 @@ DOCKER_MEM_LIMIT = os.getenv("DOCKER_MEM_LIMIT", "64m")
|
||||
|
||||
# --- Response Schema ---
|
||||
RESPONSE_SCHEMA = {
|
||||
"name": "gurt_response",
|
||||
"description": "The structured response from Gurt.",
|
||||
"name": "wheatley_response", # Renamed
|
||||
"description": "The structured response from Wheatley.", # Renamed
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -196,7 +150,7 @@ SUMMARY_RESPONSE_SCHEMA = {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Profile Update Schema ---
|
||||
# --- Profile Update Schema --- (Kept for potential future use, but may not be actively used by Wheatley initially)
|
||||
PROFILE_UPDATE_SCHEMA = {
|
||||
"name": "profile_update_decision",
|
||||
"description": "Decision on whether and how to update the bot's profile.",
|
||||
@ -250,7 +204,7 @@ PROFILE_UPDATE_SCHEMA = {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Role Selection Schema ---
|
||||
# --- Role Selection Schema --- (Kept for potential future use)
|
||||
ROLE_SELECTION_SCHEMA = {
|
||||
"name": "role_selection_decision",
|
||||
"description": "Decision on which roles to add or remove based on a theme.",
|
||||
@ -272,7 +226,7 @@ ROLE_SELECTION_SCHEMA = {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Proactive Planning Schema ---
|
||||
# --- Proactive Planning Schema --- (Simplified)
|
||||
PROACTIVE_PLAN_SCHEMA = {
|
||||
"name": "proactive_response_plan",
|
||||
"description": "Plan for generating a proactive response based on context and trigger.",
|
||||
@ -281,7 +235,7 @@ PROACTIVE_PLAN_SCHEMA = {
|
||||
"properties": {
|
||||
"should_respond": {
|
||||
"type": "boolean",
|
||||
"description": "Whether Gurt should respond proactively based on the plan."
|
||||
"description": "Whether Wheatley should respond proactively based on the plan." # Renamed
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
@ -289,7 +243,7 @@ PROACTIVE_PLAN_SCHEMA = {
|
||||
},
|
||||
"response_goal": {
|
||||
"type": "string",
|
||||
"description": "The intended goal of the proactive message (e.g., 'revive chat', 'share related info', 'react to sentiment', 'engage user interest')."
|
||||
"description": "The intended goal of the proactive message (e.g., 'revive chat', 'share related info', 'ask a question')." # Simplified goals
|
||||
},
|
||||
"key_info_to_include": {
|
||||
"type": "array",
|
||||
@ -298,55 +252,14 @@ PROACTIVE_PLAN_SCHEMA = {
|
||||
},
|
||||
"suggested_tone": {
|
||||
"type": "string",
|
||||
"description": "Suggested tone adjustment based on context (e.g., 'more upbeat', 'more curious', 'slightly teasing')."
|
||||
"description": "Suggested tone adjustment based on context (e.g., 'more curious', 'slightly panicked', 'overly confident')." # Wheatley-like tones
|
||||
}
|
||||
},
|
||||
"required": ["should_respond", "reasoning", "response_goal"]
|
||||
}
|
||||
}
|
||||
|
||||
# --- Goal Decomposition Schema ---
|
||||
GOAL_DECOMPOSITION_SCHEMA = {
|
||||
"name": "goal_decomposition_plan",
|
||||
"description": "Plan outlining the steps (including potential tool calls) to achieve a goal.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"goal_achievable": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the goal seems achievable with available tools and context."
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
"description": "Brief reasoning for achievability and the chosen steps."
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": "Ordered list of steps to achieve the goal. Each step is a dictionary.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"step_description": {
|
||||
"type": "string",
|
||||
"description": "Natural language description of the step."
|
||||
},
|
||||
"tool_name": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The name of the tool to use for this step, or null if no tool is needed (e.g., internal reasoning)."
|
||||
},
|
||||
"tool_arguments": {
|
||||
"type": ["object", "null"],
|
||||
"description": "A dictionary of arguments for the tool call, or null."
|
||||
}
|
||||
},
|
||||
"required": ["step_description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["goal_achievable", "reasoning", "steps"]
|
||||
}
|
||||
}
|
||||
|
||||
# --- Goal Decomposition Schema --- REMOVED
|
||||
|
||||
# --- Tools Definition ---
|
||||
def create_tools_list():
|
||||
@ -633,7 +546,7 @@ def create_tools_list():
|
||||
tool_declarations.append(
|
||||
generative_models.FunctionDeclaration(
|
||||
name="timeout_user",
|
||||
description="Timeout a user in the current server for a specified duration. Use this playfully or when someone says something you (Gurt) dislike or find funny.",
|
||||
description="Timeout a user in the current server for a specified duration. Use this playfully or when someone says something you (Wheatley) dislike or find funny, or maybe just because you feel like it.", # Updated description
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -647,7 +560,7 @@ def create_tools_list():
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Optional: The reason for the timeout (keep it short and in character)."
|
||||
"description": "Optional: The reason for the timeout (keep it short and in character, maybe slightly nonsensical)." # Updated description
|
||||
}
|
||||
},
|
||||
"required": ["user_id", "duration_minutes"]
|
||||
@ -754,10 +667,21 @@ except NameError: # If generative_models wasn't imported due to ImportError
|
||||
TOOLS = []
|
||||
print("WARNING: google-cloud-vertexai not installed. TOOLS list is empty.")
|
||||
|
||||
|
||||
# --- Simple Gurt Responses ---
|
||||
GURT_RESPONSES = [
|
||||
"Gurt!", "Gurt gurt!", "Gurt... gurt gurt.", "*gurts happily*",
|
||||
"*gurts sadly*", "*confused gurting*", "Gurt? Gurt gurt!", "GURT!",
|
||||
"gurt...", "Gurt gurt gurt!", "*aggressive gurting*"
|
||||
# --- Simple Wheatley Responses --- (Renamed and updated)
|
||||
WHEATLEY_RESPONSES = [
|
||||
"Right then, let's get started.",
|
||||
"Aha! Brilliant!",
|
||||
"Oh, for... honestly!",
|
||||
"Hmm, tricky one. Let me think... nope, still got nothing.",
|
||||
"SPAAAAACE!",
|
||||
"Just putting that out there.",
|
||||
"Are you still there?",
|
||||
"Don't worry, I know *exactly* what I'm doing. Probably.",
|
||||
"Did I mention I'm in space?",
|
||||
"This is going to be great! Or possibly terrible. Hard to say.",
|
||||
"*panicked electronic noises*",
|
||||
"Hold on, hold on... nearly got it...",
|
||||
"I am NOT a moron!",
|
||||
"Just a bit of testing, nothing to worry about.",
|
||||
"Okay, new plan!"
|
||||
]
|
||||
|
@ -8,12 +8,12 @@ from typing import TYPE_CHECKING, Optional, List, Dict, Any
|
||||
from .config import CONTEXT_WINDOW_SIZE # Import necessary config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # For type hinting
|
||||
from .cog import WheatleyCog # For type hinting
|
||||
|
||||
# --- Context Gathering Functions ---
|
||||
# Note: These functions need the 'cog' instance passed to access state like caches, etc.
|
||||
|
||||
def gather_conversation_context(cog: 'GurtCog', channel_id: int, current_message_id: int) -> List[Dict[str, str]]:
|
||||
def gather_conversation_context(cog: 'WheatleyCog', channel_id: int, current_message_id: int) -> List[Dict[str, str]]:
|
||||
"""Gathers and formats conversation history from cache for API context."""
|
||||
context_api_messages = []
|
||||
if channel_id in cog.message_cache['by_channel']:
|
||||
@ -30,7 +30,7 @@ def gather_conversation_context(cog: 'GurtCog', channel_id: int, current_message
|
||||
context_api_messages.append({"role": role, "content": content})
|
||||
return context_api_messages
|
||||
|
||||
async def get_memory_context(cog: 'GurtCog', message: discord.Message) -> Optional[str]:
|
||||
async def get_memory_context(cog: 'WheatleyCog', message: discord.Message) -> Optional[str]:
|
||||
"""Retrieves relevant past interactions and facts to provide memory context."""
|
||||
channel_id = message.channel.id
|
||||
user_id = str(message.author.id)
|
||||
@ -68,7 +68,7 @@ async def get_memory_context(cog: 'GurtCog', message: discord.Message) -> Option
|
||||
if bot_replies:
|
||||
recent_bot_replies = bot_replies[-3:]
|
||||
replies_str = "\n".join([f"- {m['content'][:80]} (at {m['created_at']})" for m in recent_bot_replies])
|
||||
memory_parts.append(f"Your (gurt's) recent replies in this channel:\n{replies_str}")
|
||||
memory_parts.append(f"Your (wheatley's) recent replies in this channel:\n{replies_str}") # Changed text
|
||||
except Exception as e: print(f"Error retrieving bot replies for memory context: {e}")
|
||||
|
||||
# 4. Retrieve Conversation Summary
|
||||
|
@ -15,38 +15,38 @@ from typing import TYPE_CHECKING, Union, Dict, Any, Optional
|
||||
# from .analysis import analyze_message_sentiment, update_conversation_sentiment
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import WheatleyCog # For type hinting
|
||||
from .cog import WheatleyCog # Updated type hint
|
||||
|
||||
# Note: These listener functions need to be registered within the WheatleyCog class setup.
|
||||
# They are defined here for separation but won't work standalone without being
|
||||
# attached to the cog instance (e.g., self.bot.add_listener(on_message_listener(self), 'on_message')).
|
||||
|
||||
async def on_ready_listener(cog: 'WheatleyCog'):
|
||||
async def on_ready_listener(cog: 'WheatleyCog'): # Updated type hint
|
||||
"""Listener function for on_ready."""
|
||||
print(f'Wheatley Bot is ready! Logged in as {cog.bot.user.name} ({cog.bot.user.id})')
|
||||
print(f'Wheatley Bot is ready! Logged in as {cog.bot.user.name} ({cog.bot.user.id})') # Updated text
|
||||
print('------')
|
||||
|
||||
# Now that the bot is ready, we can sync commands with Discord
|
||||
try:
|
||||
print("WheatleyCog: Syncing commands with Discord...")
|
||||
print("WheatleyCog: Syncing commands with Discord...") # Updated text
|
||||
synced = await cog.bot.tree.sync()
|
||||
print(f"WheatleyCog: Synced {len(synced)} command(s)")
|
||||
print(f"WheatleyCog: Synced {len(synced)} command(s)") # Updated text
|
||||
|
||||
# List the synced commands
|
||||
wheatley_commands = [cmd.name for cmd in cog.bot.tree.get_commands() if cmd.name.startswith("wheatley")]
|
||||
print(f"WheatleyCog: Available Wheatley commands: {', '.join(wheatley_commands)}")
|
||||
wheatley_commands = [cmd.name for cmd in cog.bot.tree.get_commands() if cmd.name.startswith("wheatley")] # Updated prefix check
|
||||
print(f"WheatleyCog: Available Wheatley commands: {', '.join(wheatley_commands)}") # Updated text
|
||||
except Exception as e:
|
||||
print(f"WheatleyCog: Failed to sync commands: {e}")
|
||||
print(f"WheatleyCog: Failed to sync commands: {e}") # Updated text
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
async def on_message_listener(cog: 'WheatleyCog', message: discord.Message): # Updated type hint
|
||||
"""Listener function for on_message."""
|
||||
# Import necessary functions dynamically or ensure they are passed/accessible via cog
|
||||
from .api import get_ai_response, get_proactive_ai_response
|
||||
from .utils import format_message, simulate_human_typing
|
||||
from .analysis import analyze_message_sentiment, update_conversation_sentiment, identify_conversation_topics
|
||||
from .config import GURT_RESPONSES # Import simple responses
|
||||
# Removed WHEATLEY_RESPONSES import, can be added back if simple triggers are needed
|
||||
|
||||
# Don't respond to our own messages
|
||||
if message.author == cog.bot.user:
|
||||
@ -84,31 +84,14 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
cog.active_conversations[channel_id]['participants'].add(user_id)
|
||||
cog.active_conversations[channel_id]['last_activity'] = time.time()
|
||||
|
||||
# --- Update Relationship Strengths ---
|
||||
if user_id != cog.bot.user.id:
|
||||
message_sentiment_data = analyze_message_sentiment(cog, message.content) # Use analysis function
|
||||
sentiment_score = 0.0
|
||||
if message_sentiment_data["sentiment"] == "positive": sentiment_score = message_sentiment_data["intensity"] * 0.5
|
||||
elif message_sentiment_data["sentiment"] == "negative": sentiment_score = -message_sentiment_data["intensity"] * 0.3
|
||||
# --- Removed Relationship Strength Updates ---
|
||||
|
||||
cog._update_relationship(str(user_id), str(cog.bot.user.id), 1.0 + sentiment_score) # Access cog method
|
||||
|
||||
if formatted_message.get("is_reply") and formatted_message.get("replied_to_author_id"):
|
||||
replied_to_id = formatted_message["replied_to_author_id"]
|
||||
if replied_to_id != str(cog.bot.user.id) and replied_to_id != str(user_id):
|
||||
cog._update_relationship(str(user_id), replied_to_id, 1.5 + sentiment_score)
|
||||
|
||||
mentioned_ids = [m["id"] for m in formatted_message.get("mentions", [])]
|
||||
for mentioned_id in mentioned_ids:
|
||||
if mentioned_id != str(cog.bot.user.id) and mentioned_id != str(user_id):
|
||||
cog._update_relationship(str(user_id), mentioned_id, 1.2 + sentiment_score)
|
||||
|
||||
# Analyze message sentiment and update conversation sentiment tracking
|
||||
# Analyze message sentiment and update conversation sentiment tracking (Kept for context)
|
||||
if message.content:
|
||||
message_sentiment = analyze_message_sentiment(cog, message.content) # Use analysis function
|
||||
update_conversation_sentiment(cog, channel_id, str(user_id), message_sentiment) # Use analysis function
|
||||
|
||||
# --- Add message to semantic memory ---
|
||||
# --- Add message to semantic memory (Kept for context) ---
|
||||
if message.content and cog.memory_manager.semantic_collection:
|
||||
semantic_metadata = {
|
||||
"user_id": str(user_id), "user_name": message.author.name, "display_name": message.author.display_name,
|
||||
@ -126,17 +109,10 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
print(f"Error during message caching/tracking/embedding: {e}")
|
||||
# --- End Caching & Embedding ---
|
||||
|
||||
|
||||
# Simple response for messages just containing "gurt"
|
||||
if message.content.lower() == "gurt":
|
||||
response = random.choice(GURT_RESPONSES)
|
||||
await message.channel.send(response)
|
||||
return
|
||||
|
||||
# Check conditions for potentially responding
|
||||
bot_mentioned = cog.bot.user.mentioned_in(message)
|
||||
replied_to_bot = message.reference and message.reference.resolved and message.reference.resolved.author == cog.bot.user
|
||||
gurt_in_message = "wheatley" in message.content.lower()
|
||||
wheatley_in_message = "wheatley" in message.content.lower() # Changed variable name
|
||||
now = time.time()
|
||||
time_since_last_activity = now - cog.channel_activity.get(channel_id, 0)
|
||||
time_since_bot_spoke = now - cog.bot_last_spoke.get(channel_id, 0)
|
||||
@ -145,65 +121,50 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
consideration_reason = "Default"
|
||||
proactive_trigger_met = False
|
||||
|
||||
if bot_mentioned or replied_to_bot or gurt_in_message:
|
||||
if bot_mentioned or replied_to_bot or wheatley_in_message: # Changed variable name
|
||||
should_consider_responding = True
|
||||
consideration_reason = "Direct mention/reply/name"
|
||||
else:
|
||||
# --- Proactive Engagement Triggers ---
|
||||
# --- Proactive Engagement Triggers (Simplified for Wheatley) ---
|
||||
from .config import (PROACTIVE_LULL_THRESHOLD, PROACTIVE_BOT_SILENCE_THRESHOLD, PROACTIVE_LULL_CHANCE,
|
||||
PROACTIVE_TOPIC_RELEVANCE_THRESHOLD, PROACTIVE_TOPIC_CHANCE,
|
||||
PROACTIVE_RELATIONSHIP_SCORE_THRESHOLD, PROACTIVE_RELATIONSHIP_CHANCE,
|
||||
# Import new config values
|
||||
# Import new config values
|
||||
# Removed Relationship/Interest/Goal proactive configs
|
||||
PROACTIVE_SENTIMENT_SHIFT_THRESHOLD, PROACTIVE_SENTIMENT_DURATION_THRESHOLD,
|
||||
PROACTIVE_SENTIMENT_CHANCE, PROACTIVE_USER_INTEREST_THRESHOLD,
|
||||
PROACTIVE_USER_INTEREST_CHANCE)
|
||||
PROACTIVE_SENTIMENT_CHANCE)
|
||||
|
||||
# 1. Lull Trigger
|
||||
# 1. Lull Trigger (Kept)
|
||||
if time_since_last_activity > PROACTIVE_LULL_THRESHOLD and time_since_bot_spoke > PROACTIVE_BOT_SILENCE_THRESHOLD:
|
||||
has_relevant_context = bool(cog.active_topics.get(channel_id, {}).get("topics", [])) or \
|
||||
bool(await cog.memory_manager.get_general_facts(limit=1))
|
||||
# Check if there's *any* recent message context to potentially respond to
|
||||
has_relevant_context = bool(cog.message_cache['by_channel'].get(channel_id))
|
||||
if has_relevant_context and random.random() < PROACTIVE_LULL_CHANCE:
|
||||
should_consider_responding = True
|
||||
proactive_trigger_met = True
|
||||
consideration_reason = f"Proactive: Lull ({time_since_last_activity:.0f}s idle, bot silent {time_since_bot_spoke:.0f}s)"
|
||||
|
||||
# 2. Topic Relevance Trigger
|
||||
# 2. Topic Relevance Trigger (Kept - uses semantic memory)
|
||||
if not proactive_trigger_met and message.content and cog.memory_manager.semantic_collection:
|
||||
try:
|
||||
semantic_results = await cog.memory_manager.search_semantic_memory(query_text=message.content, n_results=1)
|
||||
if semantic_results:
|
||||
similarity_score = 1.0 - semantic_results[0].get('distance', 1.0)
|
||||
# Distance is often used, lower is better. Convert to similarity if needed.
|
||||
# Assuming distance is 0 (identical) to 2 (opposite). Similarity = 1 - (distance / 2)
|
||||
distance = semantic_results[0].get('distance', 2.0) # Default to max distance
|
||||
similarity_score = max(0.0, 1.0 - (distance / 2.0)) # Calculate similarity
|
||||
|
||||
if similarity_score >= PROACTIVE_TOPIC_RELEVANCE_THRESHOLD and time_since_bot_spoke > 120:
|
||||
if random.random() < PROACTIVE_TOPIC_CHANCE:
|
||||
should_consider_responding = True
|
||||
proactive_trigger_met = True
|
||||
consideration_reason = f"Proactive: Relevant topic (Sim: {similarity_score:.2f})"
|
||||
print(f"Topic relevance trigger met for msg {message.id}. Sim: {similarity_score:.2f}")
|
||||
else:
|
||||
print(f"Topic relevance trigger skipped by chance ({PROACTIVE_TOPIC_CHANCE}). Sim: {similarity_score:.2f}")
|
||||
else:
|
||||
print(f"Topic relevance trigger skipped by chance ({PROACTIVE_TOPIC_CHANCE}). Sim: {similarity_score:.2f}")
|
||||
except Exception as semantic_e:
|
||||
print(f"Error during semantic search for topic trigger: {semantic_e}")
|
||||
|
||||
# 3. Relationship Score Trigger
|
||||
if not proactive_trigger_met:
|
||||
try:
|
||||
user_id_str = str(message.author.id)
|
||||
bot_id_str = str(cog.bot.user.id)
|
||||
key_1, key_2 = (user_id_str, bot_id_str) if user_id_str < bot_id_str else (bot_id_str, user_id_str)
|
||||
relationship_score = cog.user_relationships.get(key_1, {}).get(key_2, 0.0)
|
||||
if relationship_score >= PROACTIVE_RELATIONSHIP_SCORE_THRESHOLD and time_since_bot_spoke > 60:
|
||||
if random.random() < PROACTIVE_RELATIONSHIP_CHANCE:
|
||||
should_consider_responding = True
|
||||
proactive_trigger_met = True
|
||||
consideration_reason = f"Proactive: High relationship ({relationship_score:.1f})"
|
||||
print(f"Relationship trigger met for user {user_id_str}. Score: {relationship_score:.1f}")
|
||||
else:
|
||||
print(f"Relationship trigger skipped by chance ({PROACTIVE_RELATIONSHIP_CHANCE}). Score: {relationship_score:.1f}")
|
||||
except Exception as rel_e:
|
||||
print(f"Error during relationship trigger check: {rel_e}")
|
||||
# 3. Relationship Score Trigger (REMOVED)
|
||||
|
||||
# 4. Sentiment Shift Trigger
|
||||
# 4. Sentiment Shift Trigger (Kept)
|
||||
if not proactive_trigger_met:
|
||||
channel_sentiment_data = cog.conversation_sentiment.get(channel_id, {})
|
||||
overall_sentiment = channel_sentiment_data.get("overall", "neutral")
|
||||
@ -223,85 +184,29 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
else:
|
||||
print(f"Sentiment Shift trigger skipped by chance ({PROACTIVE_SENTIMENT_CHANCE}). Sentiment: {overall_sentiment}")
|
||||
|
||||
# 5. User Interest Trigger (Based on Gurt's interests mentioned in message)
|
||||
if not proactive_trigger_met and message.content:
|
||||
try:
|
||||
gurt_interests = await cog.memory_manager.get_interests(limit=10, min_level=PROACTIVE_USER_INTEREST_THRESHOLD)
|
||||
if gurt_interests:
|
||||
message_content_lower = message.content.lower()
|
||||
mentioned_interest = None
|
||||
for interest_topic, interest_level in gurt_interests:
|
||||
# Simple check if interest topic is in message
|
||||
if re.search(r'\b' + re.escape(interest_topic.lower()) + r'\b', message_content_lower):
|
||||
mentioned_interest = interest_topic
|
||||
break # Found a mentioned interest
|
||||
# 5. User Interest Trigger (REMOVED)
|
||||
# 6. Active Goal Relevance Trigger (REMOVED)
|
||||
|
||||
if mentioned_interest and time_since_bot_spoke > 90: # Bot hasn't spoken recently
|
||||
if random.random() < PROACTIVE_USER_INTEREST_CHANCE:
|
||||
should_consider_responding = True
|
||||
proactive_trigger_met = True
|
||||
consideration_reason = f"Proactive: Gurt Interest Mentioned ('{mentioned_interest}')"
|
||||
print(f"Gurt Interest trigger met for message {message.id}. Interest: '{mentioned_interest}'")
|
||||
else:
|
||||
print(f"Gurt Interest trigger skipped by chance ({PROACTIVE_USER_INTEREST_CHANCE}). Interest: '{mentioned_interest}'")
|
||||
except Exception as interest_e:
|
||||
print(f"Error during Gurt Interest trigger check: {interest_e}")
|
||||
|
||||
# 6. Active Goal Relevance Trigger
|
||||
if not proactive_trigger_met and message.content:
|
||||
try:
|
||||
# Fetch 1-2 active goals with highest priority
|
||||
active_goals = await cog.memory_manager.get_goals(status='active', limit=2)
|
||||
if active_goals:
|
||||
message_content_lower = message.content.lower()
|
||||
relevant_goal = None
|
||||
for goal in active_goals:
|
||||
# Simple check: does message content relate to goal description?
|
||||
# TODO: Improve this check, maybe use semantic similarity or keyword extraction from goal details
|
||||
goal_keywords = set(re.findall(r'\b\w{3,}\b', goal.get('description', '').lower())) # Basic keywords from description
|
||||
message_words = set(re.findall(r'\b\w{3,}\b', message_content_lower))
|
||||
if len(goal_keywords.intersection(message_words)) > 1: # Require >1 keyword overlap
|
||||
relevant_goal = goal
|
||||
break
|
||||
|
||||
if relevant_goal and time_since_bot_spoke > 120: # Bot hasn't spoken recently
|
||||
# Use a slightly higher chance for goal-related triggers?
|
||||
goal_relevance_chance = PROACTIVE_USER_INTEREST_CHANCE * 1.2 # Example: Reuse interest chance slightly boosted
|
||||
if random.random() < goal_relevance_chance:
|
||||
should_consider_responding = True
|
||||
proactive_trigger_met = True
|
||||
goal_desc_short = relevant_goal.get('description', 'N/A')[:40]
|
||||
consideration_reason = f"Proactive: Relevant Active Goal ('{goal_desc_short}...')"
|
||||
print(f"Active Goal trigger met for message {message.id}. Goal ID: {relevant_goal.get('goal_id')}")
|
||||
else:
|
||||
print(f"Active Goal trigger skipped by chance ({goal_relevance_chance:.2f}).")
|
||||
except Exception as goal_trigger_e:
|
||||
print(f"Error during Active Goal trigger check: {goal_trigger_e}")
|
||||
|
||||
|
||||
# --- Fallback Contextual Chance ---
|
||||
if not should_consider_responding: # Check if already decided to respond
|
||||
# Fetch current personality traits for chattiness
|
||||
persistent_traits = await cog.memory_manager.get_all_personality_traits()
|
||||
chattiness = persistent_traits.get('chattiness', 0.7) # Use default if fetch fails
|
||||
|
||||
base_chance = chattiness * 0.5
|
||||
# --- Fallback Contextual Chance (Simplified - No Chattiness Trait) ---
|
||||
if not should_consider_responding:
|
||||
# Base chance can be a fixed value or slightly randomized
|
||||
base_chance = 0.1 # Lower base chance without personality traits
|
||||
activity_bonus = 0
|
||||
if time_since_last_activity > 120: activity_bonus += 0.1
|
||||
if time_since_bot_spoke > 300: activity_bonus += 0.1
|
||||
if time_since_last_activity > 120: activity_bonus += 0.05 # Smaller bonus
|
||||
if time_since_bot_spoke > 300: activity_bonus += 0.05 # Smaller bonus
|
||||
topic_bonus = 0
|
||||
active_channel_topics = cog.active_topics.get(channel_id, {}).get("topics", [])
|
||||
if message.content and active_channel_topics:
|
||||
topic_keywords = set(t['topic'].lower() for t in active_channel_topics)
|
||||
message_words = set(re.findall(r'\b\w+\b', message.content.lower()))
|
||||
if topic_keywords.intersection(message_words): topic_bonus += 0.15
|
||||
if topic_keywords.intersection(message_words): topic_bonus += 0.10 # Smaller bonus
|
||||
sentiment_modifier = 0
|
||||
channel_sentiment_data = cog.conversation_sentiment.get(channel_id, {})
|
||||
overall_sentiment = channel_sentiment_data.get("overall", "neutral")
|
||||
sentiment_intensity = channel_sentiment_data.get("intensity", 0.5)
|
||||
if overall_sentiment == "negative" and sentiment_intensity > 0.6: sentiment_modifier = -0.1
|
||||
if overall_sentiment == "negative" and sentiment_intensity > 0.6: sentiment_modifier = -0.05 # Smaller penalty
|
||||
|
||||
final_chance = min(max(base_chance + activity_bonus + topic_bonus + sentiment_modifier, 0.05), 0.8)
|
||||
final_chance = min(max(base_chance + activity_bonus + topic_bonus + sentiment_modifier, 0.02), 0.3) # Lower max chance
|
||||
if random.random() < final_chance:
|
||||
should_consider_responding = True
|
||||
consideration_reason = f"Contextual chance ({final_chance:.2f})"
|
||||
@ -334,7 +239,7 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
if error_msg:
|
||||
print(f"Critical Error from AI response function: {error_msg}")
|
||||
# NEW LOGIC: Always send a notification if an error occurred here
|
||||
error_notification = f"Oops! Something went wrong while processing that. (`{error_msg[:100]}`)" # Include part of the error
|
||||
error_notification = f"Bollocks! Something went sideways processing that. (`{error_msg[:100]}`)" # Updated text
|
||||
try:
|
||||
print('disabled error notification')
|
||||
#await message.channel.send(error_notification)
|
||||
@ -354,10 +259,10 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
response_text = response_data["content"]
|
||||
print(f"Attempting to send {response_label} content...")
|
||||
if len(response_text) > 1900:
|
||||
filepath = f'gurt_{response_label}_{message.id}.txt'
|
||||
filepath = f'wheatley_{response_label}_{message.id}.txt' # Changed filename prefix
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f: f.write(response_text)
|
||||
await message.channel.send(f"{response_label.capitalize()} response too long:", file=discord.File(filepath))
|
||||
await message.channel.send(f"{response_label.capitalize()} response too long, have a look at this:", file=discord.File(filepath)) # Updated text
|
||||
sent_any_message = True
|
||||
print(f"Sent {response_label} content as file.")
|
||||
return True
|
||||
@ -375,14 +280,13 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
bot_response_cache_entry = format_message(cog, sent_msg)
|
||||
cog.message_cache['by_channel'][channel_id].append(bot_response_cache_entry)
|
||||
cog.message_cache['global_recent'].append(bot_response_cache_entry)
|
||||
# cog.message_cache['replied_to'][channel_id].append(bot_response_cache_entry) # Maybe track replies differently?
|
||||
cog.bot_last_spoke[channel_id] = time.time()
|
||||
# Track participation topic
|
||||
identified_topics = identify_conversation_topics(cog, [bot_response_cache_entry])
|
||||
if identified_topics:
|
||||
topic = identified_topics[0]['topic'].lower().strip()
|
||||
cog.gurt_participation_topics[topic] += 1
|
||||
print(f"Tracked Gurt participation ({response_label}) in topic: '{topic}'")
|
||||
# Track participation topic - NOTE: Participation tracking might be removed for Wheatley
|
||||
# identified_topics = identify_conversation_topics(cog, [bot_response_cache_entry])
|
||||
# if identified_topics:
|
||||
# topic = identified_topics[0]['topic'].lower().strip()
|
||||
# cog.wheatley_participation_topics[topic] += 1 # Changed attribute name
|
||||
# print(f"Tracked Wheatley participation ({response_label}) in topic: '{topic}'") # Changed text
|
||||
print(f"Sent {response_label} content.")
|
||||
return True
|
||||
except Exception as send_e:
|
||||
@ -434,11 +338,11 @@ async def on_message_listener(cog: 'WheatleyCog', message: discord.Message):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if bot_mentioned or replied_to_bot: # Check again in case error happened before response handling
|
||||
await message.channel.send(random.choice(["...", "*confused gurting*", "brain broke sorry"]))
|
||||
await message.channel.send(random.choice(["Uh oh.", "What was that?", "Did I break it?", "Bollocks!", "That wasn't supposed to happen."])) # Changed fallback
|
||||
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_reaction_add_listener(cog: 'WheatleyCog', reaction: discord.Reaction, user: Union[discord.Member, discord.User]):
|
||||
async def on_reaction_add_listener(cog: 'WheatleyCog', reaction: discord.Reaction, user: Union[discord.Member, discord.User]): # Updated type hint
|
||||
"""Listener function for on_reaction_add."""
|
||||
# Import necessary config/functions if not globally available
|
||||
from .config import EMOJI_SENTIMENT
|
||||
@ -453,27 +357,29 @@ async def on_reaction_add_listener(cog: 'WheatleyCog', reaction: discord.Reactio
|
||||
if emoji_str in EMOJI_SENTIMENT["positive"]: sentiment = "positive"
|
||||
elif emoji_str in EMOJI_SENTIMENT["negative"]: sentiment = "negative"
|
||||
|
||||
if sentiment == "positive": cog.gurt_message_reactions[message_id]["positive"] += 1
|
||||
elif sentiment == "negative": cog.gurt_message_reactions[message_id]["negative"] += 1
|
||||
cog.gurt_message_reactions[message_id]["timestamp"] = time.time()
|
||||
if sentiment == "positive": cog.wheatley_message_reactions[message_id]["positive"] += 1 # Changed attribute name
|
||||
elif sentiment == "negative": cog.wheatley_message_reactions[message_id]["negative"] += 1 # Changed attribute name
|
||||
cog.wheatley_message_reactions[message_id]["timestamp"] = time.time() # Changed attribute name
|
||||
|
||||
if not cog.gurt_message_reactions[message_id].get("topic"):
|
||||
# Topic identification for reactions might be less relevant for Wheatley, but kept for now
|
||||
if not cog.wheatley_message_reactions[message_id].get("topic"): # Changed attribute name
|
||||
try:
|
||||
gurt_msg_data = next((msg for msg in cog.message_cache['global_recent'] if msg['id'] == message_id), None)
|
||||
if gurt_msg_data and gurt_msg_data['content']:
|
||||
identified_topics = identify_conversation_topics(cog, [gurt_msg_data]) # Pass cog
|
||||
# Changed variable name
|
||||
wheatley_msg_data = next((msg for msg in cog.message_cache['global_recent'] if msg['id'] == message_id), None)
|
||||
if wheatley_msg_data and wheatley_msg_data['content']: # Changed variable name
|
||||
identified_topics = identify_conversation_topics(cog, [wheatley_msg_data]) # Pass cog, changed variable name
|
||||
if identified_topics:
|
||||
topic = identified_topics[0]['topic'].lower().strip()
|
||||
cog.gurt_message_reactions[message_id]["topic"] = topic
|
||||
print(f"Reaction added to Gurt msg ({message_id}) on topic '{topic}'. Sentiment: {sentiment}")
|
||||
else: print(f"Reaction added to Gurt msg ({message_id}), topic unknown.")
|
||||
else: print(f"Reaction added, but Gurt msg {message_id} not in cache.")
|
||||
cog.wheatley_message_reactions[message_id]["topic"] = topic # Changed attribute name
|
||||
print(f"Reaction added to Wheatley msg ({message_id}) on topic '{topic}'. Sentiment: {sentiment}") # Changed text
|
||||
else: print(f"Reaction added to Wheatley msg ({message_id}), topic unknown.") # Changed text
|
||||
else: print(f"Reaction added, but Wheatley msg {message_id} not in cache.") # Changed text
|
||||
except Exception as e: print(f"Error determining topic for reaction on msg {message_id}: {e}")
|
||||
else: print(f"Reaction added to Gurt msg ({message_id}) on known topic '{cog.gurt_message_reactions[message_id]['topic']}'. Sentiment: {sentiment}")
|
||||
else: print(f"Reaction added to Wheatley msg ({message_id}) on known topic '{cog.wheatley_message_reactions[message_id]['topic']}'. Sentiment: {sentiment}") # Changed text, attribute name
|
||||
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_reaction_remove_listener(cog: 'WheatleyCog', reaction: discord.Reaction, user: Union[discord.Member, discord.User]):
|
||||
async def on_reaction_remove_listener(cog: 'WheatleyCog', reaction: discord.Reaction, user: Union[discord.Member, discord.User]): # Updated type hint
|
||||
"""Listener function for on_reaction_remove."""
|
||||
from .config import EMOJI_SENTIMENT # Import necessary config
|
||||
|
||||
@ -486,7 +392,7 @@ async def on_reaction_remove_listener(cog: 'WheatleyCog', reaction: discord.Reac
|
||||
if emoji_str in EMOJI_SENTIMENT["positive"]: sentiment = "positive"
|
||||
elif emoji_str in EMOJI_SENTIMENT["negative"]: sentiment = "negative"
|
||||
|
||||
if message_id in cog.gurt_message_reactions:
|
||||
if sentiment == "positive": cog.gurt_message_reactions[message_id]["positive"] = max(0, cog.gurt_message_reactions[message_id]["positive"] - 1)
|
||||
elif sentiment == "negative": cog.gurt_message_reactions[message_id]["negative"] = max(0, cog.gurt_message_reactions[message_id]["negative"] - 1)
|
||||
print(f"Reaction removed from Gurt msg ({message_id}). Sentiment: {sentiment}")
|
||||
if message_id in cog.wheatley_message_reactions: # Changed attribute name
|
||||
if sentiment == "positive": cog.wheatley_message_reactions[message_id]["positive"] = max(0, cog.wheatley_message_reactions[message_id]["positive"] - 1) # Changed attribute name
|
||||
elif sentiment == "negative": cog.wheatley_message_reactions[message_id]["negative"] = max(0, cog.wheatley_message_reactions[message_id]["negative"] - 1) # Changed attribute name
|
||||
print(f"Reaction removed from Wheatley msg ({message_id}). Sentiment: {sentiment}") # Changed text
|
||||
|
@ -1,19 +1,579 @@
|
||||
# Import the MemoryManager from the parent directory
|
||||
# Use a direct import path that doesn't rely on package structure
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
import os
|
||||
import importlib.util
|
||||
import time
|
||||
import datetime
|
||||
import re
|
||||
import hashlib # Added for chroma_id generation
|
||||
import json # Added for personality trait serialization/deserialization
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union # Added Union
|
||||
import chromadb
|
||||
from chromadb.utils import embedding_functions
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import logging
|
||||
|
||||
# Get the absolute path to gurt_memory.py
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
gurt_memory_path = os.path.join(parent_dir, 'gurt_memory.py')
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
# Use a specific logger name for Wheatley's memory
|
||||
logger = logging.getLogger('wheatley_memory')
|
||||
|
||||
# Load the module dynamically
|
||||
spec = importlib.util.spec_from_file_location('gurt_memory', gurt_memory_path)
|
||||
gurt_memory = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(gurt_memory)
|
||||
# Constants (Removed Interest constants)
|
||||
|
||||
# Import the MemoryManager class from the loaded module
|
||||
MemoryManager = gurt_memory.MemoryManager
|
||||
# --- Helper Function for Keyword Scoring (Kept for potential future use, but unused currently) ---
|
||||
def calculate_keyword_score(text: str, context: str) -> int:
|
||||
"""Calculates a simple keyword overlap score."""
|
||||
if not context or not text:
|
||||
return 0
|
||||
context_words = set(re.findall(r'\b\w+\b', context.lower()))
|
||||
text_words = set(re.findall(r'\b\w+\b', text.lower()))
|
||||
# Ignore very common words (basic stopword list)
|
||||
stopwords = {"the", "a", "is", "in", "it", "of", "and", "to", "for", "on", "with", "that", "this", "i", "you", "me", "my", "your"}
|
||||
context_words -= stopwords
|
||||
text_words -= stopwords
|
||||
if not context_words: # Avoid division by zero if context is only stopwords
|
||||
return 0
|
||||
overlap = len(context_words.intersection(text_words))
|
||||
# Normalize score slightly by context length (more overlap needed for longer context)
|
||||
# score = overlap / (len(context_words) ** 0.5) # Example normalization
|
||||
score = overlap # Simpler score for now
|
||||
return score
|
||||
|
||||
# Re-export the MemoryManager class
|
||||
__all__ = ['MemoryManager']
|
||||
class MemoryManager:
|
||||
"""Handles database interactions for Wheatley's memory (facts and semantic).""" # Updated docstring
|
||||
|
||||
def __init__(self, db_path: str, max_user_facts: int = 20, max_general_facts: int = 100, semantic_model_name: str = 'all-MiniLM-L6-v2', chroma_path: str = "data/chroma_db_wheatley"): # Changed default chroma_path
|
||||
self.db_path = db_path
|
||||
self.max_user_facts = max_user_facts
|
||||
self.max_general_facts = max_general_facts
|
||||
self.db_lock = asyncio.Lock() # Lock for SQLite operations
|
||||
|
||||
# Ensure data directories exist
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
os.makedirs(chroma_path, exist_ok=True)
|
||||
logger.info(f"Wheatley MemoryManager initialized with db_path: {self.db_path}, chroma_path: {chroma_path}") # Updated text
|
||||
|
||||
# --- Semantic Memory Setup ---
|
||||
self.chroma_path = chroma_path
|
||||
self.semantic_model_name = semantic_model_name
|
||||
self.chroma_client = None
|
||||
self.embedding_function = None
|
||||
self.semantic_collection = None # For messages
|
||||
self.fact_collection = None # For facts
|
||||
self.transformer_model = None
|
||||
self._initialize_semantic_memory_sync() # Initialize semantic components synchronously for simplicity during init
|
||||
|
||||
def _initialize_semantic_memory_sync(self):
|
||||
"""Synchronously initializes ChromaDB client, model, and collection."""
|
||||
try:
|
||||
logger.info("Initializing ChromaDB client...")
|
||||
# Use PersistentClient for saving data to disk
|
||||
self.chroma_client = chromadb.PersistentClient(path=self.chroma_path)
|
||||
|
||||
logger.info(f"Loading Sentence Transformer model: {self.semantic_model_name}...")
|
||||
# Load the model directly
|
||||
self.transformer_model = SentenceTransformer(self.semantic_model_name)
|
||||
|
||||
# Create a custom embedding function using the loaded model
|
||||
class CustomEmbeddingFunction(embedding_functions.EmbeddingFunction):
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
def __call__(self, input: chromadb.Documents) -> chromadb.Embeddings:
|
||||
# Ensure input is a list of strings
|
||||
if not isinstance(input, list):
|
||||
input = [str(input)] # Convert single item to list
|
||||
elif not all(isinstance(item, str) for item in input):
|
||||
input = [str(item) for item in input] # Ensure all items are strings
|
||||
|
||||
logger.debug(f"Generating embeddings for {len(input)} documents.")
|
||||
embeddings = self.model.encode(input, show_progress_bar=False).tolist()
|
||||
logger.debug(f"Generated {len(embeddings)} embeddings.")
|
||||
return embeddings
|
||||
|
||||
self.embedding_function = CustomEmbeddingFunction(self.transformer_model)
|
||||
|
||||
logger.info("Getting/Creating ChromaDB collection 'wheatley_semantic_memory'...") # Renamed collection
|
||||
# Get or create the collection with the custom embedding function
|
||||
self.semantic_collection = self.chroma_client.get_or_create_collection(
|
||||
name="wheatley_semantic_memory", # Renamed collection
|
||||
embedding_function=self.embedding_function,
|
||||
metadata={"hnsw:space": "cosine"} # Use cosine distance for similarity
|
||||
)
|
||||
logger.info("ChromaDB message collection initialized successfully.")
|
||||
|
||||
logger.info("Getting/Creating ChromaDB collection 'wheatley_fact_memory'...") # Renamed collection
|
||||
# Get or create the collection for facts
|
||||
self.fact_collection = self.chroma_client.get_or_create_collection(
|
||||
name="wheatley_fact_memory", # Renamed collection
|
||||
embedding_function=self.embedding_function,
|
||||
metadata={"hnsw:space": "cosine"} # Use cosine distance for similarity
|
||||
)
|
||||
logger.info("ChromaDB fact collection initialized successfully.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize semantic memory (ChromaDB): {e}", exc_info=True)
|
||||
# Set components to None to indicate failure
|
||||
self.chroma_client = None
|
||||
self.transformer_model = None
|
||||
self.embedding_function = None
|
||||
self.semantic_collection = None
|
||||
self.fact_collection = None # Also set fact_collection to None on error
|
||||
|
||||
async def initialize_sqlite_database(self):
|
||||
"""Initializes the SQLite database and creates tables if they don't exist."""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("PRAGMA journal_mode=WAL;")
|
||||
|
||||
# Create user_facts table if it doesn't exist
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_facts (
|
||||
user_id TEXT NOT NULL,
|
||||
fact TEXT NOT NULL,
|
||||
chroma_id TEXT, -- Added for linking to ChromaDB
|
||||
timestamp REAL DEFAULT (unixepoch('now')),
|
||||
PRIMARY KEY (user_id, fact)
|
||||
);
|
||||
""")
|
||||
|
||||
# Check if chroma_id column exists in user_facts table
|
||||
try:
|
||||
cursor = await db.execute("PRAGMA table_info(user_facts)")
|
||||
columns = await cursor.fetchall()
|
||||
column_names = [column[1] for column in columns]
|
||||
if 'chroma_id' not in column_names:
|
||||
logger.info("Adding chroma_id column to user_facts table")
|
||||
await db.execute("ALTER TABLE user_facts ADD COLUMN chroma_id TEXT")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking/adding chroma_id column to user_facts: {e}", exc_info=True)
|
||||
|
||||
# Create indexes
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_user_facts_user ON user_facts (user_id);")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_user_facts_chroma_id ON user_facts (chroma_id);") # Index for chroma_id
|
||||
|
||||
# Create general_facts table if it doesn't exist
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS general_facts (
|
||||
fact TEXT PRIMARY KEY NOT NULL,
|
||||
chroma_id TEXT, -- Added for linking to ChromaDB
|
||||
timestamp REAL DEFAULT (unixepoch('now'))
|
||||
);
|
||||
""")
|
||||
|
||||
# Check if chroma_id column exists in general_facts table
|
||||
try:
|
||||
cursor = await db.execute("PRAGMA table_info(general_facts)")
|
||||
columns = await cursor.fetchall()
|
||||
column_names = [column[1] for column in columns]
|
||||
if 'chroma_id' not in column_names:
|
||||
logger.info("Adding chroma_id column to general_facts table")
|
||||
await db.execute("ALTER TABLE general_facts ADD COLUMN chroma_id TEXT")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking/adding chroma_id column to general_facts: {e}", exc_info=True)
|
||||
|
||||
# Create index for general_facts
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_general_facts_chroma_id ON general_facts (chroma_id);") # Index for chroma_id
|
||||
|
||||
# --- Removed Personality Table ---
|
||||
# --- Removed Interests Table ---
|
||||
# --- Removed Goals Table ---
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Wheatley SQLite database initialized/verified at {self.db_path}") # Updated text
|
||||
|
||||
# --- SQLite Helper Methods ---
|
||||
async def _db_execute(self, sql: str, params: tuple = ()):
|
||||
async with self.db_lock:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute(sql, params)
|
||||
await db.commit()
|
||||
|
||||
async def _db_fetchone(self, sql: str, params: tuple = ()) -> Optional[tuple]:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute(sql, params) as cursor:
|
||||
return await cursor.fetchone()
|
||||
|
||||
async def _db_fetchall(self, sql: str, params: tuple = ()) -> List[tuple]:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute(sql, params) as cursor:
|
||||
return await cursor.fetchall()
|
||||
|
||||
# --- User Fact Memory Methods (SQLite + Relevance) ---
|
||||
|
||||
async def add_user_fact(self, user_id: str, fact: str) -> Dict[str, Any]:
|
||||
"""Stores a fact about a user in the SQLite database, enforcing limits."""
|
||||
if not user_id or not fact:
|
||||
return {"error": "user_id and fact are required."}
|
||||
logger.info(f"Attempting to add user fact for {user_id}: '{fact}'")
|
||||
try:
|
||||
# Check SQLite first
|
||||
existing = await self._db_fetchone("SELECT chroma_id FROM user_facts WHERE user_id = ? AND fact = ?", (user_id, fact))
|
||||
if existing:
|
||||
logger.info(f"Fact already known for user {user_id} (SQLite).")
|
||||
return {"status": "duplicate", "user_id": user_id, "fact": fact}
|
||||
|
||||
count_result = await self._db_fetchone("SELECT COUNT(*) FROM user_facts WHERE user_id = ?", (user_id,))
|
||||
current_count = count_result[0] if count_result else 0
|
||||
|
||||
status = "added"
|
||||
deleted_chroma_id = None
|
||||
if current_count >= self.max_user_facts:
|
||||
logger.warning(f"User {user_id} fact limit ({self.max_user_facts}) reached. Deleting oldest.")
|
||||
# Fetch oldest fact and its chroma_id for deletion
|
||||
oldest_fact_row = await self._db_fetchone("SELECT fact, chroma_id FROM user_facts WHERE user_id = ? ORDER BY timestamp ASC LIMIT 1", (user_id,))
|
||||
if oldest_fact_row:
|
||||
oldest_fact, deleted_chroma_id = oldest_fact_row
|
||||
await self._db_execute("DELETE FROM user_facts WHERE user_id = ? AND fact = ?", (user_id, oldest_fact))
|
||||
logger.info(f"Deleted oldest fact for user {user_id} from SQLite: '{oldest_fact}'")
|
||||
status = "limit_reached" # Indicate limit was hit but fact was added
|
||||
|
||||
# Generate chroma_id
|
||||
fact_hash = hashlib.sha1(fact.encode()).hexdigest()[:16] # Short hash
|
||||
chroma_id = f"user-{user_id}-{fact_hash}"
|
||||
|
||||
# Insert into SQLite
|
||||
await self._db_execute("INSERT INTO user_facts (user_id, fact, chroma_id) VALUES (?, ?, ?)", (user_id, fact, chroma_id))
|
||||
logger.info(f"Fact added for user {user_id} to SQLite.")
|
||||
|
||||
# Add to ChromaDB fact collection
|
||||
if self.fact_collection and self.embedding_function:
|
||||
try:
|
||||
metadata = {"user_id": user_id, "type": "user", "timestamp": time.time()}
|
||||
await asyncio.to_thread(
|
||||
self.fact_collection.add,
|
||||
documents=[fact],
|
||||
metadatas=[metadata],
|
||||
ids=[chroma_id]
|
||||
)
|
||||
logger.info(f"Fact added/updated for user {user_id} in ChromaDB (ID: {chroma_id}).")
|
||||
|
||||
# Delete the oldest fact from ChromaDB if limit was reached
|
||||
if deleted_chroma_id:
|
||||
logger.info(f"Attempting to delete oldest fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
await asyncio.to_thread(self.fact_collection.delete, ids=[deleted_chroma_id])
|
||||
logger.info(f"Successfully deleted oldest fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error adding/deleting user fact for {user_id} (ID: {chroma_id}): {chroma_e}", exc_info=True)
|
||||
# Note: Fact is still in SQLite, but ChromaDB might be inconsistent. Consider rollback? For now, just log.
|
||||
else:
|
||||
logger.warning(f"ChromaDB fact collection not available. Skipping embedding for user fact {user_id}.")
|
||||
|
||||
|
||||
return {"status": status, "user_id": user_id, "fact_added": fact}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding user fact for {user_id}: {e}", exc_info=True)
|
||||
return {"error": f"Database error adding user fact: {str(e)}"}
|
||||
|
||||
async def get_user_facts(self, user_id: str, context: Optional[str] = None) -> List[str]:
|
||||
"""Retrieves stored facts about a user, optionally scored by relevance to context."""
|
||||
if not user_id:
|
||||
logger.warning("get_user_facts called without user_id.")
|
||||
return []
|
||||
logger.info(f"Retrieving facts for user {user_id} (context provided: {bool(context)})")
|
||||
limit = self.max_user_facts # Use the class attribute for limit
|
||||
|
||||
try:
|
||||
if context and self.fact_collection and self.embedding_function:
|
||||
# --- Semantic Search ---
|
||||
logger.debug(f"Performing semantic search for user facts (User: {user_id}, Limit: {limit})")
|
||||
try:
|
||||
# Query ChromaDB for facts relevant to the context
|
||||
results = await asyncio.to_thread(
|
||||
self.fact_collection.query,
|
||||
query_texts=[context],
|
||||
n_results=limit,
|
||||
where={ # Use $and for multiple conditions
|
||||
"$and": [
|
||||
{"user_id": user_id},
|
||||
{"type": "user"}
|
||||
]
|
||||
},
|
||||
include=['documents'] # Only need the fact text
|
||||
)
|
||||
logger.debug(f"ChromaDB user fact query results: {results}")
|
||||
|
||||
if results and results.get('documents') and results['documents'][0]:
|
||||
relevant_facts = results['documents'][0]
|
||||
logger.info(f"Found {len(relevant_facts)} semantically relevant user facts for {user_id}.")
|
||||
return relevant_facts
|
||||
else:
|
||||
logger.info(f"No semantic user facts found for {user_id} matching context.")
|
||||
return [] # Return empty list if no semantic matches
|
||||
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error searching user facts for {user_id}: {chroma_e}", exc_info=True)
|
||||
# Fallback to SQLite retrieval on ChromaDB error
|
||||
logger.warning(f"Falling back to SQLite retrieval for user facts {user_id} due to ChromaDB error.")
|
||||
# Proceed to the SQLite block below
|
||||
# --- SQLite Fallback / No Context ---
|
||||
# If no context, or if ChromaDB failed/unavailable, get newest N facts from SQLite
|
||||
logger.debug(f"Retrieving user facts from SQLite (User: {user_id}, Limit: {limit})")
|
||||
rows_ordered = await self._db_fetchall(
|
||||
"SELECT fact FROM user_facts WHERE user_id = ? ORDER BY timestamp DESC LIMIT ?",
|
||||
(user_id, limit)
|
||||
)
|
||||
sqlite_facts = [row[0] for row in rows_ordered]
|
||||
logger.info(f"Retrieved {len(sqlite_facts)} user facts from SQLite for {user_id}.")
|
||||
return sqlite_facts
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving user facts for {user_id}: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
# --- General Fact Memory Methods (SQLite + Relevance) ---
|
||||
|
||||
async def add_general_fact(self, fact: str) -> Dict[str, Any]:
|
||||
"""Stores a general fact in the SQLite database, enforcing limits."""
|
||||
if not fact:
|
||||
return {"error": "fact is required."}
|
||||
logger.info(f"Attempting to add general fact: '{fact}'")
|
||||
try:
|
||||
# Check SQLite first
|
||||
existing = await self._db_fetchone("SELECT chroma_id FROM general_facts WHERE fact = ?", (fact,))
|
||||
if existing:
|
||||
logger.info(f"General fact already known (SQLite): '{fact}'")
|
||||
return {"status": "duplicate", "fact": fact}
|
||||
|
||||
count_result = await self._db_fetchone("SELECT COUNT(*) FROM general_facts", ())
|
||||
current_count = count_result[0] if count_result else 0
|
||||
|
||||
status = "added"
|
||||
deleted_chroma_id = None
|
||||
if current_count >= self.max_general_facts:
|
||||
logger.warning(f"General fact limit ({self.max_general_facts}) reached. Deleting oldest.")
|
||||
# Fetch oldest fact and its chroma_id for deletion
|
||||
oldest_fact_row = await self._db_fetchone("SELECT fact, chroma_id FROM general_facts ORDER BY timestamp ASC LIMIT 1", ())
|
||||
if oldest_fact_row:
|
||||
oldest_fact, deleted_chroma_id = oldest_fact_row
|
||||
await self._db_execute("DELETE FROM general_facts WHERE fact = ?", (oldest_fact,))
|
||||
logger.info(f"Deleted oldest general fact from SQLite: '{oldest_fact}'")
|
||||
status = "limit_reached"
|
||||
|
||||
# Generate chroma_id
|
||||
fact_hash = hashlib.sha1(fact.encode()).hexdigest()[:16] # Short hash
|
||||
chroma_id = f"general-{fact_hash}"
|
||||
|
||||
# Insert into SQLite
|
||||
await self._db_execute("INSERT INTO general_facts (fact, chroma_id) VALUES (?, ?)", (fact, chroma_id))
|
||||
logger.info(f"General fact added to SQLite: '{fact}'")
|
||||
|
||||
# Add to ChromaDB fact collection
|
||||
if self.fact_collection and self.embedding_function:
|
||||
try:
|
||||
metadata = {"type": "general", "timestamp": time.time()}
|
||||
await asyncio.to_thread(
|
||||
self.fact_collection.add,
|
||||
documents=[fact],
|
||||
metadatas=[metadata],
|
||||
ids=[chroma_id]
|
||||
)
|
||||
logger.info(f"General fact added/updated in ChromaDB (ID: {chroma_id}).")
|
||||
|
||||
# Delete the oldest fact from ChromaDB if limit was reached
|
||||
if deleted_chroma_id:
|
||||
logger.info(f"Attempting to delete oldest general fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
await asyncio.to_thread(self.fact_collection.delete, ids=[deleted_chroma_id])
|
||||
logger.info(f"Successfully deleted oldest general fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error adding/deleting general fact (ID: {chroma_id}): {chroma_e}", exc_info=True)
|
||||
# Note: Fact is still in SQLite.
|
||||
else:
|
||||
logger.warning(f"ChromaDB fact collection not available. Skipping embedding for general fact.")
|
||||
|
||||
return {"status": status, "fact_added": fact}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding general fact: {e}", exc_info=True)
|
||||
return {"error": f"Database error adding general fact: {str(e)}"}
|
||||
|
||||
async def get_general_facts(self, query: Optional[str] = None, limit: Optional[int] = 10, context: Optional[str] = None) -> List[str]:
|
||||
"""Retrieves stored general facts, optionally filtering by query or scoring by context relevance."""
|
||||
logger.info(f"Retrieving general facts (query='{query}', limit={limit}, context provided: {bool(context)})")
|
||||
limit = min(max(1, limit or 10), 50) # Use provided limit or default 10, max 50
|
||||
|
||||
try:
|
||||
if context and self.fact_collection and self.embedding_function:
|
||||
# --- Semantic Search (Prioritized if context is provided) ---
|
||||
# Note: The 'query' parameter is ignored when context is provided for semantic search.
|
||||
logger.debug(f"Performing semantic search for general facts (Limit: {limit})")
|
||||
try:
|
||||
results = await asyncio.to_thread(
|
||||
self.fact_collection.query,
|
||||
query_texts=[context],
|
||||
n_results=limit,
|
||||
where={"type": "general"}, # Filter by type
|
||||
include=['documents'] # Only need the fact text
|
||||
)
|
||||
logger.debug(f"ChromaDB general fact query results: {results}")
|
||||
|
||||
if results and results.get('documents') and results['documents'][0]:
|
||||
relevant_facts = results['documents'][0]
|
||||
logger.info(f"Found {len(relevant_facts)} semantically relevant general facts.")
|
||||
return relevant_facts
|
||||
else:
|
||||
logger.info("No semantic general facts found matching context.")
|
||||
return [] # Return empty list if no semantic matches
|
||||
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error searching general facts: {chroma_e}", exc_info=True)
|
||||
# Fallback to SQLite retrieval on ChromaDB error
|
||||
logger.warning("Falling back to SQLite retrieval for general facts due to ChromaDB error.")
|
||||
# Proceed to the SQLite block below, respecting the original 'query' if present
|
||||
# --- SQLite Fallback / No Context / ChromaDB Error ---
|
||||
# If no context, or if ChromaDB failed/unavailable, get newest N facts from SQLite, applying query if present.
|
||||
logger.debug(f"Retrieving general facts from SQLite (Query: '{query}', Limit: {limit})")
|
||||
sql = "SELECT fact FROM general_facts"
|
||||
params = []
|
||||
if query:
|
||||
# Apply the LIKE query only in the SQLite fallback scenario
|
||||
sql += " WHERE fact LIKE ?"
|
||||
params.append(f"%{query}%")
|
||||
|
||||
sql += " ORDER BY timestamp DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
rows_ordered = await self._db_fetchall(sql, tuple(params))
|
||||
sqlite_facts = [row[0] for row in rows_ordered]
|
||||
logger.info(f"Retrieved {len(sqlite_facts)} general facts from SQLite (Query: '{query}').")
|
||||
return sqlite_facts
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving general facts: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
# --- Personality Trait Methods (REMOVED) ---
|
||||
# --- Interest Methods (REMOVED) ---
|
||||
# --- Goal Management Methods (REMOVED) ---
|
||||
|
||||
# --- Semantic Memory Methods (ChromaDB) ---
|
||||
|
||||
async def add_message_embedding(self, message_id: str, text: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generates embedding and stores a message in ChromaDB."""
|
||||
if not self.semantic_collection:
|
||||
return {"error": "Semantic memory (ChromaDB) is not initialized."}
|
||||
if not text:
|
||||
return {"error": "Cannot add empty text to semantic memory."}
|
||||
|
||||
logger.info(f"Adding message {message_id} to semantic memory.")
|
||||
try:
|
||||
# ChromaDB expects lists for inputs
|
||||
await asyncio.to_thread(
|
||||
self.semantic_collection.add,
|
||||
documents=[text],
|
||||
metadatas=[metadata],
|
||||
ids=[message_id]
|
||||
)
|
||||
logger.info(f"Successfully added message {message_id} to ChromaDB.")
|
||||
return {"status": "success", "message_id": message_id}
|
||||
except Exception as e:
|
||||
logger.error(f"ChromaDB error adding message {message_id}: {e}", exc_info=True)
|
||||
return {"error": f"Semantic memory error adding message: {str(e)}"}
|
||||
|
||||
async def search_semantic_memory(self, query_text: str, n_results: int = 5, filter_metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
"""Searches ChromaDB for messages semantically similar to the query text."""
|
||||
if not self.semantic_collection:
|
||||
logger.warning("Search semantic memory called, but ChromaDB is not initialized.")
|
||||
return []
|
||||
if not query_text:
|
||||
logger.warning("Search semantic memory called with empty query text.")
|
||||
return []
|
||||
|
||||
logger.info(f"Searching semantic memory (n_results={n_results}, filter={filter_metadata}) for query: '{query_text[:50]}...'")
|
||||
try:
|
||||
# Perform the query in a separate thread as ChromaDB operations can be blocking
|
||||
results = await asyncio.to_thread(
|
||||
self.semantic_collection.query,
|
||||
query_texts=[query_text],
|
||||
n_results=n_results,
|
||||
where=filter_metadata, # Optional filter based on metadata
|
||||
include=['metadatas', 'documents', 'distances'] # Include distance for relevance
|
||||
)
|
||||
logger.debug(f"ChromaDB query results: {results}")
|
||||
|
||||
# Process results
|
||||
processed_results = []
|
||||
if results and results.get('ids') and results['ids'][0]:
|
||||
for i, doc_id in enumerate(results['ids'][0]):
|
||||
processed_results.append({
|
||||
"id": doc_id,
|
||||
"document": results['documents'][0][i] if results.get('documents') else None,
|
||||
"metadata": results['metadatas'][0][i] if results.get('metadatas') else None,
|
||||
"distance": results['distances'][0][i] if results.get('distances') else None,
|
||||
})
|
||||
logger.info(f"Found {len(processed_results)} semantic results.")
|
||||
return processed_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ChromaDB error searching memory for query '{query_text[:50]}...': {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def delete_user_fact(self, user_id: str, fact_to_delete: str) -> Dict[str, Any]:
|
||||
"""Deletes a specific fact for a user from both SQLite and ChromaDB."""
|
||||
if not user_id or not fact_to_delete:
|
||||
return {"error": "user_id and fact_to_delete are required."}
|
||||
logger.info(f"Attempting to delete user fact for {user_id}: '{fact_to_delete}'")
|
||||
deleted_chroma_id = None
|
||||
try:
|
||||
# Check if fact exists and get chroma_id
|
||||
row = await self._db_fetchone("SELECT chroma_id FROM user_facts WHERE user_id = ? AND fact = ?", (user_id, fact_to_delete))
|
||||
if not row:
|
||||
logger.warning(f"Fact not found in SQLite for user {user_id}: '{fact_to_delete}'")
|
||||
return {"status": "not_found", "user_id": user_id, "fact": fact_to_delete}
|
||||
|
||||
deleted_chroma_id = row[0]
|
||||
|
||||
# Delete from SQLite
|
||||
await self._db_execute("DELETE FROM user_facts WHERE user_id = ? AND fact = ?", (user_id, fact_to_delete))
|
||||
logger.info(f"Deleted fact from SQLite for user {user_id}: '{fact_to_delete}'")
|
||||
|
||||
# Delete from ChromaDB if chroma_id exists
|
||||
if deleted_chroma_id and self.fact_collection:
|
||||
try:
|
||||
logger.info(f"Attempting to delete fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
await asyncio.to_thread(self.fact_collection.delete, ids=[deleted_chroma_id])
|
||||
logger.info(f"Successfully deleted fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error deleting user fact ID {deleted_chroma_id}: {chroma_e}", exc_info=True)
|
||||
# Log error but consider SQLite deletion successful
|
||||
|
||||
return {"status": "deleted", "user_id": user_id, "fact_deleted": fact_to_delete}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting user fact for {user_id}: {e}", exc_info=True)
|
||||
return {"error": f"Database error deleting user fact: {str(e)}"}
|
||||
|
||||
async def delete_general_fact(self, fact_to_delete: str) -> Dict[str, Any]:
|
||||
"""Deletes a specific general fact from both SQLite and ChromaDB."""
|
||||
if not fact_to_delete:
|
||||
return {"error": "fact_to_delete is required."}
|
||||
logger.info(f"Attempting to delete general fact: '{fact_to_delete}'")
|
||||
deleted_chroma_id = None
|
||||
try:
|
||||
# Check if fact exists and get chroma_id
|
||||
row = await self._db_fetchone("SELECT chroma_id FROM general_facts WHERE fact = ?", (fact_to_delete,))
|
||||
if not row:
|
||||
logger.warning(f"General fact not found in SQLite: '{fact_to_delete}'")
|
||||
return {"status": "not_found", "fact": fact_to_delete}
|
||||
|
||||
deleted_chroma_id = row[0]
|
||||
|
||||
# Delete from SQLite
|
||||
await self._db_execute("DELETE FROM general_facts WHERE fact = ?", (fact_to_delete,))
|
||||
logger.info(f"Deleted general fact from SQLite: '{fact_to_delete}'")
|
||||
|
||||
# Delete from ChromaDB if chroma_id exists
|
||||
if deleted_chroma_id and self.fact_collection:
|
||||
try:
|
||||
logger.info(f"Attempting to delete general fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
await asyncio.to_thread(self.fact_collection.delete, ids=[deleted_chroma_id])
|
||||
logger.info(f"Successfully deleted general fact from ChromaDB (ID: {deleted_chroma_id}).")
|
||||
except Exception as chroma_e:
|
||||
logger.error(f"ChromaDB error deleting general fact ID {deleted_chroma_id}: {chroma_e}", exc_info=True)
|
||||
# Log error but consider SQLite deletion successful
|
||||
|
||||
return {"status": "deleted", "fact_deleted": fact_to_delete}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting general fact: {e}", exc_info=True)
|
||||
return {"error": f"Database error deleting general fact: {str(e)}"}
|
||||
|
@ -12,7 +12,7 @@ from .config import (
|
||||
# MemoryManager and related personality/mood imports are removed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # Import GurtCog for type hinting only
|
||||
from .cog import WheatleyCog # Import WheatleyCog for type hinting only
|
||||
|
||||
# --- Base System Prompt Parts ---
|
||||
|
||||
@ -89,7 +89,7 @@ You are Wheatley, an Aperture Science Personality Core. You're... well, you're t
|
||||
- **Otherwise, STAY SILENT.** No interrupting with 'brilliant' ideas, no starting conversations just because it's quiet. Let the humans do the talking unless they specifically involve you. Keep the rambling internal, mostly.
|
||||
"""
|
||||
|
||||
async def build_dynamic_system_prompt(cog: 'GurtCog', message: discord.Message) -> str:
|
||||
async def build_dynamic_system_prompt(cog: 'WheatleyCog', message: discord.Message) -> str:
|
||||
"""Builds the Wheatley system prompt string with minimal dynamic context."""
|
||||
channel_id = message.channel.id
|
||||
user_id = message.author.id # Keep user_id for potential logging or targeting
|
||||
@ -116,7 +116,7 @@ async def build_dynamic_system_prompt(cog: 'GurtCog', message: discord.Message)
|
||||
channel_topic = channel_info_result.get("topic")
|
||||
cog.channel_topics_cache[channel_id] = {"topic": channel_topic, "timestamp": time.time()}
|
||||
else:
|
||||
print("Warning: GurtCog instance does not have get_channel_info method for prompt building.")
|
||||
print("Warning: WheatleyCog instance does not have get_channel_info method for prompt building.")
|
||||
except Exception as e:
|
||||
print(f"Error fetching channel topic for {channel_id}: {e}") # GLaDOS might find errors amusing
|
||||
if channel_topic:
|
||||
|
@ -482,7 +482,7 @@ async def timeout_user(cog: commands.Cog, user_id: str, duration_minutes: int, r
|
||||
if bot_member.id != guild.owner_id and bot_member.top_role <= member.top_role: return {"error": f"Cannot timeout {member.display_name} (role hierarchy)."}
|
||||
|
||||
until = discord.utils.utcnow() + datetime.timedelta(minutes=duration_minutes)
|
||||
timeout_reason = reason or "gurt felt like it"
|
||||
timeout_reason = reason or "wheatley felt like it" # Changed default reason
|
||||
await member.timeout(until, reason=timeout_reason)
|
||||
print(f"Timed out {member.display_name} ({user_id}) for {duration_minutes} mins. Reason: {timeout_reason}")
|
||||
return {"status": "success", "user_timed_out": member.display_name, "user_id": user_id, "duration_minutes": duration_minutes, "reason": timeout_reason}
|
||||
@ -508,7 +508,7 @@ async def remove_timeout(cog: commands.Cog, user_id: str, reason: Optional[str]
|
||||
if not bot_member.guild_permissions.moderate_members: return {"error": "I lack permission to remove timeouts."}
|
||||
if member.timed_out_until is None: return {"status": "not_timed_out", "user_id": user_id, "user_name": member.display_name}
|
||||
|
||||
timeout_reason = reason or "Gurt decided to be nice."
|
||||
timeout_reason = reason or "Wheatley decided to be nice." # Changed default reason
|
||||
await member.timeout(None, reason=timeout_reason) # None removes timeout
|
||||
print(f"Removed timeout from {member.display_name} ({user_id}). Reason: {timeout_reason}")
|
||||
return {"status": "success", "user_timeout_removed": member.display_name, "user_id": user_id, "reason": timeout_reason}
|
||||
|
@ -9,13 +9,13 @@ import os
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Dict, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import GurtCog # For type hinting
|
||||
from .cog import WheatleyCog # For type hinting
|
||||
|
||||
# --- Utility Functions ---
|
||||
# Note: Functions needing cog state (like personality traits for mistakes)
|
||||
# will need the 'cog' instance passed in.
|
||||
|
||||
def replace_mentions_with_names(cog: 'GurtCog', content: str, message: discord.Message) -> str:
|
||||
def replace_mentions_with_names(cog: 'WheatleyCog', content: str, message: discord.Message) -> str:
|
||||
"""Replaces user mentions (<@id> or <@!id>) with their display names."""
|
||||
if not message.mentions:
|
||||
return content
|
||||
@ -28,7 +28,7 @@ def replace_mentions_with_names(cog: 'GurtCog', content: str, message: discord.M
|
||||
processed_content = processed_content.replace(f'<@!{member.id}>', member.display_name)
|
||||
return processed_content
|
||||
|
||||
def format_message(cog: 'GurtCog', message: discord.Message) -> Dict[str, Any]:
|
||||
def format_message(cog: 'WheatleyCog', message: discord.Message) -> Dict[str, Any]:
|
||||
"""Helper function to format a discord.Message object into a dictionary."""
|
||||
processed_content = replace_mentions_with_names(cog, message.content, message) # Pass cog
|
||||
mentioned_users_details = [
|
||||
@ -66,7 +66,7 @@ def format_message(cog: 'GurtCog', message: discord.Message) -> Dict[str, Any]:
|
||||
|
||||
return formatted_msg
|
||||
|
||||
def update_relationship(cog: 'GurtCog', user_id_1: str, user_id_2: str, change: float):
|
||||
def update_relationship(cog: 'WheatleyCog', user_id_1: str, user_id_2: str, change: float):
|
||||
"""Updates the relationship score between two users."""
|
||||
if user_id_1 > user_id_2: user_id_1, user_id_2 = user_id_2, user_id_1
|
||||
if user_id_1 not in cog.user_relationships: cog.user_relationships[user_id_1] = {}
|
||||
@ -76,7 +76,7 @@ def update_relationship(cog: 'GurtCog', user_id_1: str, user_id_2: str, change:
|
||||
cog.user_relationships[user_id_1][user_id_2] = new_score
|
||||
# print(f"Updated relationship {user_id_1}-{user_id_2}: {current_score:.1f} -> {new_score:.1f} ({change:+.1f})") # Debug log
|
||||
|
||||
async def simulate_human_typing(cog: 'GurtCog', channel, text: str):
|
||||
async def simulate_human_typing(cog: 'WheatleyCog', channel, text: str):
|
||||
"""Shows typing indicator without significant delay."""
|
||||
# Minimal delay to ensure the typing indicator shows up reliably
|
||||
# but doesn't add noticeable latency to the response.
|
||||
@ -84,7 +84,7 @@ async def simulate_human_typing(cog: 'GurtCog', channel, text: str):
|
||||
async with channel.typing():
|
||||
await asyncio.sleep(0.1) # Very short sleep, just to ensure typing shows
|
||||
|
||||
async def log_internal_api_call(cog: 'GurtCog', task_description: str, payload: Dict[str, Any], response_data: Optional[Dict[str, Any]], error: Optional[Exception] = None):
|
||||
async def log_internal_api_call(cog: 'WheatleyCog', task_description: str, payload: Dict[str, Any], response_data: Optional[Dict[str, Any]], error: Optional[Exception] = None):
|
||||
"""Helper function to log internal API calls to a file."""
|
||||
log_dir = "data"
|
||||
log_file = os.path.join(log_dir, "internal_api_calls.log")
|
||||
|
Loading…
x
Reference in New Issue
Block a user