added technical documentation
This commit is contained in:
418
docs/living-ai/opinion-system.md
Normal file
418
docs/living-ai/opinion-system.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
def extract_topics_from_message(message: str) -> list[str]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `OPINION_FORMATION_ENABLED` | `true` | Enable/disable opinion system |
|
||||
|
||||
---
|
||||
|
||||
## Example Usage
|
||||
|
||||
```python
|
||||
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)
|
||||
Reference in New Issue
Block a user