Rebrand and personalize the bot as 'Bartender' - a companion for those who love deeply and feel intensely. Major changes: - Rename package: daemon_boyfriend -> loyal_companion - New default personality: Bartender - wise, steady, non-judgmental - Grief-aware system prompt (no toxic positivity, attachment-informed) - New relationship levels: New Face -> Close Friend progression - Bartender-style mood modifiers (steady presence) - New fact types: attachment_pattern, grief_context, coping_mechanism - Lower mood decay (0.05) for emotional stability - Higher fact extraction rate (0.4) - Bartender pays attention Updated all imports, configs, Docker files, and documentation.
418 lines
12 KiB
Markdown
418 lines
12 KiB
Markdown
# 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 loyal_companion.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
|