11 KiB
11 KiB
Opinion System Deep Dive
The opinion system allows the bot to develop and express opinions on topics it discusses with users.
Overview
The bot forms opinions through repeated discussions:
┌──────────────────────────────────────────────────────────────────────────────┐
│ Opinion Formation │
└──────────────────────────────────────────────────────────────────────────────┘
Discussion 1: "I love gaming!" → gaming: sentiment +0.8
Discussion 2: "Games are so fun!" → gaming: sentiment +0.7 (weighted avg)
Discussion 3: "I beat the boss!" → gaming: sentiment +0.6, interest +0.8
After 3+ discussions → Opinion formed!
"You really enjoy discussing gaming"
Opinion Attributes
Each opinion tracks:
| Attribute | Type | Range | Description |
|---|---|---|---|
topic |
string | - | The topic (lowercase) |
sentiment |
float | -1 to +1 | How positive/negative the bot feels |
interest_level |
float | 0 to 1 | How engaged/interested |
discussion_count |
int | 0+ | How often discussed |
reasoning |
string | - | AI-generated explanation (optional) |
last_reinforced_at |
datetime | - | When last discussed |
Sentiment Interpretation
| Range | Interpretation | Prompt Modifier |
|---|---|---|
| > 0.5 | Really enjoys | "You really enjoy discussing {topic}" |
| 0.2 to 0.5 | Finds interesting | "You find {topic} interesting" |
| -0.3 to 0.2 | Neutral | (no modifier) |
| < -0.3 | Not enthusiastic | "You're not particularly enthusiastic about {topic}" |
Interest Level Interpretation
| Range | Interpretation |
|---|---|
| 0.8 - 1.0 | Very engaged when discussing |
| 0.5 - 0.8 | Moderately interested |
| 0.2 - 0.5 | Somewhat interested |
| 0.0 - 0.2 | Not very interested |
Opinion Updates
When a topic is discussed, the opinion is updated using weighted averaging:
weight = 0.2 # 20% weight to new data
new_sentiment = (old_sentiment * 0.8) + (discussion_sentiment * 0.2)
new_interest = (old_interest * 0.8) + (engagement_level * 0.2)
Why 20% Weight?
- Prevents single interactions from dominating
- Opinions evolve gradually over time
- Reflects how real opinions form
- Protects against manipulation
Example Evolution
Initial: sentiment = 0.0, interest = 0.5
Discussion 1 (sentiment=0.8, engagement=0.7):
sentiment = 0.0 * 0.8 + 0.8 * 0.2 = 0.16
interest = 0.5 * 0.8 + 0.7 * 0.2 = 0.54
Discussion 2 (sentiment=0.6, engagement=0.9):
sentiment = 0.16 * 0.8 + 0.6 * 0.2 = 0.248
interest = 0.54 * 0.8 + 0.9 * 0.2 = 0.612
Discussion 3 (sentiment=0.7, engagement=0.8):
sentiment = 0.248 * 0.8 + 0.7 * 0.2 = 0.338
interest = 0.612 * 0.8 + 0.8 * 0.2 = 0.65
After 3 discussions: sentiment=0.34, interest=0.65
→ "You find programming interesting"
Topic Detection
Topics are extracted from messages using keyword matching:
Topic Categories
topic_keywords = {
# Hobbies
"gaming": ["game", "gaming", "video game", "play", "xbox", "playstation", ...],
"music": ["music", "song", "band", "album", "concert", "spotify", ...],
"movies": ["movie", "film", "cinema", "netflix", "show", "series", ...],
"reading": ["book", "read", "novel", "author", "library", "kindle"],
"sports": ["sports", "football", "soccer", "basketball", "gym", ...],
"cooking": ["cook", "recipe", "food", "restaurant", "meal", ...],
"travel": ["travel", "trip", "vacation", "flight", "hotel", ...],
"art": ["art", "painting", "drawing", "museum", "gallery", ...],
# Tech
"programming": ["code", "programming", "developer", "software", ...],
"technology": ["tech", "computer", "phone", "app", "website", ...],
"ai": ["ai", "artificial intelligence", "machine learning", ...],
# Life
"work": ["work", "job", "office", "career", "boss", "meeting"],
"family": ["family", "parents", "mom", "dad", "brother", "sister", ...],
"pets": ["pet", "dog", "cat", "puppy", "kitten", "animal"],
"health": ["health", "doctor", "exercise", "diet", "sleep", ...],
# Interests
"philosophy": ["philosophy", "meaning", "life", "existence", ...],
"science": ["science", "research", "study", "experiment", ...],
"nature": ["nature", "outdoor", "hiking", "camping", "mountain", ...],
}
Detection Function
def extract_topics_from_message(message: str) -> list[str]:
message_lower = message.lower()
found_topics = []
for topic, keywords in topic_keywords.items():
for keyword in keywords:
if keyword in message_lower:
if topic not in found_topics:
found_topics.append(topic)
break
return found_topics
Example
Message: "I've been playing this new video game all weekend!"
Detected Topics: ["gaming"]
Opinion Requirements
Opinions are only considered "formed" after 3+ discussions:
async def get_top_interests(guild_id, limit=5):
return select(BotOpinion).where(
BotOpinion.guild_id == guild_id,
BotOpinion.discussion_count >= 3, # Minimum threshold
).order_by(...)
Why 3 discussions?
- Single mentions don't indicate sustained interest
- Prevents volatile opinion formation
- Ensures meaningful opinions
- Reflects genuine engagement with topic
Prompt Modifiers
Relevant opinions are included in the AI's system prompt:
def get_opinion_prompt_modifier(opinions: list[BotOpinion]) -> str:
parts = []
for op in opinions[:3]: # Max 3 opinions
if op.sentiment > 0.5:
parts.append(f"You really enjoy discussing {op.topic}")
elif op.sentiment > 0.2:
parts.append(f"You find {op.topic} interesting")
elif op.sentiment < -0.3:
parts.append(f"You're not particularly enthusiastic about {op.topic}")
if op.reasoning:
parts.append(f"({op.reasoning})")
return "; ".join(parts)
Example Output
You really enjoy discussing programming; You find gaming interesting;
You're not particularly enthusiastic about politics (You prefer
to focus on fun and creative topics).
Opinion Reasoning
Optionally, AI can generate reasoning for opinions:
await opinion_service.set_opinion_reasoning(
topic="programming",
guild_id=123,
reasoning="It's fascinating to help people create things"
)
This adds context when the opinion is mentioned in prompts.
Database Schema
BotOpinion Table
| Column | Type | Description |
|---|---|---|
id |
Integer | Primary key |
guild_id |
BigInteger | Guild ID (nullable for global) |
topic |
String | Topic name (lowercase) |
sentiment |
Float | -1 to +1 sentiment |
interest_level |
Float | 0 to 1 interest |
discussion_count |
Integer | Number of discussions |
reasoning |
String | AI explanation (optional) |
formed_at |
DateTime | When first discussed |
last_reinforced_at |
DateTime | When last discussed |
Unique Constraint
Each (guild_id, topic) combination is unique.
API Reference
OpinionService
class OpinionService:
def __init__(self, session: AsyncSession)
async def get_opinion(
self,
topic: str,
guild_id: int | None = None
) -> BotOpinion | None
async def get_or_create_opinion(
self,
topic: str,
guild_id: int | None = None
) -> BotOpinion
async def record_topic_discussion(
self,
topic: str,
guild_id: int | None,
sentiment: float,
engagement_level: float,
) -> BotOpinion
async def set_opinion_reasoning(
self,
topic: str,
guild_id: int | None,
reasoning: str
) -> None
async def get_top_interests(
self,
guild_id: int | None = None,
limit: int = 5
) -> list[BotOpinion]
async def get_relevant_opinions(
self,
topics: list[str],
guild_id: int | None = None
) -> list[BotOpinion]
def get_opinion_prompt_modifier(
self,
opinions: list[BotOpinion]
) -> str
async def get_all_opinions(
self,
guild_id: int | None = None
) -> list[BotOpinion]
Helper Function
def extract_topics_from_message(message: str) -> list[str]
Configuration
| Variable | Default | Description |
|---|---|---|
OPINION_FORMATION_ENABLED |
true |
Enable/disable opinion system |
Example Usage
from daemon_boyfriend.services.opinion_service import (
OpinionService,
extract_topics_from_message
)
async with get_session() as session:
opinion_service = OpinionService(session)
# Extract topics from message
message = "I've been coding a lot in Python lately!"
topics = extract_topics_from_message(message)
# topics = ["programming"]
# Record discussion with positive sentiment
for topic in topics:
await opinion_service.record_topic_discussion(
topic=topic,
guild_id=123,
sentiment=0.7,
engagement_level=0.8
)
# Get top interests for prompt building
interests = await opinion_service.get_top_interests(guild_id=123)
print("Bot's top interests:")
for interest in interests:
print(f" {interest.topic}: sentiment={interest.sentiment:.2f}")
# Get opinions relevant to current conversation
relevant = await opinion_service.get_relevant_opinions(
topics=["programming", "gaming"],
guild_id=123
)
modifier = opinion_service.get_opinion_prompt_modifier(relevant)
print(f"Prompt modifier: {modifier}")
await session.commit()
Use in Conversation Flow
1. User sends message
│
▼
2. Extract topics from message
topics = extract_topics_from_message(message)
│
▼
3. Get relevant opinions
opinions = await opinion_service.get_relevant_opinions(topics, guild_id)
│
▼
4. Add to system prompt
prompt += opinion_service.get_opinion_prompt_modifier(opinions)
│
▼
5. Generate response
│
▼
6. Record topic discussions (post-response)
for topic in topics:
await opinion_service.record_topic_discussion(
topic, guild_id, sentiment, engagement
)
Design Considerations
Why Per-Guild?
- Different server contexts may lead to different opinions
- Gaming server → gaming-positive opinions
- Work server → professional topic preferences
- Allows customization per community
Why Keyword-Based Detection?
- Fast and simple
- No additional AI API calls
- Predictable behavior
- Easy to extend with more keywords
Future Improvements
For production, consider:
- NLP-based topic extraction
- LLM topic detection for nuanced topics
- Hierarchical topic relationships
- Opinion decay over time (for less-discussed topics)