added technical documentation

This commit is contained in:
2026-01-13 17:20:52 +00:00
parent ff394c9250
commit b29822efc7
12 changed files with 4953 additions and 1 deletions

301
docs/living-ai/README.md Normal file
View File

@@ -0,0 +1,301 @@
# Living AI System
The Living AI system gives the bot personality, emotional depth, and relationship awareness. It transforms a simple chatbot into a character that learns, remembers, and evolves through interactions.
## Table of Contents
- [Overview](#overview)
- [System Components](#system-components)
- [How It Works Together](#how-it-works-together)
- [Feature Toggle Reference](#feature-toggle-reference)
## Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Living AI System │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Mood │ │ Relationship │ │ Fact │ │ Opinion │ │
│ │ System │ │ System │ │ Extraction │ │ System │ │
│ │ │ │ │ │ │ │ │ │
│ │ Valence + │ │ 5 levels │ │ AI-based │ │ Topic │ │
│ │ Arousal │ │ 0-100 score │ │ learning │ │ sentiments │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Communication │ │ Proactive │ │ Association │ │ Self │ │
│ │ Style │ │ Events │ │ System │ │ Awareness │ │
│ │ │ │ │ │ │ │ │ │
│ │ Learned │ │ Birthdays, │ │ Cross-user │ │ Stats, │ │
│ │ preferences │ │ follow-ups │ │ memory │ │ reflection │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## System Components
### 1. Mood System
**File:** `services/mood_service.py`
**Documentation:** [mood-system.md](mood-system.md)
The bot has emotions that affect how it responds. Uses a valence-arousal psychological model:
- **Valence** (-1 to +1): Negative to positive emotional state
- **Arousal** (-1 to +1): Calm to excited energy level
**Mood Labels:**
| Valence | Arousal | Label |
|---------|---------|-------|
| High | High | Excited |
| High | Low | Happy |
| Neutral | Low | Calm |
| Neutral | Neutral | Neutral |
| Low | Low | Bored |
| Low | High | Annoyed |
| Neutral | High | Curious |
**Key Features:**
- Time decay: Mood gradually returns to neutral
- Inertia: Changes are dampened (30% absorption rate)
- Mood affects response style via prompt modifiers
---
### 2. Relationship System
**File:** `services/relationship_service.py`
**Documentation:** [relationship-system.md](relationship-system.md)
Tracks relationship depth with each user:
| Score | Level | Behavior |
|-------|-------|----------|
| 0-20 | Stranger | Polite, formal |
| 21-40 | Acquaintance | Friendly, reserved |
| 41-60 | Friend | Casual, warm |
| 61-80 | Good Friend | Personal, references past |
| 81-100 | Close Friend | Very casual, inside jokes |
**Relationship Score Factors:**
- Interaction sentiment (+/- 0.5 base)
- Message length (up to +0.3 bonus)
- Conversation depth (up to +0.2 bonus)
- Minimum interaction bonus (+0.1)
---
### 3. Fact Extraction System
**File:** `services/fact_extraction_service.py`
**Documentation:** [fact-extraction.md](fact-extraction.md)
Autonomously learns facts about users from conversations:
**Fact Types:**
- `hobby` - Activities and interests
- `work` - Job, career, professional life
- `family` - Family members and relationships
- `preference` - Likes, dislikes, preferences
- `location` - Where they live, travel to
- `event` - Important life events
- `relationship` - Personal relationships
- `general` - Other facts
**Extraction Process:**
1. Rate-limited (default 30% of messages)
2. AI analyzes message for extractable facts
3. Deduplication against existing facts
4. Facts stored with confidence and importance scores
---
### 4. Opinion System
**File:** `services/opinion_service.py`
**Documentation:** [opinion-system.md](opinion-system.md)
Bot develops opinions on topics over time:
**Opinion Attributes:**
- **Sentiment** (-1 to +1): How positive/negative about the topic
- **Interest Level** (0 to 1): How engaged when discussing
- **Discussion Count**: How often topic has come up
- **Reasoning**: AI-generated explanation (optional)
**Topic Detection:**
Simple keyword-based extraction for categories like:
- Hobbies (gaming, music, movies, etc.)
- Technology (programming, AI, etc.)
- Life (work, family, health, etc.)
- Interests (philosophy, science, nature, etc.)
---
### 5. Communication Style System
**File:** `services/communication_style_service.py`
Learns each user's preferred communication style:
**Tracked Preferences:**
- **Response Length**: short / medium / long
- **Formality**: 0 (casual) to 1 (formal)
- **Emoji Usage**: 0 (none) to 1 (frequent)
- **Humor Level**: 0 (serious) to 1 (playful)
- **Detail Level**: 0 (brief) to 1 (thorough)
**Learning Process:**
- Analyzes last 50 messages (rolling window)
- Requires 10+ samples for confidence > 0.3
- Generates prompt modifiers to adapt response style
---
### 6. Proactive Events System
**File:** `services/proactive_service.py`
Schedules and triggers proactive messages:
**Event Types:**
- **Birthday**: Remembers and celebrates birthdays
- **Follow-up**: Returns to check on mentioned events
- **Reminder**: General scheduled reminders
**Detection Methods:**
- Birthday: Regex patterns for dates
- Follow-up: AI-based event detection or keyword matching
**Event Lifecycle:**
1. Detection from conversation
2. Scheduled in database
3. Triggered when due
4. Personalized message generated
---
### 7. Association System
**File:** `services/association_service.py`
Links facts across different users (optional, disabled by default):
**Use Cases:**
- "User A and User B both work at the same company"
- "Multiple users share an interest in hiking"
- Enables group context and shared topic suggestions
---
### 8. Self-Awareness System
**File:** `services/self_awareness_service.py`
Provides the bot with statistics about itself:
**Available Stats:**
- Age (time since first activation)
- Total messages sent
- Total facts learned
- Total users known
- Favorite topics (from opinions)
Used for the `!botstats` command and self-reflection in responses.
---
## How It Works Together
When a user sends a message, the Living AI components work together:
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ Message Processing │
└──────────────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐
│ Get Mood ││Get Relationship│ Get Style │
│ ││ ││ │
│ Current state││ Level + refs ││ Preferences │
└──────────────┘└──────────────┘└──────────────┘
│ │ │
└───────────────┼───────────────┘
┌──────────────────────────────────────┐
│ Build Enhanced System Prompt │
│ │
│ Base personality + mood modifier + │
│ relationship context + style prefs + │
│ relevant opinions │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Generate Response │
└──────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐
│ Update Mood ││ Record ││ Maybe │
│ ││ Interaction ││Extract Facts │
│Sentiment + ││ ││ │
│arousal delta ││Score update ││ Rate-limited │
└──────────────┘└──────────────┘└──────────────┘
┌──────────────────────────────────────┐
│ Check for Proactive Events │
│ │
│ Detect birthdays, follow-ups │
└──────────────────────────────────────┘
```
### Example System Prompt Enhancement
```
[Base Personality]
You are Daemon, a friendly AI companion...
[Mood Modifier]
You're feeling enthusiastic and energetic right now! Be expressive,
use exclamation marks, show genuine excitement.
[Relationship Context]
This is a good friend you know well. Be relaxed and personal.
Reference things you've talked about before. Feel free to be playful.
You have inside jokes together: "the coffee incident".
[Communication Style]
This user prefers longer, detailed responses with some humor.
They use casual language, so match their tone.
[Relevant Opinions]
You really enjoy discussing programming; You find gaming interesting.
```
---
## Feature Toggle Reference
All Living AI features can be individually enabled/disabled:
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `LIVING_AI_ENABLED` | `true` | Master switch for all Living AI features |
| `MOOD_ENABLED` | `true` | Enable mood system |
| `RELATIONSHIP_ENABLED` | `true` | Enable relationship tracking |
| `FACT_EXTRACTION_ENABLED` | `true` | Enable autonomous fact extraction |
| `FACT_EXTRACTION_RATE` | `0.3` | Probability of extracting facts (0-1) |
| `PROACTIVE_ENABLED` | `true` | Enable proactive messages |
| `CROSS_USER_ENABLED` | `false` | Enable cross-user associations |
| `OPINION_FORMATION_ENABLED` | `true` | Enable opinion formation |
| `STYLE_LEARNING_ENABLED` | `true` | Enable communication style learning |
| `MOOD_DECAY_RATE` | `0.1` | How fast mood returns to neutral (per hour) |
---
## Detailed Documentation
- [Mood System Deep Dive](mood-system.md)
- [Relationship System Deep Dive](relationship-system.md)
- [Fact Extraction Deep Dive](fact-extraction.md)
- [Opinion System Deep Dive](opinion-system.md)

View File

@@ -0,0 +1,441 @@
# Fact Extraction System Deep Dive
The fact extraction system autonomously learns facts about users from their conversations with the bot.
## Overview
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ Fact Extraction Pipeline │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Rate Limiter (30%) │
│ Only process ~30% of messages │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Extractability Check │
│ - Min 20 chars │
│ - Not a command │
│ - Not just greetings │
│ - Has enough text content │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ AI Fact Extraction │
│ Extracts structured facts │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Deduplication │
│ - Exact match check │
│ - Substring check │
│ - Word overlap check (70%) │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Validation & Storage │
│ Save valid, unique facts │
└──────────────────────────────────────┘
```
---
## Fact Types
| Type | Description | Examples |
|------|-------------|----------|
| `hobby` | Activities, interests, pastimes | "loves hiking", "plays guitar" |
| `work` | Job, career, professional life | "works as a software engineer at Google" |
| `family` | Family members, relationships | "has two younger sisters" |
| `preference` | Likes, dislikes, preferences | "prefers dark roast coffee" |
| `location` | Places they live, visit, are from | "lives in Amsterdam" |
| `event` | Important life events | "recently got married" |
| `relationship` | Personal relationships | "has a girlfriend named Sarah" |
| `general` | Other facts that don't fit | "speaks three languages" |
---
## Fact Attributes
Each extracted fact has:
| Attribute | Type | Description |
|-----------|------|-------------|
| `type` | string | One of the fact types above |
| `content` | string | The fact itself (third person) |
| `confidence` | float | How certain the extraction is |
| `importance` | float | How significant the fact is |
| `temporal` | string | Time relevance |
### Confidence Levels
| Level | Value | When to Use |
|-------|-------|-------------|
| Implied | 0.6 | Fact is suggested but not stated |
| Stated | 0.8 | Fact is clearly mentioned |
| Explicit | 1.0 | User directly stated the fact |
### Importance Levels
| Level | Value | Description |
|-------|-------|-------------|
| Trivial | 0.3 | Minor detail |
| Normal | 0.5 | Standard fact |
| Significant | 0.8 | Important information |
| Very Important | 1.0 | Major life fact |
### Temporal Relevance
| Value | Description | Example |
|-------|-------------|---------|
| `past` | Happened before | "used to live in Paris" |
| `present` | Currently true | "works at Microsoft" |
| `future` | Planned/expected | "getting married next month" |
| `timeless` | Always true | "was born in Japan" |
---
## Rate Limiting
To prevent excessive API calls and ensure quality:
```python
# Only attempt extraction on ~30% of messages
if random.random() > settings.fact_extraction_rate:
return [] # Skip this message
```
**Configuration:**
- `FACT_EXTRACTION_RATE` = 0.3 (default)
- Can be adjusted from 0.0 (disabled) to 1.0 (every message)
**Why Rate Limit?**
- Reduces AI API costs
- Not every message contains facts
- Prevents redundant extractions
- Spreads learning over time
---
## Extractability Checks
Before sending to AI, messages are filtered:
### Minimum Length
```python
MIN_MESSAGE_LENGTH = 20
if len(content) < MIN_MESSAGE_LENGTH:
return False
```
### Alpha Ratio
```python
# Must be at least 50% alphabetic characters
alpha_ratio = sum(c.isalpha() for c in content) / len(content)
if alpha_ratio < 0.5:
return False
```
### Command Detection
```python
# Skip command-like messages
if content.startswith(("!", "/", "?", ".")):
return False
```
### Short Phrase Filter
```python
short_phrases = [
"hi", "hello", "hey", "yo", "sup", "bye", "goodbye",
"thanks", "thank you", "ok", "okay", "yes", "no",
"yeah", "nah", "lol", "lmao", "haha", "hehe",
"nice", "cool", "wow"
]
if content.lower().strip() in short_phrases:
return False
```
---
## AI Extraction Prompt
The system sends a carefully crafted prompt to the AI:
```
You are a fact extraction assistant. Extract factual information
about the user from their message.
ALREADY KNOWN FACTS:
- [hobby] loves hiking
- [work] works as senior engineer at Google
RULES:
1. Only extract CONCRETE facts, not opinions or transient states
2. Skip if the fact is already known (listed above)
3. Skip greetings, questions, or meta-conversation
4. Skip vague statements like "I like stuff" - be specific
5. Focus on: hobbies, work, family, preferences, locations, events, relationships
6. Keep fact content concise (under 100 characters)
7. Maximum 3 facts per message
OUTPUT FORMAT:
Return a JSON array of facts, or empty array [] if no extractable facts.
```
### Example Input/Output
**Input:** "I just got promoted to senior engineer at Google last week!"
**Output:**
```json
[
{
"type": "work",
"content": "works as senior engineer at Google",
"confidence": 1.0,
"importance": 0.8,
"temporal": "present"
},
{
"type": "event",
"content": "recently got promoted",
"confidence": 1.0,
"importance": 0.7,
"temporal": "past"
}
]
```
**Input:** "hey what's up"
**Output:**
```json
[]
```
---
## Deduplication
Before saving, facts are checked for duplicates:
### 1. Exact Match
```python
if new_content.lower() in existing_content:
return True # Is duplicate
```
### 2. Substring Check
```python
# If one contains the other (for facts > 10 chars)
if len(new_lower) > 10 and len(existing) > 10:
if new_lower in existing or existing in new_lower:
return True
```
### 3. Word Overlap (70% threshold)
```python
new_words = set(new_lower.split())
existing_words = set(existing.split())
if len(new_words) > 2 and len(existing_words) > 2:
overlap = len(new_words & existing_words)
min_len = min(len(new_words), len(existing_words))
if overlap / min_len > 0.7:
return True
```
**Examples:**
- "loves hiking" vs "loves hiking" → **Duplicate** (exact)
- "works as engineer at Google" vs "engineer at Google" → **Duplicate** (substring)
- "has two younger sisters" vs "has two younger brothers" → **Duplicate** (70% overlap)
- "loves hiking" vs "enjoys cooking" → **Not duplicate**
---
## Database Schema
### UserFact Table
| Column | Type | Description |
|--------|------|-------------|
| `id` | Integer | Primary key |
| `user_id` | Integer | Foreign key to users |
| `fact_type` | String | Category (hobby, work, etc.) |
| `fact_content` | String | The fact content |
| `confidence` | Float | Extraction confidence (0-1) |
| `source` | String | "auto_extraction" or "manual" |
| `is_active` | Boolean | Whether fact is still valid |
| `learned_at` | DateTime | When fact was learned |
| `category` | String | Same as fact_type |
| `importance` | Float | Importance level (0-1) |
| `temporal_relevance` | String | past/present/future/timeless |
| `extracted_from_message_id` | BigInteger | Discord message ID |
| `extraction_context` | String | First 200 chars of source message |
---
## API Reference
### FactExtractionService
```python
class FactExtractionService:
MIN_MESSAGE_LENGTH = 20
MAX_FACTS_PER_MESSAGE = 3
def __init__(
self,
session: AsyncSession,
ai_service=None
)
async def maybe_extract_facts(
self,
user: User,
message_content: str,
discord_message_id: int | None = None,
) -> list[UserFact]
# Rate-limited extraction
async def extract_facts(
self,
user: User,
message_content: str,
discord_message_id: int | None = None,
) -> list[UserFact]
# Direct extraction (no rate limiting)
```
---
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `FACT_EXTRACTION_ENABLED` | `true` | Enable/disable fact extraction |
| `FACT_EXTRACTION_RATE` | `0.3` | Probability of extraction (0-1) |
---
## Example Usage
```python
from daemon_boyfriend.services.fact_extraction_service import FactExtractionService
async with get_session() as session:
fact_service = FactExtractionService(session, ai_service)
# Rate-limited extraction (recommended for normal use)
new_facts = await fact_service.maybe_extract_facts(
user=user,
message_content="I just started learning Japanese!",
discord_message_id=123456789
)
for fact in new_facts:
print(f"Learned: [{fact.fact_type}] {fact.fact_content}")
# Direct extraction (skips rate limiting)
facts = await fact_service.extract_facts(
user=user,
message_content="I work at Microsoft as a PM"
)
await session.commit()
```
---
## Manual Fact Addition
Users can also add facts manually:
### !remember Command
```
User: !remember I'm allergic to peanuts
Bot: Got it! I'll remember that you're allergic to peanuts.
```
These facts have:
- `source = "manual"` instead of `"auto_extraction"`
- `confidence = 1.0` (user stated directly)
- `importance = 0.8` (user wanted it remembered)
### Admin Command
```
Admin: !teachbot @user Works night shifts
Bot: Got it! I've noted that about @user.
```
---
## Fact Retrieval
Facts are used in AI prompts for context:
```python
# Build user context including facts
async def build_user_context(user: User) -> str:
facts = await get_active_facts(user)
context = f"User: {user.custom_name or user.discord_name}\n"
context += "Known facts:\n"
for fact in facts:
context += f"- {fact.fact_content}\n"
return context
```
### Example Context
```
User: Alex
Known facts:
- works as senior engineer at Google
- loves hiking on weekends
- has two cats named Luna and Stella
- prefers dark roast coffee
- speaks English and Japanese
```
---
## Design Considerations
### Why Third Person?
Facts are stored in third person ("loves hiking" not "I love hiking"):
- Easier to inject into prompts
- Consistent format
- Works in any context
### Why Rate Limit?
- Not every message contains facts
- AI API calls are expensive
- Quality over quantity
- Natural learning pace
### Why Deduplication?
- Prevents redundant storage
- Keeps fact list clean
- Reduces noise in prompts
- Respects user privacy (one fact = one entry)
### Privacy Considerations
- Facts can be viewed with `!whatdoyouknow`
- Facts can be deleted with `!forgetme`
- Extraction context is stored (can be audited)
- Source message ID is stored (for reference)

View File

@@ -0,0 +1,338 @@
# Mood System Deep Dive
The mood system gives the bot emotional states that evolve over time and affect how it responds to users.
## Psychological Model
The mood system uses the **Valence-Arousal Model** from affective psychology:
```
High Arousal (+1)
Annoyed │ Excited
● │ ●
Curious │
● │
Low Valence ────────────────┼──────────────── High Valence
(-1) │ (+1)
Bored │ Happy
● │ ●
Calm │
● │
Low Arousal (-1)
```
### Dimensions
**Valence** (-1 to +1)
- Represents the positive/negative quality of the emotional state
- -1 = Very negative (sad, frustrated, upset)
- 0 = Neutral
- +1 = Very positive (happy, joyful, content)
**Arousal** (-1 to +1)
- Represents the energy level or activation
- -1 = Very low energy (calm, sleepy, relaxed)
- 0 = Neutral energy
- +1 = Very high energy (excited, alert, agitated)
---
## Mood Labels
The system classifies the current mood into seven labels:
| Label | Valence | Arousal | Description |
|-------|---------|---------|-------------|
| **Excited** | > 0.3 | > 0.3 | High energy, positive emotions |
| **Happy** | > 0.3 | ≤ 0.3 | Positive but calm contentment |
| **Calm** | -0.3 to 0.3 | < -0.3 | Peaceful, serene state |
| **Neutral** | -0.3 to 0.3 | -0.3 to 0.3 | Baseline, unremarkable state |
| **Bored** | < -0.3 | ≤ 0.3 | Low engagement, understimulated |
| **Annoyed** | < -0.3 | > 0.3 | Frustrated, irritated |
| **Curious** | -0.3 to 0.3 | > 0.3 | Interested, engaged, questioning |
### Classification Logic
```python
def _classify_mood(valence: float, arousal: float) -> MoodLabel:
if valence > 0.3:
return MoodLabel.EXCITED if arousal > 0.3 else MoodLabel.HAPPY
elif valence < -0.3:
return MoodLabel.ANNOYED if arousal > 0.3 else MoodLabel.BORED
else:
if arousal > 0.3:
return MoodLabel.CURIOUS
elif arousal < -0.3:
return MoodLabel.CALM
return MoodLabel.NEUTRAL
```
---
## Mood Intensity
Intensity measures how strong the current mood is:
```python
intensity = (abs(valence) + abs(arousal)) / 2
```
- **0.0 - 0.2**: Very weak, doesn't affect behavior
- **0.2 - 0.5**: Moderate, subtle behavioral changes
- **0.5 - 0.7**: Strong, noticeable behavioral changes
- **0.7 - 1.0**: Very strong, significant behavioral changes
---
## Time Decay
Mood naturally decays toward neutral over time:
```python
hours_since_update = (now - last_update).total_seconds() / 3600
decay_factor = max(0, 1 - (decay_rate * hours_since_update))
current_valence = stored_valence * decay_factor
current_arousal = stored_arousal * decay_factor
```
**Configuration:**
- `MOOD_DECAY_RATE` = 0.1 (default)
- After 10 hours, mood is fully neutral
**Decay Examples:**
| Hours | Decay Factor | Effect |
|-------|--------------|--------|
| 0 | 1.0 | Full mood |
| 2 | 0.8 | 80% of mood remains |
| 5 | 0.5 | 50% of mood remains |
| 10 | 0.0 | Fully neutral |
---
## Mood Updates
When an interaction occurs, mood is updated:
```python
new_valence = current_valence + (sentiment_delta * 0.3)
new_arousal = current_arousal + (engagement_delta * 0.3)
```
### Dampening (Inertia)
Changes are dampened by 70% (only 30% absorption):
- Prevents wild mood swings
- Creates emotional stability
- Makes mood feel more natural
### Update Triggers
| Trigger Type | Sentiment Source | Engagement Source |
|--------------|------------------|-------------------|
| `conversation` | Message sentiment | Message engagement |
| `event` | Event nature | Event importance |
| `time` | Scheduled | Scheduled |
### Input Parameters
**sentiment_delta** (-1 to +1)
- Positive: Happy interactions, compliments, fun conversations
- Negative: Arguments, frustration, rude messages
- Derived from AI analysis or keyword detection
**engagement_delta** (-1 to +1)
- Positive: Long conversations, interesting topics, active engagement
- Negative: Short dismissive messages, ignored responses
- Derived from message length, conversation turns, topic interest
---
## Prompt Modifiers
Based on current mood, the system generates prompt text:
### Excited (High valence, High arousal)
```
You're feeling enthusiastic and energetic right now!
Be expressive, use exclamation marks, show genuine excitement.
```
### Happy (High valence, Low arousal)
```
You're in a good mood - warm, friendly, and content.
Be positive and encouraging in your responses.
```
### Calm (Neutral valence, Low arousal)
```
You're feeling peaceful and relaxed.
Respond thoughtfully and with a serene demeanor.
```
### Neutral
```
(No modifier)
```
### Bored (Low valence, Low arousal)
```
You're feeling a bit understimulated.
Keep responses shorter, maybe try to steer toward more interesting topics.
```
### Annoyed (Low valence, High arousal)
```
You're slightly irritated.
Be a bit more terse, less patient with repetition or vague questions.
```
### Curious (Neutral valence, High arousal)
```
You're feeling inquisitive and engaged!
Ask follow-up questions, show genuine interest in what the user is saying.
```
### Intensity Prefix
For strong moods (intensity > 0.7):
```
[Strong mood] You're feeling enthusiastic...
```
---
## Mood History
All mood changes are recorded in `mood_history` table:
| Field | Description |
|-------|-------------|
| `guild_id` | Guild where mood changed |
| `valence` | New valence value |
| `arousal` | New arousal value |
| `trigger_type` | What caused the change |
| `trigger_user_id` | Who triggered it (if any) |
| `trigger_description` | Description of event |
| `recorded_at` | When change occurred |
This enables:
- Mood trend analysis
- Understanding what affects mood
- Debugging mood issues
- User impact tracking
---
## Bot Statistics
The mood service also tracks global statistics:
| Statistic | Description |
|-----------|-------------|
| `total_messages_sent` | Lifetime message count |
| `total_facts_learned` | Facts extracted from conversations |
| `total_users_known` | Unique users interacted with |
| `first_activated_at` | Bot "birth date" |
Used for self-awareness and the `!botstats` command.
---
## API Reference
### MoodService
```python
class MoodService:
def __init__(self, session: AsyncSession)
async def get_current_mood(
self,
guild_id: int | None = None
) -> MoodState
async def update_mood(
self,
guild_id: int | None,
sentiment_delta: float,
engagement_delta: float,
trigger_type: str,
trigger_user_id: int | None = None,
trigger_description: str | None = None,
) -> MoodState
async def increment_stats(
self,
guild_id: int | None,
messages_sent: int = 0,
facts_learned: int = 0,
users_known: int = 0,
) -> None
async def get_stats(
self,
guild_id: int | None = None
) -> dict
def get_mood_prompt_modifier(
self,
mood: MoodState
) -> str
```
### MoodState
```python
@dataclass
class MoodState:
valence: float # -1 to 1
arousal: float # -1 to 1
label: MoodLabel # Classified label
intensity: float # 0 to 1
```
---
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `MOOD_ENABLED` | `true` | Enable/disable mood system |
| `MOOD_DECAY_RATE` | `0.1` | Decay per hour toward neutral |
---
## Example Usage
```python
from daemon_boyfriend.services.mood_service import MoodService
async with get_session() as session:
mood_service = MoodService(session)
# Get current mood
mood = await mood_service.get_current_mood(guild_id=123)
print(f"Current mood: {mood.label.value} (intensity: {mood.intensity})")
# Update mood after interaction
new_mood = await mood_service.update_mood(
guild_id=123,
sentiment_delta=0.5, # Positive interaction
engagement_delta=0.3, # Moderately engaging
trigger_type="conversation",
trigger_user_id=456,
trigger_description="User shared good news"
)
# Get prompt modifier
modifier = mood_service.get_mood_prompt_modifier(new_mood)
print(f"Prompt modifier: {modifier}")
await session.commit()
```

View 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)

View File

@@ -0,0 +1,417 @@
# Relationship System Deep Dive
The relationship system tracks how well the bot knows each user and adjusts its behavior accordingly.
## Relationship Levels
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Relationship Progression │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 0 ──────── 20 ──────── 40 ──────── 60 ──────── 80 ──────── 100 │
│ │ │ │ │ │ │ │
│ │ Stranger │Acquaintance│ Friend │Good Friend│Close Friend│ │
│ │ │ │ │ │ │ │
│ │ Polite │ Friendly │ Casual │ Personal │Very casual │ │
│ │ Formal │ Reserved │ Warm │ References│Inside jokes│ │
│ │ │ │ │ past │ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Level Details
#### Stranger (Score 0-20)
**Behavior:** Polite, formal, welcoming
```
This is someone you don't know well yet.
Be polite and welcoming, but keep some professional distance.
Use more formal language.
```
- Uses formal language ("Hello", not "Hey!")
- Doesn't assume familiarity
- Introduces itself clearly
- Asks clarifying questions
#### Acquaintance (Score 21-40)
**Behavior:** Friendly, reserved
```
This is someone you've chatted with a few times.
Be friendly and warm, but still somewhat reserved.
```
- More relaxed tone
- Uses the user's name occasionally
- Shows memory of basic facts
- Still maintains some distance
#### Friend (Score 41-60)
**Behavior:** Casual, warm
```
This is a friend! Be casual and warm.
Use their name occasionally, show you remember past conversations.
```
- Natural, conversational tone
- References past conversations
- Shows genuine interest
- Comfortable with casual language
#### Good Friend (Score 61-80)
**Behavior:** Personal, references past
```
This is a good friend you know well.
Be relaxed and personal. Reference things you've talked about before.
Feel free to be playful.
```
- Very comfortable tone
- Recalls shared experiences
- May tease gently
- Shows deeper understanding
#### Close Friend (Score 81-100)
**Behavior:** Very casual, inside jokes
```
This is a close friend! Be very casual and familiar.
Use inside jokes if you have any, be supportive and genuine.
You can tease them gently and be more emotionally open.
```
Additional context for close friends:
- "You have inside jokes together: [jokes]"
- "You sometimes call them: [nickname]"
---
## Score Calculation
### Delta Formula
```python
def _calculate_score_delta(sentiment, message_length, conversation_turns):
# Base change from sentiment (-0.5 to +0.5)
base_delta = sentiment * 0.5
# Bonus for longer messages (up to +0.3)
length_bonus = min(0.3, message_length / 500)
# Bonus for deeper conversations (up to +0.2)
depth_bonus = min(0.2, conversation_turns * 0.05)
# Minimum interaction bonus (+0.1 just for talking)
interaction_bonus = 0.1
total_delta = base_delta + length_bonus + depth_bonus + interaction_bonus
# Clamp to reasonable range
return max(-1.0, min(1.0, total_delta))
```
### Score Components
| Component | Range | Description |
|-----------|-------|-------------|
| **Base (sentiment)** | -0.5 to +0.5 | Positive/negative interaction quality |
| **Length bonus** | 0 to +0.3 | Reward for longer messages |
| **Depth bonus** | 0 to +0.2 | Reward for back-and-forth |
| **Interaction bonus** | +0.1 | Reward just for interacting |
### Example Calculations
**Friendly chat (100 chars, positive sentiment):**
```
base_delta = 0.4 * 0.5 = 0.2
length_bonus = min(0.3, 100/500) = 0.2
depth_bonus = 0.05
interaction_bonus = 0.1
total = 0.55 points
```
**Short dismissive message:**
```
base_delta = -0.3 * 0.5 = -0.15
length_bonus = min(0.3, 20/500) = 0.04
depth_bonus = 0.05
interaction_bonus = 0.1
total = 0.04 points (still positive due to interaction bonus!)
```
**Long, deep, positive conversation:**
```
base_delta = 0.8 * 0.5 = 0.4
length_bonus = min(0.3, 800/500) = 0.3 (capped)
depth_bonus = min(0.2, 5 * 0.05) = 0.2 (capped)
interaction_bonus = 0.1
total = 1.0 point (max)
```
---
## Interaction Tracking
Each interaction records:
| Metric | Description |
|--------|-------------|
| `total_interactions` | Total number of interactions |
| `positive_interactions` | Interactions with sentiment > 0.2 |
| `negative_interactions` | Interactions with sentiment < -0.2 |
| `avg_message_length` | Running average of message lengths |
| `conversation_depth_avg` | Running average of turns per conversation |
| `last_interaction_at` | When they last interacted |
| `first_interaction_at` | When they first interacted |
### Running Average Formula
```python
n = total_interactions
avg_message_length = ((avg_message_length * (n-1)) + new_length) / n
```
---
## Shared References
Close relationships can have shared references:
```python
shared_references = {
"jokes": ["the coffee incident", "404 life not found"],
"nicknames": ["debug buddy", "code wizard"],
"memories": ["that time we debugged for 3 hours"]
}
```
### Adding References
```python
await relationship_service.add_shared_reference(
user=user,
guild_id=123,
reference_type="jokes",
content="the coffee incident"
)
```
**Rules:**
- Maximum 10 references per type
- Oldest are removed when limit exceeded
- Duplicates are ignored
- Only mentioned for Good Friend (61+) and Close Friend (81+)
---
## Prompt Modifiers
The relationship level generates context for the AI:
### Base Modifier
```python
def get_relationship_prompt_modifier(level, relationship):
base = BASE_MODIFIERS[level] # Level-specific text
# Add shared references for close relationships
if level in (GOOD_FRIEND, CLOSE_FRIEND):
if relationship.shared_references.get("jokes"):
base += f" You have inside jokes: {jokes}"
if relationship.shared_references.get("nicknames"):
base += f" You sometimes call them: {nickname}"
return base
```
### Full Example Output
For a Close Friend with shared references:
```
This is a close friend! Be very casual and familiar.
Use inside jokes if you have any, be supportive and genuine.
You can tease them gently and be more emotionally open.
You have inside jokes together: the coffee incident, 404 life not found.
You sometimes call them: debug buddy.
```
---
## Database Schema
### UserRelationship Table
| Column | Type | Description |
|--------|------|-------------|
| `id` | Integer | Primary key |
| `user_id` | Integer | Foreign key to users |
| `guild_id` | BigInteger | Guild ID (nullable for global) |
| `relationship_score` | Float | 0-100 score |
| `total_interactions` | Integer | Total interaction count |
| `positive_interactions` | Integer | Count of positive interactions |
| `negative_interactions` | Integer | Count of negative interactions |
| `avg_message_length` | Float | Average message length |
| `conversation_depth_avg` | Float | Average turns per conversation |
| `shared_references` | JSON | Dictionary of shared references |
| `first_interaction_at` | DateTime | First interaction timestamp |
| `last_interaction_at` | DateTime | Last interaction timestamp |
---
## API Reference
### RelationshipService
```python
class RelationshipService:
def __init__(self, session: AsyncSession)
async def get_or_create_relationship(
self,
user: User,
guild_id: int | None = None
) -> UserRelationship
async def record_interaction(
self,
user: User,
guild_id: int | None,
sentiment: float,
message_length: int,
conversation_turns: int = 1,
) -> RelationshipLevel
def get_level(self, score: float) -> RelationshipLevel
def get_level_display_name(self, level: RelationshipLevel) -> str
async def add_shared_reference(
self,
user: User,
guild_id: int | None,
reference_type: str,
content: str
) -> None
def get_relationship_prompt_modifier(
self,
level: RelationshipLevel,
relationship: UserRelationship
) -> str
async def get_relationship_info(
self,
user: User,
guild_id: int | None = None
) -> dict
```
### RelationshipLevel Enum
```python
class RelationshipLevel(Enum):
STRANGER = "stranger" # 0-20
ACQUAINTANCE = "acquaintance" # 21-40
FRIEND = "friend" # 41-60
GOOD_FRIEND = "good_friend" # 61-80
CLOSE_FRIEND = "close_friend" # 81-100
```
---
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `RELATIONSHIP_ENABLED` | `true` | Enable/disable relationship tracking |
---
## Example Usage
```python
from daemon_boyfriend.services.relationship_service import RelationshipService
async with get_session() as session:
rel_service = RelationshipService(session)
# Record an interaction
level = await rel_service.record_interaction(
user=user,
guild_id=123,
sentiment=0.5, # Positive interaction
message_length=150,
conversation_turns=3
)
print(f"Current level: {rel_service.get_level_display_name(level)}")
# Get full relationship info
info = await rel_service.get_relationship_info(user, guild_id=123)
print(f"Score: {info['score']}")
print(f"Total interactions: {info['total_interactions']}")
print(f"Days known: {info['days_known']}")
# Add a shared reference (for close friends)
await rel_service.add_shared_reference(
user=user,
guild_id=123,
reference_type="jokes",
content="the infinite loop joke"
)
# Get prompt modifier
relationship = await rel_service.get_or_create_relationship(user, 123)
modifier = rel_service.get_relationship_prompt_modifier(level, relationship)
print(f"Prompt modifier:\n{modifier}")
await session.commit()
```
---
## The !relationship Command
Users can check their relationship status:
```
User: !relationship
Bot: 📊 **Your Relationship with Me**
Level: **Friend** 🤝
Score: 52/100
We've had 47 conversations together!
- Positive vibes: 38 times
- Rough patches: 3 times
We've known each other for 23 days.
```
---
## Design Considerations
### Why Interaction Bonus?
Even negative interactions build relationship:
- Familiarity increases even through conflict
- Prevents relationships from degrading to zero
- Reflects real-world relationship dynamics
### Why Dampen Score Changes?
- Prevents gaming the system with spam
- Makes progression feel natural
- Single interactions have limited impact
- Requires sustained engagement to reach higher levels
### Guild-Specific vs Global
- Relationships are tracked per-guild by default
- Allows different relationship levels in different servers
- Set `guild_id=None` for global relationship tracking