refactor: Transform daemon_boyfriend into Loyal Companion

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.
This commit is contained in:
2026-01-14 18:08:35 +01:00
parent 3d939201f0
commit dbd534d860
60 changed files with 310 additions and 381 deletions

View File

@@ -11,8 +11,8 @@ from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from daemon_boyfriend.config import Settings
from daemon_boyfriend.models.base import Base
from loyal_companion.config import Settings
from loyal_companion.models.base import Base
# --- Event Loop Fixture ---
@@ -140,7 +140,7 @@ def mock_discord_bot() -> MagicMock:
@pytest.fixture
def mock_ai_response() -> MagicMock:
"""Create a mock AI response."""
from daemon_boyfriend.services.providers.base import AIResponse
from loyal_companion.services.providers.base import AIResponse
return AIResponse(
content="This is a test response from the AI.",
@@ -207,7 +207,7 @@ def mock_gemini_client() -> MagicMock:
@pytest_asyncio.fixture
async def sample_user(db_session: AsyncSession):
"""Create a sample user in the database."""
from daemon_boyfriend.models import User
from loyal_companion.models import User
user = User(
discord_id=123456789,
@@ -223,7 +223,7 @@ async def sample_user(db_session: AsyncSession):
@pytest_asyncio.fixture
async def sample_user_with_facts(db_session: AsyncSession, sample_user):
"""Create a sample user with facts."""
from daemon_boyfriend.models import UserFact
from loyal_companion.models import UserFact
facts = [
UserFact(
@@ -252,7 +252,7 @@ async def sample_user_with_facts(db_session: AsyncSession, sample_user):
@pytest_asyncio.fixture
async def sample_conversation(db_session: AsyncSession, sample_user):
"""Create a sample conversation."""
from daemon_boyfriend.models import Conversation
from loyal_companion.models import Conversation
conversation = Conversation(
user_id=sample_user.id,
@@ -268,7 +268,7 @@ async def sample_conversation(db_session: AsyncSession, sample_user):
@pytest_asyncio.fixture
async def sample_bot_state(db_session: AsyncSession):
"""Create a sample bot state."""
from daemon_boyfriend.models import BotState
from loyal_companion.models import BotState
bot_state = BotState(
guild_id=111222333,
@@ -284,7 +284,7 @@ async def sample_bot_state(db_session: AsyncSession):
@pytest_asyncio.fixture
async def sample_user_relationship(db_session: AsyncSession, sample_user):
"""Create a sample user relationship."""
from daemon_boyfriend.models import UserRelationship
from loyal_companion.models import UserRelationship
relationship = UserRelationship(
user_id=sample_user.id,

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone
import pytest
from daemon_boyfriend.models import (
from loyal_companion.models import (
BotOpinion,
BotState,
Conversation,
@@ -20,7 +20,7 @@ from daemon_boyfriend.models import (
UserPreference,
UserRelationship,
)
from daemon_boyfriend.models.base import utc_now
from loyal_companion.models.base import utc_now
class TestUtcNow:

View File

@@ -4,16 +4,16 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from daemon_boyfriend.services.providers.anthropic import AnthropicProvider
from daemon_boyfriend.services.providers.base import (
from loyal_companion.services.providers.anthropic import AnthropicProvider
from loyal_companion.services.providers.base import (
AIProvider,
AIResponse,
ImageAttachment,
Message,
)
from daemon_boyfriend.services.providers.gemini import GeminiProvider
from daemon_boyfriend.services.providers.openai import OpenAIProvider
from daemon_boyfriend.services.providers.openrouter import OpenRouterProvider
from loyal_companion.services.providers.gemini import GeminiProvider
from loyal_companion.services.providers.openai import OpenAIProvider
from loyal_companion.services.providers.openrouter import OpenRouterProvider
class TestMessage:
@@ -69,7 +69,7 @@ class TestOpenAIProvider:
@pytest.fixture
def provider(self, mock_openai_client):
"""Create an OpenAI provider with mocked client."""
with patch("daemon_boyfriend.services.providers.openai.AsyncOpenAI") as mock_class:
with patch("loyal_companion.services.providers.openai.AsyncOpenAI") as mock_class:
mock_class.return_value = mock_openai_client
provider = OpenAIProvider(api_key="test_key", model="gpt-4o-mini")
provider.client = mock_openai_client
@@ -143,7 +143,7 @@ class TestAnthropicProvider:
def provider(self, mock_anthropic_client):
"""Create an Anthropic provider with mocked client."""
with patch(
"daemon_boyfriend.services.providers.anthropic.anthropic.AsyncAnthropic"
"loyal_companion.services.providers.anthropic.anthropic.AsyncAnthropic"
) as mock_class:
mock_class.return_value = mock_anthropic_client
provider = AnthropicProvider(api_key="test_key", model="claude-sonnet-4-20250514")
@@ -200,7 +200,7 @@ class TestGeminiProvider:
@pytest.fixture
def provider(self, mock_gemini_client):
"""Create a Gemini provider with mocked client."""
with patch("daemon_boyfriend.services.providers.gemini.genai.Client") as mock_class:
with patch("loyal_companion.services.providers.gemini.genai.Client") as mock_class:
mock_class.return_value = mock_gemini_client
provider = GeminiProvider(api_key="test_key", model="gemini-2.0-flash")
provider.client = mock_gemini_client
@@ -248,7 +248,7 @@ class TestOpenRouterProvider:
@pytest.fixture
def provider(self, mock_openai_client):
"""Create an OpenRouter provider with mocked client."""
with patch("daemon_boyfriend.services.providers.openrouter.AsyncOpenAI") as mock_class:
with patch("loyal_companion.services.providers.openrouter.AsyncOpenAI") as mock_class:
mock_class.return_value = mock_openai_client
provider = OpenRouterProvider(api_key="test_key", model="openai/gpt-4o")
provider.client = mock_openai_client

View File

@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from daemon_boyfriend.models import (
from loyal_companion.models import (
BotOpinion,
BotState,
Conversation,
@@ -14,14 +14,14 @@ from daemon_boyfriend.models import (
UserFact,
UserRelationship,
)
from daemon_boyfriend.services.ai_service import AIService
from daemon_boyfriend.services.fact_extraction_service import FactExtractionService
from daemon_boyfriend.services.mood_service import MoodLabel, MoodService, MoodState
from daemon_boyfriend.services.opinion_service import OpinionService, extract_topics_from_message
from daemon_boyfriend.services.persistent_conversation import PersistentConversationManager
from daemon_boyfriend.services.relationship_service import RelationshipLevel, RelationshipService
from daemon_boyfriend.services.self_awareness_service import SelfAwarenessService
from daemon_boyfriend.services.user_service import UserService
from loyal_companion.services.ai_service import AIService
from loyal_companion.services.fact_extraction_service import FactExtractionService
from loyal_companion.services.mood_service import MoodLabel, MoodService, MoodState
from loyal_companion.services.opinion_service import OpinionService, extract_topics_from_message
from loyal_companion.services.persistent_conversation import PersistentConversationManager
from loyal_companion.services.relationship_service import RelationshipLevel, RelationshipService
from loyal_companion.services.self_awareness_service import SelfAwarenessService
from loyal_companion.services.user_service import UserService
class TestUserService:
@@ -577,8 +577,8 @@ class TestAIService:
def test_get_system_prompt_default(self, mock_settings):
"""Test getting default system prompt."""
with patch("daemon_boyfriend.services.ai_service.settings", mock_settings):
with patch("daemon_boyfriend.services.ai_service.AIService._init_provider"):
with patch("loyal_companion.services.ai_service.settings", mock_settings):
with patch("loyal_companion.services.ai_service.AIService._init_provider"):
service = AIService(mock_settings)
service._provider = MagicMock()
@@ -590,8 +590,8 @@ class TestAIService:
def test_get_system_prompt_custom(self, mock_settings):
"""Test getting custom system prompt."""
mock_settings.system_prompt = "Custom prompt"
with patch("daemon_boyfriend.services.ai_service.settings", mock_settings):
with patch("daemon_boyfriend.services.ai_service.AIService._init_provider"):
with patch("loyal_companion.services.ai_service.settings", mock_settings):
with patch("loyal_companion.services.ai_service.AIService._init_provider"):
service = AIService(mock_settings)
service._provider = MagicMock()
@@ -601,8 +601,8 @@ class TestAIService:
def test_provider_name(self, mock_settings):
"""Test getting provider name."""
with patch("daemon_boyfriend.services.ai_service.settings", mock_settings):
with patch("daemon_boyfriend.services.ai_service.AIService._init_provider"):
with patch("loyal_companion.services.ai_service.settings", mock_settings):
with patch("loyal_companion.services.ai_service.AIService._init_provider"):
service = AIService(mock_settings)
mock_provider = MagicMock()
mock_provider.provider_name = "openai"
@@ -612,8 +612,8 @@ class TestAIService:
def test_model_property(self, mock_settings):
"""Test getting model name."""
with patch("daemon_boyfriend.services.ai_service.settings", mock_settings):
with patch("daemon_boyfriend.services.ai_service.AIService._init_provider"):
with patch("loyal_companion.services.ai_service.settings", mock_settings):
with patch("loyal_companion.services.ai_service.AIService._init_provider"):
service = AIService(mock_settings)
service._provider = MagicMock()