# 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