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.
12 KiB
12 KiB
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
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
n = total_interactions
avg_message_length = ((avg_message_length * (n-1)) + new_length) / n
Shared References
Close relationships can have shared references:
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
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
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
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
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
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=Nonefor global relationship tracking