added technical documentation
This commit is contained in:
301
docs/living-ai/README.md
Normal file
301
docs/living-ai/README.md
Normal 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)
|
||||
441
docs/living-ai/fact-extraction.md
Normal file
441
docs/living-ai/fact-extraction.md
Normal 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)
|
||||
338
docs/living-ai/mood-system.md
Normal file
338
docs/living-ai/mood-system.md
Normal 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()
|
||||
```
|
||||
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)
|
||||
417
docs/living-ai/relationship-system.md
Normal file
417
docs/living-ai/relationship-system.md
Normal 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
|
||||
Reference in New Issue
Block a user