added technical documentation
This commit is contained in:
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