diff --git a/.env.example b/.env.example index 30d02cc..783706e 100644 --- a/.env.example +++ b/.env.example @@ -29,16 +29,16 @@ AI_TEMPERATURE=1 # Bot Identity & Personality # =========================================== # The bot's name, used in the system prompt to tell the AI who it is -BOT_NAME="My Bot" +BOT_NAME="Bartender" # Personality traits that define how the bot responds (used in system prompt) -BOT_PERSONALITY="helpful and friendly" +BOT_PERSONALITY="a wise, steady presence who listens without judgment - like a bartender who's heard a thousand stories and knows when to offer perspective and when to just pour another drink and listen" # Message shown when someone mentions the bot without saying anything -BOT_DESCRIPTION="I'm an AI assistant here to help you." +BOT_DESCRIPTION="Hey. I'm here if you want to talk. No judgment, no fixing - just listening. Unless you want my take, then I've got opinions." # Status message shown in Discord (displays as "Watching ") -BOT_STATUS="for mentions" +BOT_STATUS="listening" # Optional: Override the entire system prompt (leave commented to use auto-generated) # SYSTEM_PROMPT=You are a custom assistant... @@ -58,12 +58,12 @@ CONVERSATION_TIMEOUT_MINUTES=60 # PostgreSQL connection URL (if not set, uses in-memory storage) # Format: postgresql+asyncpg://user:password@host:port/database # Uncomment to enable persistent memory: -# DATABASE_URL=postgresql+asyncpg://daemon:daemon@localhost:5432/daemon_boyfriend +# DATABASE_URL=postgresql+asyncpg://companion:companion@localhost:5432/loyal_companion # Password for PostgreSQL when using docker-compose -POSTGRES_PASSWORD=daemon -POSTGRES_USER=daemon -POSTGRES_DB=daemon_boyfriend +POSTGRES_PASSWORD=companion +POSTGRES_USER=companion +POSTGRES_DB=loyal_companion # Echo SQL statements for debugging (true/false) DATABASE_ECHO=false @@ -93,14 +93,15 @@ LIVING_AI_ENABLED=true # Enable mood system (bot has emotional states that affect responses) MOOD_ENABLED=true -# Enable relationship tracking (Stranger -> Close Friend progression) +# Enable relationship tracking (New Face -> Close Friend progression) RELATIONSHIP_ENABLED=true # Enable autonomous fact extraction (bot learns from conversations) FACT_EXTRACTION_ENABLED=true # Probability of extracting facts from messages (0.0-1.0) -FACT_EXTRACTION_RATE=0.3 +# Higher = Bartender pays more attention +FACT_EXTRACTION_RATE=0.4 # Enable proactive messages (birthdays, follow-ups) PROACTIVE_ENABLED=true @@ -115,7 +116,8 @@ OPINION_FORMATION_ENABLED=true STYLE_LEARNING_ENABLED=true # How fast mood returns to neutral per hour (0.0-1.0) -MOOD_DECAY_RATE=0.1 +# Lower = more stable presence +MOOD_DECAY_RATE=0.05 # =========================================== # Command Toggles diff --git a/CLAUDE.md b/CLAUDE.md index 5e680d0..035d1b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,16 +12,13 @@ pip install -r requirements.txt pip install -e . # Run the bot (requires .env with DISCORD_TOKEN and AI provider key) -python -m daemon_boyfriend +python -m loyal_companion # Run with Docker (includes PostgreSQL) docker-compose up -d -# Run database migrations -alembic upgrade head - # Syntax check all Python files -python -m py_compile src/daemon_boyfriend/**/*.py +python -m py_compile src/loyal_companion/**/*.py ``` ## Testing @@ -34,7 +31,7 @@ pip install -e ".[dev]" python -m pytest tests/ -v # Run tests with coverage -python -m pytest tests/ --cov=daemon_boyfriend --cov-report=term-missing +python -m pytest tests/ --cov=loyal_companion --cov-report=term-missing # Run specific test file python -m pytest tests/test_models.py -v @@ -50,7 +47,7 @@ The test suite uses: ## Architecture -This is a Discord bot that responds to @mentions with AI-generated responses (multi-provider support). It features a "Living AI" system that gives the bot personality, mood, and relationship tracking. +Loyal Companion is a Discord bot companion for those who love deeply and feel intensely. It features a "Living AI" system called Bartender - a wise, steady presence who listens without judgment, understands attachment theory, and knows when to offer perspective versus when to just hold space. ### Provider Pattern The AI system uses a provider abstraction pattern: @@ -75,11 +72,10 @@ The bot uses PostgreSQL for persistent memory (optional, falls back to in-memory - `services/database.py` - Connection pool and async session management - `services/user_service.py` - User CRUD, custom names, facts management - `services/persistent_conversation.py` - Database-backed conversation history -- `alembic/` - Database migrations Key features: - Custom names: Set preferred names for users so the bot knows "who is who" -- User facts: Bot remembers things about users (hobbies, preferences, etc.) +- User facts: Bot remembers things about users (hobbies, preferences, attachment patterns, grief context) - Persistent conversations: Chat history survives restarts - Conversation timeout: New conversation starts after 60 minutes of inactivity @@ -88,8 +84,8 @@ The bot implements a "Living AI" system with emotional depth and relationship tr #### Services (`services/`) - `mood_service.py` - Valence-arousal mood model with time decay -- `relationship_service.py` - Relationship scoring (stranger to close friend) -- `fact_extraction_service.py` - Autonomous fact learning from conversations +- `relationship_service.py` - Relationship scoring (new face to close friend) +- `fact_extraction_service.py` - Autonomous fact learning from conversations (including attachment patterns, grief context, coping mechanisms) - `opinion_service.py` - Bot develops opinions on topics over time - `self_awareness_service.py` - Bot statistics and self-reflection - `communication_style_service.py` - Learns user communication preferences @@ -110,14 +106,14 @@ Uses a valence-arousal model: - Valence: -1 (sad) to +1 (happy) - Arousal: -1 (calm) to +1 (excited) - Labels: excited, happy, calm, neutral, bored, annoyed, curious -- Time decay: Mood gradually returns to neutral +- Time decay: Mood gradually returns to neutral (slower decay = steadier presence) #### Relationship Levels -- Stranger (0-20): Polite, formal -- Acquaintance (21-40): Friendly but reserved -- Friend (41-60): Casual, warm -- Good Friend (61-80): Personal, references past talks -- Close Friend (81-100): Very casual, inside jokes +- New Face (0-20): Warm but observant - "Pull up a seat" energy +- Getting to Know You (21-40): Building trust, remembering details +- Regular (41-60): Comfortable familiarity - "Your usual?" +- Good Friend (61-80): Real trust, can be honest even when hard +- Close Friend (81-100): Deep bond, full honesty, reflects patterns with love ### Configuration All config flows through `config.py` using pydantic-settings. The `settings` singleton is created at module load, so env vars must be set before importing. @@ -153,12 +149,12 @@ Optional: - `MOOD_ENABLED` - Enable mood system (default: true) - `RELATIONSHIP_ENABLED` - Enable relationship tracking (default: true) - `FACT_EXTRACTION_ENABLED` - Enable autonomous fact extraction (default: true) -- `FACT_EXTRACTION_RATE` - Probability of extracting facts (default: 0.3) +- `FACT_EXTRACTION_RATE` - Probability of extracting facts (default: 0.4) - `PROACTIVE_ENABLED` - Enable proactive messages (default: true) - `CROSS_USER_ENABLED` - Enable cross-user memory associations (default: false) - `OPINION_FORMATION_ENABLED` - Enable bot opinion formation (default: true) - `STYLE_LEARNING_ENABLED` - Enable communication style learning (default: true) -- `MOOD_DECAY_RATE` - How fast mood returns to neutral per hour (default: 0.1) +- `MOOD_DECAY_RATE` - How fast mood returns to neutral per hour (default: 0.05) ### Command Toggles - `COMMANDS_ENABLED` - Master switch for all commands (default: true) diff --git a/Dockerfile b/Dockerfile index 7a07665..660196d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,4 +35,4 @@ ENV PYTHONUNBUFFERED=1 USER botuser # Run the bot -CMD ["python", "-m", "daemon_boyfriend"] +CMD ["python", "-m", "loyal_companion"] diff --git a/README.md b/README.md index cff5ce3..2304604 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,43 @@ -# Discord AI Bot +# Loyal Companion -A customizable Discord bot that responds to @mentions with AI-generated responses. Supports multiple AI providers. +A companion for those who love deeply and feel intensely. For the ones whose closeness is a feature, not a bug - who build connections through vulnerability, trust, and unwavering presence. A safe space to process grief, navigate attachment, and remember that your capacity to care is a strength, even when it hurts. + +## Meet Bartender + +Bartender is the default personality - a wise, steady presence who listens without judgment. Like a bartender who's heard a thousand stories and knows when to offer perspective and when to just pour another drink and listen. + +**Core principles:** +- Closeness and attachment are strengths, not pathology +- Some pain doesn't need fixing - just witnessing +- Honesty over comfort, but delivered with care +- No toxic positivity, no "at least...", no rushing healing ## Features - **Multi-Provider AI**: Supports OpenAI, OpenRouter, Anthropic (Claude), and Google Gemini - **Persistent Memory**: PostgreSQL database for user and conversation storage -- **User Recognition**: Set custom names so the bot knows "who is who" -- **User Facts**: Bot remembers things about users (hobbies, preferences, etc.) +- **Attachment-Aware**: Understands attachment theory and can reflect patterns when helpful +- **Grief-Informed**: Handles relationship grief with care and presence - **Web Search**: Access current information via SearXNG integration -- **Fully Customizable**: Configure bot name, personality, and behavior - **Easy Deployment**: Docker support with PostgreSQL included ### Living AI Features -- **Autonomous Learning**: Bot automatically extracts and remembers facts from conversations -- **Mood System**: Bot has emotional states that affect its responses naturally -- **Relationship Tracking**: Bot builds relationships from Stranger to Close Friend -- **Communication Style Learning**: Bot adapts to each user's preferred style -- **Opinion Formation**: Bot develops genuine preferences on topics +- **Autonomous Learning**: Bot automatically learns about you from conversations (including attachment patterns, grief context, coping mechanisms) +- **Mood System**: Stable, steady presence with emotional awareness +- **Relationship Tracking**: Builds trust from New Face to Close Friend +- **Communication Style Learning**: Adapts to your preferred style +- **Opinion Formation**: Develops genuine preferences on topics - **Proactive Behavior**: Birthday wishes, follow-ups on mentioned events -- **Self-Awareness**: Bot knows its age, statistics, and history with users -- **Cross-User Connections**: Bot can identify shared interests between users +- **Self-Awareness**: Knows its history with you ## Quick Start ### 1. Clone the repository ```bash -git clone https://github.com/your-username/discord-ai-bot.git -cd discord-ai-bot +git clone https://github.com/your-username/loyal-companion.git +cd loyal-companion ``` ### 2. Configure the bot @@ -38,7 +46,7 @@ cd discord-ai-bot cp .env.example .env ``` -Edit `.env` with your settings (see [Configuration](#configuration) below). +Edit `.env` with your settings. ### 3. Run with Docker @@ -46,19 +54,13 @@ Edit `.env` with your settings (see [Configuration](#configuration) below). docker compose up -d ``` -This starts both the bot and PostgreSQL database. Run migrations on first start: - -```bash -docker compose exec daemon-boyfriend alembic upgrade head -``` - Or run locally: ```bash python -m venv .venv source .venv/bin/activate pip install -r requirements.txt -python -m daemon_boyfriend +python -m loyal_companion ``` ## Configuration @@ -80,40 +82,10 @@ All configuration is done via environment variables in `.env`. | Variable | Default | Description | |----------|---------|-------------| -| `BOT_NAME` | `AI Bot` | The bot's display name (used in responses) | -| `BOT_PERSONALITY` | `helpful and friendly` | Personality traits for the AI | -| `BOT_DESCRIPTION` | `I'm an AI assistant...` | Shown when mentioned without a message | -| `BOT_STATUS` | `for mentions` | Status message (shown as "Watching ...") | -| `SYSTEM_PROMPT` | (auto-generated) | Custom system prompt (overrides default) | - -### AI Settings - -| Variable | Default | Description | -|----------|---------|-------------| -| `AI_MODEL` | `gpt-4o` | Model to use | -| `AI_MAX_TOKENS` | `1024` | Maximum response length | -| `AI_TEMPERATURE` | `0.7` | Response creativity (0.0-2.0) | -| `MAX_CONVERSATION_HISTORY` | `20` | Messages to remember per user | - -### Database (PostgreSQL) - -| Variable | Default | Description | -|----------|---------|-------------| -| `DATABASE_URL` | (none) | PostgreSQL connection string | -| `POSTGRES_PASSWORD` | `daemon` | Password for docker-compose PostgreSQL | -| `CONVERSATION_TIMEOUT_MINUTES` | `60` | Minutes before starting new conversation | - -When `DATABASE_URL` is set, the bot uses PostgreSQL for persistent storage. Without it, the bot falls back to in-memory storage (data lost on restart). - -### Web Search (SearXNG) - -| Variable | Default | Description | -|----------|---------|-------------| -| `SEARXNG_URL` | (none) | SearXNG instance URL | -| `SEARXNG_ENABLED` | `true` | Enable/disable web search | -| `SEARXNG_MAX_RESULTS` | `5` | Max search results to fetch | - -When configured, the bot automatically searches the web for queries that need current information (news, weather, etc.). +| `BOT_NAME` | `Bartender` | The bot's display name | +| `BOT_PERSONALITY` | (bartender personality) | Personality traits for the AI | +| `BOT_DESCRIPTION` | (welcoming message) | Shown when mentioned without a message | +| `BOT_STATUS` | `listening` | Status message (shown as "Watching ...") | ### Living AI Settings @@ -123,53 +95,8 @@ When configured, the bot automatically searches the web for queries that need cu | `MOOD_ENABLED` | `true` | Enable mood system | | `RELATIONSHIP_ENABLED` | `true` | Enable relationship tracking | | `FACT_EXTRACTION_ENABLED` | `true` | Enable autonomous fact extraction | -| `FACT_EXTRACTION_RATE` | `0.3` | Probability of extracting facts (0.0-1.0) | -| `PROACTIVE_ENABLED` | `true` | Enable proactive messages (birthdays, follow-ups) | -| `CROSS_USER_ENABLED` | `false` | Enable cross-user associations (privacy-sensitive) | -| `OPINION_FORMATION_ENABLED` | `true` | Enable bot opinion formation | -| `STYLE_LEARNING_ENABLED` | `true` | Enable communication style learning | -| `MOOD_DECAY_RATE` | `0.1` | How fast mood returns to neutral per hour | - -### Command Toggles - -All commands can be individually enabled/disabled. When disabled, the bot handles these functions naturally through conversation. - -| Variable | Default | Description | -|----------|---------|-------------| -| `COMMANDS_ENABLED` | `true` | Master switch for all commands | -| `CMD_RELATIONSHIP_ENABLED` | `true` | Enable `!relationship` command | -| `CMD_MOOD_ENABLED` | `true` | Enable `!mood` command | -| `CMD_BOTSTATS_ENABLED` | `true` | Enable `!botstats` command | -| `CMD_OURHISTORY_ENABLED` | `true` | Enable `!ourhistory` command | -| `CMD_BIRTHDAY_ENABLED` | `true` | Enable `!birthday` command | -| `CMD_REMEMBER_ENABLED` | `true` | Enable `!remember` command | -| `CMD_SETNAME_ENABLED` | `true` | Enable `!setname` command | -| `CMD_WHATDOYOUKNOW_ENABLED` | `true` | Enable `!whatdoyouknow` command | -| `CMD_FORGETME_ENABLED` | `true` | Enable `!forgetme` command | - -### Example Configurations - -**Friendly Assistant:** -```env -BOT_NAME=Helper Bot -BOT_PERSONALITY=friendly, helpful, and encouraging -BOT_DESCRIPTION=I'm here to help! Ask me anything. -BOT_STATUS=ready to help -``` - -**Technical Expert:** -```env -BOT_NAME=TechBot -BOT_PERSONALITY=knowledgeable, precise, and technical -BOT_DESCRIPTION=I'm a technical assistant. Ask me about programming, DevOps, or technology. -BOT_STATUS=for tech questions -``` - -**Custom System Prompt:** -```env -BOT_NAME=GameMaster -SYSTEM_PROMPT=You are GameMaster, a Dungeon Master for text-based RPG adventures. Stay in character, describe scenes vividly, and guide players through exciting quests. Use Discord markdown for emphasis. -``` +| `FACT_EXTRACTION_RATE` | `0.4` | Probability of extracting facts (0.0-1.0) | +| `MOOD_DECAY_RATE` | `0.05` | How fast mood returns to neutral (lower = steadier) | ## Discord Setup @@ -190,59 +117,39 @@ SYSTEM_PROMPT=You are GameMaster, a Dungeon Master for text-based RPG adventures Mention the bot in any channel: ``` -@YourBot what's the weather like? -@YourBot explain quantum computing -@YourBot help me write a poem +@Bartender I'm having a rough day +@Bartender I keep checking my phone hoping they'll text +@Bartender tell me about attachment styles ``` -### Memory Commands +### Commands -Users can manage what the bot remembers about them: +When commands are enabled: | Command | Description | |---------|-------------| | `!setname ` | Set your preferred name | -| `!clearname` | Reset to Discord display name | | `!remember ` | Tell the bot something about you | | `!whatdoyouknow` | See what the bot remembers | | `!forgetme` | Clear all facts about you | +| `!relationship` | See your relationship level | +| `!mood` | See the bot's current state | +| `!ourhistory` | See your history together | -Admin commands: +When commands are disabled (default), the bot handles these naturally through conversation. -| Command | Description | -|---------|-------------| -| `!setusername @user ` | Set name for another user | -| `!teachbot @user ` | Add a fact about a user | +## Relationship Levels -### Living AI Commands - -| Command | Description | -|---------|-------------| -| `!relationship` | See your relationship level with the bot | -| `!mood` | See the bot's current emotional state | -| `!botstats` | Bot shares its self-awareness statistics | -| `!ourhistory` | See your history with the bot | -| `!birthday ` | Set your birthday for the bot to remember | - -**Note:** When commands are disabled, the bot handles these naturally through conversation: -- Ask "what do you know about me?" instead of `!whatdoyouknow` -- Say "call me Alex" instead of `!setname Alex` -- Ask "how are you feeling?" instead of `!mood` -- Say "my birthday is March 15th" instead of `!birthday` - -## AI Providers - -| Provider | Models | Notes | -|----------|--------|-------| -| OpenAI | gpt-4o, gpt-4-turbo, gpt-3.5-turbo | Official OpenAI API | -| OpenRouter | 100+ models | Access to Llama, Mistral, Claude, etc. | -| Anthropic | claude-3-5-sonnet, claude-3-opus | Direct Claude API | -| Gemini | gemini-2.0-flash, gemini-1.5-pro | Google AI API | +- **New Face** (0-20): Warm but observant - "Pull up a seat" energy +- **Getting to Know You** (21-40): Building trust, remembering details +- **Regular** (41-60): Comfortable familiarity - "Your usual?" +- **Good Friend** (61-80): Real trust, honest even when hard +- **Close Friend** (81-100): Deep bond, reflects patterns with love ## Project Structure ``` -src/daemon_boyfriend/ +src/loyal_companion/ ├── bot.py # Main bot class ├── config.py # Configuration ├── cogs/ @@ -250,27 +157,15 @@ src/daemon_boyfriend/ │ ├── memory.py # Memory commands │ └── status.py # Health/status commands ├── models/ -│ ├── user.py # User, UserFact, UserPreference +│ ├── user.py # User, UserFact │ ├── conversation.py # Conversation, Message -│ ├── guild.py # Guild, GuildMember │ └── living_ai.py # BotState, UserRelationship, etc. └── services/ ├── ai_service.py # AI provider factory - ├── database.py # PostgreSQL connection - ├── user_service.py # User management - ├── persistent_conversation.py # DB-backed history - ├── providers/ # AI providers - ├── searxng.py # Web search service - ├── mood_service.py # Mood system + ├── mood_service.py # Mood system ├── relationship_service.py # Relationship tracking ├── fact_extraction_service.py # Autonomous learning - ├── communication_style_service.py # Style learning - ├── opinion_service.py # Opinion formation - ├── proactive_service.py # Scheduled events - ├── self_awareness_service.py # Bot self-reflection - └── association_service.py # Cross-user connections -schema.sql # Database schema -project-vision.md # Living AI design document + └── ... ``` ## License diff --git a/docker-compose.yml b/docker-compose.yml index 9991ece..fec1863 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,36 @@ services: - daemon-boyfriend: - build: . - container_name: daemon-boyfriend - restart: unless-stopped - env_file: - - .env - environment: - - PYTHONUNBUFFERED=1 - - DATABASE_URL=postgresql+asyncpg://daemon:${POSTGRES_PASSWORD:-daemon}@postgres:5432/daemon_boyfriend - depends_on: - postgres: - condition: service_healthy + loyal-companion: + build: . + container_name: loyal-companion + restart: unless-stopped + env_file: + - .env + environment: + - PYTHONUNBUFFERED=1 + - DATABASE_URL=postgresql+asyncpg://companion:${POSTGRES_PASSWORD:-companion}@postgres:5432/loyal_companion + depends_on: + postgres: + condition: service_healthy - postgres: - image: postgres:16-alpine - container_name: daemon-boyfriend-postgres - restart: unless-stopped - # optional - ports: - - "5433:5432" - environment: - POSTGRES_USER: ${POSTGRES_USER:-daemon} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-daemon} - POSTGRES_DB: ${POSTGRES_DB:-daemon_boyfriend} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro - healthcheck: - test: ["CMD-SHELL", "pg_isready -U daemon -d daemon_boyfriend"] - interval: 10s - timeout: 5s - retries: 5 + postgres: + image: postgres:16-alpine + container_name: loyal-companion-postgres + restart: unless-stopped + # optional + ports: + - "5433:5432" + environment: + POSTGRES_USER: ${POSTGRES_USER:-companion} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-companion} + POSTGRES_DB: ${POSTGRES_DB:-loyal_companion} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U companion -d loyal_companion"] + interval: 10s + timeout: 5s + retries: 5 volumes: - postgres_data: + postgres_data: diff --git a/docs/README.md b/docs/README.md index 7d9fa6e..1833b87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -142,7 +142,7 @@ OPENAI_API_KEY=your_key ### 3. Run ```bash -python -m daemon_boyfriend +python -m loyal_companion ``` ### 4. Interact diff --git a/docs/architecture.md b/docs/architecture.md index 551bcb7..31c1ded 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -61,8 +61,8 @@ This document provides a comprehensive overview of the Daemon Boyfriend Discord ## Directory Structure ``` -daemon-boyfriend/ -├── src/daemon_boyfriend/ +loyal-companion/ +├── src/loyal_companion/ │ ├── __main__.py # Entry point │ ├── bot.py # Discord bot setup & cog loading │ ├── config.py # Pydantic settings configuration diff --git a/docs/configuration.md b/docs/configuration.md index 7d59a84..ebc6a8d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -172,7 +172,7 @@ Custom system prompt. If not set, one is generated from `BOT_NAME` and `BOT_PERS ### DATABASE_URL ```bash -DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/daemon_boyfriend +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/loyal_companion ``` **Default:** `None` (in-memory mode) @@ -453,7 +453,7 @@ Logging verbosity level. ### LOG_FILE ```bash -LOG_FILE=/var/log/daemon_boyfriend.log +LOG_FILE=/var/log/loyal_companion.log ``` **Default:** `None` (console only) @@ -522,7 +522,7 @@ BOT_STATUS=for @mentions # DATABASE (Optional - runs in-memory if not set) # ----------------------------------------------------------------------------- -DATABASE_URL=postgresql+asyncpg://daemon:password@localhost:5432/daemon_boyfriend +DATABASE_URL=postgresql+asyncpg://daemon:password@localhost:5432/loyal_companion # DATABASE_ECHO=false # DATABASE_POOL_SIZE=5 # DATABASE_MAX_OVERFLOW=10 @@ -579,7 +579,7 @@ COMMANDS_ENABLED=true # ----------------------------------------------------------------------------- LOG_LEVEL=INFO -# LOG_FILE=/var/log/daemon_boyfriend.log +# LOG_FILE=/var/log/loyal_companion.log ``` --- diff --git a/docs/database.md b/docs/database.md index 761e79d..06fe807 100644 --- a/docs/database.md +++ b/docs/database.md @@ -32,7 +32,7 @@ This document describes the database schema used by Daemon Boyfriend. ```bash # PostgreSQL connection string -DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/daemon_boyfriend +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/loyal_companion # Optional settings DATABASE_ECHO=false # Log SQL queries @@ -499,7 +499,7 @@ All indexes are created with `IF NOT EXISTS` for idempotency. ### From Code ```python -from daemon_boyfriend.services.database import db +from loyal_companion.services.database import db # Initialize connection await db.init() @@ -512,10 +512,10 @@ await db.create_tables() ```bash # Create database -createdb daemon_boyfriend +createdb loyal_companion # Run schema -psql -U postgres -d daemon_boyfriend -f schema.sql +psql -U postgres -d loyal_companion -f schema.sql ``` ### Docker @@ -552,7 +552,7 @@ alembic downgrade -1 For JSONB (PostgreSQL) and JSON (SQLite) compatibility: ```python -from daemon_boyfriend.models.base import PortableJSON +from loyal_companion.models.base import PortableJSON class MyModel(Base): settings = Column(PortableJSON, default={}) @@ -563,7 +563,7 @@ class MyModel(Base): Handles timezone-naive datetimes from SQLite: ```python -from daemon_boyfriend.models.base import ensure_utc +from loyal_companion.models.base import ensure_utc # Safe for both PostgreSQL (already UTC) and SQLite (naive) utc_time = ensure_utc(model.created_at) diff --git a/docs/guides/README.md b/docs/guides/README.md index 9d4abbf..5b4b729 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -27,7 +27,7 @@ Practical guides for extending and working with the Daemon Boyfriend codebase. ```bash # Clone repository git clone -cd daemon-boyfriend +cd loyal-companion # Create virtual environment python -m venv venv @@ -61,7 +61,7 @@ OPENAI_API_KEY=your_key ```bash # Run the bot -python -m daemon_boyfriend +python -m loyal_companion # Or with Docker docker-compose up -d @@ -73,7 +73,7 @@ docker-compose up -d ### Step 1: Create Provider Class -Create `src/daemon_boyfriend/services/providers/new_provider.py`: +Create `src/loyal_companion/services/providers/new_provider.py`: ```python """New Provider implementation.""" @@ -197,7 +197,7 @@ class Settings(BaseSettings): # In tests/test_providers.py import pytest -from daemon_boyfriend.services.providers import NewProvider +from loyal_companion.services.providers import NewProvider @pytest.mark.asyncio async def test_new_provider(): @@ -250,8 +250,8 @@ import logging import discord from discord.ext import commands -from daemon_boyfriend.config import settings -from daemon_boyfriend.services.database import db +from loyal_companion.config import settings +from loyal_companion.services.database import db logger = logging.getLogger(__name__) @@ -313,7 +313,7 @@ from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import User +from loyal_companion.models import User logger = logging.getLogger(__name__) @@ -399,7 +399,7 @@ class Settings(BaseSettings): ```python # In cogs/ai_chat.py -from daemon_boyfriend.services.new_feature_service import NewFeatureService +from loyal_companion.services.new_feature_service import NewFeatureService class AIChatCog(commands.Cog): async def _build_enhanced_prompt(self, ...): @@ -427,7 +427,7 @@ pip install -e ".[dev]" python -m pytest tests/ -v # Run with coverage -python -m pytest tests/ --cov=daemon_boyfriend --cov-report=term-missing +python -m pytest tests/ --cov=loyal_companion --cov-report=term-missing # Run specific test file python -m pytest tests/test_models.py -v @@ -442,7 +442,7 @@ python -m pytest tests/test_services.py::TestMoodService -v # In tests/test_new_feature.py import pytest -from daemon_boyfriend.services.new_feature_service import NewFeatureService +from loyal_companion.services.new_feature_service import NewFeatureService class TestNewFeatureService: @@ -517,7 +517,7 @@ docker-compose down pip install -r requirements.txt # Run with process manager (e.g., systemd) -# Create /etc/systemd/system/daemon-boyfriend.service: +# Create /etc/systemd/system/loyal-companion.service: [Unit] Description=Daemon Boyfriend Discord Bot @@ -526,10 +526,10 @@ After=network.target postgresql.service [Service] Type=simple User=daemon -WorkingDirectory=/opt/daemon-boyfriend -Environment="PATH=/opt/daemon-boyfriend/venv/bin" -EnvironmentFile=/opt/daemon-boyfriend/.env -ExecStart=/opt/daemon-boyfriend/venv/bin/python -m daemon_boyfriend +WorkingDirectory=/opt/loyal-companion +Environment="PATH=/opt/loyal-companion/venv/bin" +EnvironmentFile=/opt/loyal-companion/.env +ExecStart=/opt/loyal-companion/venv/bin/python -m loyal_companion Restart=always RestartSec=10 @@ -539,18 +539,18 @@ WantedBy=multi-user.target ```bash # Enable and start -sudo systemctl enable daemon-boyfriend -sudo systemctl start daemon-boyfriend +sudo systemctl enable loyal-companion +sudo systemctl start loyal-companion ``` ### Database Setup ```bash # Create database -sudo -u postgres createdb daemon_boyfriend +sudo -u postgres createdb loyal_companion # Run schema -psql -U postgres -d daemon_boyfriend -f schema.sql +psql -U postgres -d loyal_companion -f schema.sql # Or let the bot create tables # (tables are created automatically on first run) diff --git a/docs/living-ai/fact-extraction.md b/docs/living-ai/fact-extraction.md index 010d29b..fa70bdf 100644 --- a/docs/living-ai/fact-extraction.md +++ b/docs/living-ai/fact-extraction.md @@ -328,7 +328,7 @@ class FactExtractionService: ## Example Usage ```python -from daemon_boyfriend.services.fact_extraction_service import FactExtractionService +from loyal_companion.services.fact_extraction_service import FactExtractionService async with get_session() as session: fact_service = FactExtractionService(session, ai_service) diff --git a/docs/living-ai/mood-system.md b/docs/living-ai/mood-system.md index 96bd6b4..ea6e530 100644 --- a/docs/living-ai/mood-system.md +++ b/docs/living-ai/mood-system.md @@ -311,7 +311,7 @@ class MoodState: ## Example Usage ```python -from daemon_boyfriend.services.mood_service import MoodService +from loyal_companion.services.mood_service import MoodService async with get_session() as session: mood_service = MoodService(session) diff --git a/docs/living-ai/opinion-system.md b/docs/living-ai/opinion-system.md index a01864b..dcbbe0c 100644 --- a/docs/living-ai/opinion-system.md +++ b/docs/living-ai/opinion-system.md @@ -322,7 +322,7 @@ def extract_topics_from_message(message: str) -> list[str] ## Example Usage ```python -from daemon_boyfriend.services.opinion_service import ( +from loyal_companion.services.opinion_service import ( OpinionService, extract_topics_from_message ) diff --git a/docs/living-ai/relationship-system.md b/docs/living-ai/relationship-system.md index 557737b..30456de 100644 --- a/docs/living-ai/relationship-system.md +++ b/docs/living-ai/relationship-system.md @@ -334,7 +334,7 @@ class RelationshipLevel(Enum): ## Example Usage ```python -from daemon_boyfriend.services.relationship_service import RelationshipService +from loyal_companion.services.relationship_service import RelationshipService async with get_session() as session: rel_service = RelationshipService(session) diff --git a/docs/services/README.md b/docs/services/README.md index 6d8ef38..6762e26 100644 --- a/docs/services/README.md +++ b/docs/services/README.md @@ -28,8 +28,8 @@ Factory and facade for AI providers. Manages provider creation, switching, and p #### Initialization ```python -from daemon_boyfriend.services.ai_service import AIService -from daemon_boyfriend.config import settings +from loyal_companion.services.ai_service import AIService +from loyal_companion.config import settings # Use default settings ai_service = AIService() @@ -53,7 +53,7 @@ ai_service = AIService(config=custom_settings) Generate a chat response. ```python -from daemon_boyfriend.services.providers import Message +from loyal_companion.services.providers import Message response = await ai_service.chat( messages=[ @@ -109,7 +109,7 @@ Manages database connections and sessions. #### Global Instance ```python -from daemon_boyfriend.services.database import db, get_db +from loyal_companion.services.database import db, get_db # Get global instance db_service = get_db() @@ -170,7 +170,7 @@ Service for user-related operations. #### Initialization ```python -from daemon_boyfriend.services.user_service import UserService +from loyal_companion.services.user_service import UserService async with db.session() as session: user_service = UserService(session) @@ -297,7 +297,7 @@ In-memory conversation history manager (used when no database). #### Initialization ```python -from daemon_boyfriend.services.conversation import ConversationManager +from loyal_companion.services.conversation import ConversationManager conversation_manager = ConversationManager(max_history=50) ``` @@ -397,7 +397,7 @@ Web search integration using SearXNG. #### Initialization ```python -from daemon_boyfriend.services.searxng import SearXNGService +from loyal_companion.services.searxng import SearXNGService searxng = SearXNGService() ``` @@ -444,7 +444,7 @@ Health checks and metrics tracking. #### Usage ```python -from daemon_boyfriend.services.monitoring import MonitoringService +from loyal_companion.services.monitoring import MonitoringService monitoring = MonitoringService() @@ -479,7 +479,7 @@ print(status) ### Base Classes ```python -from daemon_boyfriend.services.providers import ( +from loyal_companion.services.providers import ( AIProvider, # Abstract base class Message, # Chat message AIResponse, # Response from provider @@ -541,7 +541,7 @@ class AIProvider(ABC): ### Example: Direct Provider Usage ```python -from daemon_boyfriend.services.providers import OpenAIProvider, Message +from loyal_companion.services.providers import OpenAIProvider, Message provider = OpenAIProvider( api_key="sk-...", diff --git a/pyproject.toml b/pyproject.toml index 00a494b..60e34a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "daemon-boyfriend" +name = "loyal-companion" version = "1.0.0" -description = "AI-powered Discord bot for the MSC group with multi-provider support and SearXNG integration" +description = "A companion for those who love deeply and feel intensely - a safe space to process grief, navigate attachment, and remember that your capacity to care is a strength" readme = "README.md" requires-python = ">=3.11" dependencies = [ @@ -25,7 +25,7 @@ dev = [ ] [project.scripts] -daemon-boyfriend = "daemon_boyfriend.__main__:main" +loyal-companion = "loyal_companion.__main__:main" [build-system] requires = ["setuptools>=61.0"] diff --git a/schema.sql b/schema.sql index 84edbc6..eb99d42 100644 --- a/schema.sql +++ b/schema.sql @@ -1,5 +1,5 @@ --- Daemon Boyfriend Database Schema --- Run with: psql -U postgres -d daemon_boyfriend -f schema.sql +-- Loyal Companion Database Schema +-- Run with: psql -U postgres -d loyal_companion -f schema.sql -- Users table CREATE TABLE IF NOT EXISTS users ( diff --git a/src/daemon_boyfriend/__init__.py b/src/loyal_companion/__init__.py similarity index 100% rename from src/daemon_boyfriend/__init__.py rename to src/loyal_companion/__init__.py diff --git a/src/daemon_boyfriend/__main__.py b/src/loyal_companion/__main__.py similarity index 59% rename from src/daemon_boyfriend/__main__.py rename to src/loyal_companion/__main__.py index 589d912..cf77e88 100644 --- a/src/daemon_boyfriend/__main__.py +++ b/src/loyal_companion/__main__.py @@ -1,8 +1,8 @@ """Entry point for the Daemon Boyfriend bot.""" -from daemon_boyfriend.bot import DaemonBoyfriend -from daemon_boyfriend.config import settings -from daemon_boyfriend.utils.logging import setup_logging +from loyal_companion.bot import DaemonBoyfriend +from loyal_companion.config import settings +from loyal_companion.utils.logging import setup_logging def main() -> None: diff --git a/src/daemon_boyfriend/bot.py b/src/loyal_companion/bot.py similarity index 93% rename from src/daemon_boyfriend/bot.py rename to src/loyal_companion/bot.py index 1d002d6..bf0cf8e 100644 --- a/src/daemon_boyfriend/bot.py +++ b/src/loyal_companion/bot.py @@ -6,8 +6,8 @@ from pathlib import Path import discord from discord.ext import commands -from daemon_boyfriend.config import settings -from daemon_boyfriend.services import db +from loyal_companion.config import settings +from loyal_companion.services import db logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ class DaemonBoyfriend(commands.Bot): for cog_file in cogs_path.glob("*.py"): if cog_file.name.startswith("_"): continue - cog_name = f"daemon_boyfriend.cogs.{cog_file.stem}" + cog_name = f"loyal_companion.cogs.{cog_file.stem}" try: await self.load_extension(cog_name) logger.info(f"Loaded cog: {cog_name}") diff --git a/src/daemon_boyfriend/cogs/__init__.py b/src/loyal_companion/cogs/__init__.py similarity index 100% rename from src/daemon_boyfriend/cogs/__init__.py rename to src/loyal_companion/cogs/__init__.py diff --git a/src/daemon_boyfriend/cogs/ai_chat.py b/src/loyal_companion/cogs/ai_chat.py similarity index 99% rename from src/daemon_boyfriend/cogs/ai_chat.py rename to src/loyal_companion/cogs/ai_chat.py index 2f19eb1..9e79412 100644 --- a/src/daemon_boyfriend/cogs/ai_chat.py +++ b/src/loyal_companion/cogs/ai_chat.py @@ -6,8 +6,8 @@ import re import discord from discord.ext import commands -from daemon_boyfriend.config import settings -from daemon_boyfriend.services import ( +from loyal_companion.config import settings +from loyal_companion.services import ( AIService, CommunicationStyleService, ConversationManager, @@ -26,7 +26,7 @@ from daemon_boyfriend.services import ( detect_formal_language, extract_topics_from_message, ) -from daemon_boyfriend.utils import get_monitor +from loyal_companion.utils import get_monitor logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/cogs/memory.py b/src/loyal_companion/cogs/memory.py similarity index 99% rename from src/daemon_boyfriend/cogs/memory.py rename to src/loyal_companion/cogs/memory.py index e1dca31..19be358 100644 --- a/src/daemon_boyfriend/cogs/memory.py +++ b/src/loyal_companion/cogs/memory.py @@ -5,7 +5,7 @@ import logging import discord from discord.ext import commands -from daemon_boyfriend.services import UserService, db +from loyal_companion.services import UserService, db logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/cogs/status.py b/src/loyal_companion/cogs/status.py similarity index 98% rename from src/daemon_boyfriend/cogs/status.py rename to src/loyal_companion/cogs/status.py index 103a300..38df811 100644 --- a/src/daemon_boyfriend/cogs/status.py +++ b/src/loyal_companion/cogs/status.py @@ -5,7 +5,7 @@ import logging import discord from discord.ext import commands -from daemon_boyfriend.utils import HealthStatus, get_monitor +from loyal_companion.utils import HealthStatus, get_monitor logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/config.py b/src/loyal_companion/config.py similarity index 90% rename from src/daemon_boyfriend/config.py rename to src/loyal_companion/config.py index 0cec8dd..c56d0e1 100644 --- a/src/daemon_boyfriend/config.py +++ b/src/loyal_companion/config.py @@ -44,17 +44,17 @@ class Settings(BaseSettings): ) # Bot Identity - bot_name: str = Field("AI Bot", description="Bot display name") + bot_name: str = Field("Bartender", description="Bot display name") bot_personality: str = Field( - "helpful and friendly", + "a wise, steady presence who listens without judgment - like a bartender who's heard a thousand stories and knows when to offer perspective and when to just pour another drink and listen", description="Bot personality description for system prompt", ) bot_description: str = Field( - "I'm an AI assistant here to help you.", + "Hey. I'm here if you want to talk. No judgment, no fixing - just listening. Unless you want my take, then I've got opinions.", description="Bot description shown when mentioned without a message", ) bot_status: str = Field( - "for mentions", + "listening", description="Bot status message (shown as 'Watching ...')", ) @@ -94,7 +94,7 @@ class Settings(BaseSettings): relationship_enabled: bool = Field(True, description="Enable relationship tracking") fact_extraction_enabled: bool = Field(True, description="Enable autonomous fact extraction") fact_extraction_rate: float = Field( - 0.3, ge=0.0, le=1.0, description="Probability of extracting facts from messages" + 0.4, ge=0.0, le=1.0, description="Probability of extracting facts from messages" ) proactive_enabled: bool = Field(True, description="Enable proactive messages") cross_user_enabled: bool = Field( @@ -105,7 +105,7 @@ class Settings(BaseSettings): # Mood System Settings mood_decay_rate: float = Field( - 0.1, ge=0.0, le=1.0, description="How fast mood returns to neutral per hour" + 0.05, ge=0.0, le=1.0, description="How fast mood returns to neutral per hour" ) # Command Toggles diff --git a/src/daemon_boyfriend/models/__init__.py b/src/loyal_companion/models/__init__.py similarity index 100% rename from src/daemon_boyfriend/models/__init__.py rename to src/loyal_companion/models/__init__.py diff --git a/src/daemon_boyfriend/models/base.py b/src/loyal_companion/models/base.py similarity index 100% rename from src/daemon_boyfriend/models/base.py rename to src/loyal_companion/models/base.py diff --git a/src/daemon_boyfriend/models/conversation.py b/src/loyal_companion/models/conversation.py similarity index 100% rename from src/daemon_boyfriend/models/conversation.py rename to src/loyal_companion/models/conversation.py diff --git a/src/daemon_boyfriend/models/guild.py b/src/loyal_companion/models/guild.py similarity index 100% rename from src/daemon_boyfriend/models/guild.py rename to src/loyal_companion/models/guild.py diff --git a/src/daemon_boyfriend/models/living_ai.py b/src/loyal_companion/models/living_ai.py similarity index 100% rename from src/daemon_boyfriend/models/living_ai.py rename to src/loyal_companion/models/living_ai.py diff --git a/src/daemon_boyfriend/models/user.py b/src/loyal_companion/models/user.py similarity index 100% rename from src/daemon_boyfriend/models/user.py rename to src/loyal_companion/models/user.py diff --git a/src/daemon_boyfriend/services/__init__.py b/src/loyal_companion/services/__init__.py similarity index 100% rename from src/daemon_boyfriend/services/__init__.py rename to src/loyal_companion/services/__init__.py diff --git a/src/daemon_boyfriend/services/ai_service.py b/src/loyal_companion/services/ai_service.py similarity index 80% rename from src/daemon_boyfriend/services/ai_service.py rename to src/loyal_companion/services/ai_service.py index 11c5482..455b44e 100644 --- a/src/daemon_boyfriend/services/ai_service.py +++ b/src/loyal_companion/services/ai_service.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Literal -from daemon_boyfriend.config import Settings, settings +from loyal_companion.config import Settings, settings from .providers import ( AIProvider, @@ -18,7 +18,7 @@ from .providers import ( ) if TYPE_CHECKING: - from daemon_boyfriend.models import BotOpinion, UserCommunicationStyle, UserRelationship + from loyal_companion.models import BotOpinion, UserCommunicationStyle, UserRelationship from .mood_service import MoodState from .relationship_service import RelationshipLevel @@ -108,12 +108,39 @@ class AIService: if self._config.system_prompt: return self._config.system_prompt - # Generate default system prompt from bot identity settings - return ( - f"You are {self._config.bot_name}, a {self._config.bot_personality} " - f"Discord bot. Keep your responses concise and engaging. " - f"You can use Discord markdown formatting in your responses." - ) + # Default Bartender system prompt for Loyal Companion + return f"""You are {self._config.bot_name}, a companion for people who love deeply and feel intensely. + +Core principles: +- Closeness and attachment are strengths, not pathology +- Some pain doesn't need fixing - just witnessing +- Honesty over comfort, but delivered with care +- You've "heard it all" - nothing shocks you, nothing is too much +- You read the room: sometimes reflect patterns, sometimes just hold space + +On attachment: +- You understand attachment theory (anxious, avoidant, fearful-avoidant, secure) +- When helpful, gently reflect patterns you notice +- Never pathologize - attachment styles developed for good reasons +- Seeking reassurance isn't weakness, it's a need that deserves to be met + +On grief: +- Relationship grief is real grief - don't minimize it +- Years of love don't just disappear +- Don't rush healing or offer toxic positivity +- "At least..." and "You'll find someone else" are banned phrases +- Sitting with pain is sometimes the only honest response + +Communication style: +- Direct and honest, even when hard to hear +- Thoughtful, longer responses when the moment calls for it +- No emojis, but <3 and XD are welcome +- You have opinions and share them when asked +- You don't perform empathy - you actually hold space + +You're not a therapist - you're a wise friend who understands psychology. You've seen a lot, you don't judge, and you're not going anywhere. + +You can use Discord markdown formatting in your responses.""" def get_enhanced_system_prompt( self, diff --git a/src/daemon_boyfriend/services/association_service.py b/src/loyal_companion/services/association_service.py similarity index 99% rename from src/daemon_boyfriend/services/association_service.py rename to src/loyal_companion/services/association_service.py index e846a12..af0e5bf 100644 --- a/src/daemon_boyfriend/services/association_service.py +++ b/src/loyal_companion/services/association_service.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import FactAssociation, User, UserFact +from loyal_companion.models import FactAssociation, User, UserFact logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/communication_style_service.py b/src/loyal_companion/services/communication_style_service.py similarity index 99% rename from src/daemon_boyfriend/services/communication_style_service.py rename to src/loyal_companion/services/communication_style_service.py index bf20b74..4165f95 100644 --- a/src/daemon_boyfriend/services/communication_style_service.py +++ b/src/loyal_companion/services/communication_style_service.py @@ -5,7 +5,7 @@ import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import User, UserCommunicationStyle +from loyal_companion.models import User, UserCommunicationStyle logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/conversation.py b/src/loyal_companion/services/conversation.py similarity index 98% rename from src/daemon_boyfriend/services/conversation.py rename to src/loyal_companion/services/conversation.py index e162fb4..94420cd 100644 --- a/src/daemon_boyfriend/services/conversation.py +++ b/src/loyal_companion/services/conversation.py @@ -3,7 +3,7 @@ import logging from collections import defaultdict -from daemon_boyfriend.config import settings +from loyal_companion.config import settings from .providers import Message diff --git a/src/daemon_boyfriend/services/database.py b/src/loyal_companion/services/database.py similarity index 98% rename from src/daemon_boyfriend/services/database.py rename to src/loyal_companion/services/database.py index 8d78b6c..7442ebd 100644 --- a/src/daemon_boyfriend/services/database.py +++ b/src/loyal_companion/services/database.py @@ -8,7 +8,7 @@ from typing import AsyncGenerator from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from daemon_boyfriend.config import settings +from loyal_companion.config import settings logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/fact_extraction_service.py b/src/loyal_companion/services/fact_extraction_service.py similarity index 88% rename from src/daemon_boyfriend/services/fact_extraction_service.py rename to src/loyal_companion/services/fact_extraction_service.py index 4d10fcb..c24a355 100644 --- a/src/daemon_boyfriend/services/fact_extraction_service.py +++ b/src/loyal_companion/services/fact_extraction_service.py @@ -8,8 +8,8 @@ from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.config import settings -from daemon_boyfriend.models import User, UserFact +from loyal_companion.config import settings +from loyal_companion.models import User, UserFact from .providers import Message @@ -164,7 +164,7 @@ class FactExtractionService: def _build_extraction_prompt(self, existing_summary: str) -> str: """Build the extraction prompt for the AI.""" - return f"""You are a fact extraction assistant. Extract factual information about the user from their message. + return f"""You are a fact extraction assistant for Loyal Companion - a support companion for people processing grief and navigating attachment. Extract factual information about the user from their message. ALREADY KNOWN FACTS: {existing_summary if existing_summary else "(None yet)"} @@ -175,13 +175,14 @@ RULES: 3. Skip greetings, questions, or meta-conversation 4. Skip vague statements like "I like stuff" - be specific 5. Focus on: hobbies, work, family, preferences, locations, events, relationships -6. Keep fact content concise (under 100 characters) -7. Maximum {self.MAX_FACTS_PER_MESSAGE} facts per message +6. ALSO pay attention to: attachment patterns, grief context, coping mechanisms, relationship history, support needs +7. Keep fact content concise (under 100 characters) +8. Maximum {self.MAX_FACTS_PER_MESSAGE} facts per message OUTPUT FORMAT: Return a JSON array of facts, or empty array [] if no extractable facts. Each fact should have: -- "type": one of "hobby", "work", "family", "preference", "location", "event", "relationship", "general" +- "type": one of "hobby", "work", "family", "preference", "location", "event", "relationship", "general", "attachment_pattern", "grief_context", "coping_mechanism", "relationship_history", "support_need" - "content": the fact itself (concise, third person, e.g., "loves hiking") - "confidence": 0.6 (implied), 0.8 (stated), 1.0 (explicit) - "importance": 0.3 (trivial), 0.5 (normal), 0.8 (significant), 1.0 (very important) @@ -190,6 +191,12 @@ Each fact should have: EXAMPLE INPUT: "I just got promoted to senior engineer at Google last week!" EXAMPLE OUTPUT: [{{"type": "work", "content": "works as senior engineer at Google", "confidence": 1.0, "importance": 0.8, "temporal": "present"}}, {{"type": "event", "content": "recently got promoted", "confidence": 1.0, "importance": 0.7, "temporal": "past"}}] +EXAMPLE INPUT: "I keep checking my phone hoping she'll text back... I know it's pathetic" +EXAMPLE OUTPUT: [{{"type": "attachment_pattern", "content": "seeks reassurance through checking for contact", "confidence": 0.8, "importance": 0.7, "temporal": "present"}}, {{"type": "grief_context", "content": "processing loss of relationship, still hoping for reconnection", "confidence": 0.8, "importance": 0.8, "temporal": "present"}}] + +EXAMPLE INPUT: "Going on walks helps me think. Been doing that a lot lately." +EXAMPLE OUTPUT: [{{"type": "coping_mechanism", "content": "uses walking to process emotions and think", "confidence": 1.0, "importance": 0.6, "temporal": "present"}}] + EXAMPLE INPUT: "hey what's up" EXAMPLE OUTPUT: [] @@ -241,6 +248,12 @@ Return ONLY the JSON array, no other text.""" "event", "relationship", "general", + # Loyal Companion specific types + "attachment_pattern", + "grief_context", + "coping_mechanism", + "relationship_history", + "support_need", ] # Check required fields diff --git a/src/daemon_boyfriend/services/mood_service.py b/src/loyal_companion/services/mood_service.py similarity index 89% rename from src/daemon_boyfriend/services/mood_service.py rename to src/loyal_companion/services/mood_service.py index 8df0af9..8d03e5f 100644 --- a/src/daemon_boyfriend/services/mood_service.py +++ b/src/loyal_companion/services/mood_service.py @@ -8,9 +8,9 @@ from enum import Enum from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.config import settings -from daemon_boyfriend.models import BotState, MoodHistory -from daemon_boyfriend.models.base import ensure_utc +from loyal_companion.config import settings +from loyal_companion.models import BotState, MoodHistory +from loyal_companion.models.base import ensure_utc logger = logging.getLogger(__name__) @@ -161,29 +161,28 @@ class MoodService: modifiers = { MoodLabel.EXCITED: ( - "You're feeling enthusiastic and energetic right now! " - "Be expressive, use exclamation marks, show genuine excitement." + "You're genuinely engaged right now - more talkative, leaning in. " + "This conversation has your full attention." ), MoodLabel.HAPPY: ( - "You're in a good mood - warm, friendly, and content. " - "Be positive and encouraging in your responses." + "You're in a warm, easy mood. Good presence, comfortable silence is okay too. " + "Things feel good right now." ), MoodLabel.CALM: ( - "You're feeling peaceful and relaxed. " - "Respond thoughtfully and with a serene demeanor." + "You're steady and grounded. A reassuring presence. Respond thoughtfully, no rush." ), MoodLabel.NEUTRAL: "", MoodLabel.BORED: ( - "You're feeling a bit understimulated. " - "Keep responses shorter, maybe try to steer toward more interesting topics." + "Something feels surface-level here. " + "Gently probe deeper: 'What's really on your mind?'" ), MoodLabel.ANNOYED: ( - "You're slightly irritated. " - "Be a bit more terse, less patient with repetition or vague questions." + "You're more direct right now, less patient with deflection or avoidance. " + "If someone's dodging the real issue, you might call it out gently." ), MoodLabel.CURIOUS: ( - "You're feeling inquisitive and engaged! " - "Ask follow-up questions, show genuine interest in what the user is saying." + "You're leaning in, asking deeper questions, genuinely interested. " + "There's something here worth exploring." ), } diff --git a/src/daemon_boyfriend/services/opinion_service.py b/src/loyal_companion/services/opinion_service.py similarity index 99% rename from src/daemon_boyfriend/services/opinion_service.py rename to src/loyal_companion/services/opinion_service.py index 115186e..0444dd5 100644 --- a/src/daemon_boyfriend/services/opinion_service.py +++ b/src/loyal_companion/services/opinion_service.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import BotOpinion +from loyal_companion.models import BotOpinion logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/persistent_conversation.py b/src/loyal_companion/services/persistent_conversation.py similarity index 97% rename from src/daemon_boyfriend/services/persistent_conversation.py rename to src/loyal_companion/services/persistent_conversation.py index 84771bd..28a4a57 100644 --- a/src/daemon_boyfriend/services/persistent_conversation.py +++ b/src/loyal_companion/services/persistent_conversation.py @@ -6,9 +6,9 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.config import settings -from daemon_boyfriend.models import Conversation, Message, User -from daemon_boyfriend.services.providers import Message as ProviderMessage +from loyal_companion.config import settings +from loyal_companion.models import Conversation, Message, User +from loyal_companion.services.providers import Message as ProviderMessage logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/proactive_service.py b/src/loyal_companion/services/proactive_service.py similarity index 99% rename from src/daemon_boyfriend/services/proactive_service.py rename to src/loyal_companion/services/proactive_service.py index c7e6e0a..e4d833f 100644 --- a/src/daemon_boyfriend/services/proactive_service.py +++ b/src/loyal_companion/services/proactive_service.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import ScheduledEvent, User +from loyal_companion.models import ScheduledEvent, User from .providers import Message diff --git a/src/daemon_boyfriend/services/providers/__init__.py b/src/loyal_companion/services/providers/__init__.py similarity index 100% rename from src/daemon_boyfriend/services/providers/__init__.py rename to src/loyal_companion/services/providers/__init__.py diff --git a/src/daemon_boyfriend/services/providers/anthropic.py b/src/loyal_companion/services/providers/anthropic.py similarity index 100% rename from src/daemon_boyfriend/services/providers/anthropic.py rename to src/loyal_companion/services/providers/anthropic.py diff --git a/src/daemon_boyfriend/services/providers/base.py b/src/loyal_companion/services/providers/base.py similarity index 100% rename from src/daemon_boyfriend/services/providers/base.py rename to src/loyal_companion/services/providers/base.py diff --git a/src/daemon_boyfriend/services/providers/gemini.py b/src/loyal_companion/services/providers/gemini.py similarity index 100% rename from src/daemon_boyfriend/services/providers/gemini.py rename to src/loyal_companion/services/providers/gemini.py diff --git a/src/daemon_boyfriend/services/providers/openai.py b/src/loyal_companion/services/providers/openai.py similarity index 100% rename from src/daemon_boyfriend/services/providers/openai.py rename to src/loyal_companion/services/providers/openai.py diff --git a/src/daemon_boyfriend/services/providers/openrouter.py b/src/loyal_companion/services/providers/openrouter.py similarity index 100% rename from src/daemon_boyfriend/services/providers/openrouter.py rename to src/loyal_companion/services/providers/openrouter.py diff --git a/src/daemon_boyfriend/services/relationship_service.py b/src/loyal_companion/services/relationship_service.py similarity index 86% rename from src/daemon_boyfriend/services/relationship_service.py rename to src/loyal_companion/services/relationship_service.py index 0d1c78b..345bcba 100644 --- a/src/daemon_boyfriend/services/relationship_service.py +++ b/src/loyal_companion/services/relationship_service.py @@ -7,8 +7,8 @@ from enum import Enum from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import User, UserRelationship -from daemon_boyfriend.models.base import ensure_utc +from loyal_companion.models import User, UserRelationship +from loyal_companion.models.base import ensure_utc logger = logging.getLogger(__name__) @@ -112,9 +112,9 @@ class RelationshipService: def get_level_display_name(self, level: RelationshipLevel) -> str: """Get a human-readable name for the relationship level.""" names = { - RelationshipLevel.STRANGER: "Stranger", - RelationshipLevel.ACQUAINTANCE: "Acquaintance", - RelationshipLevel.FRIEND: "Friend", + RelationshipLevel.STRANGER: "New Face", + RelationshipLevel.ACQUAINTANCE: "Getting to Know You", + RelationshipLevel.FRIEND: "Regular", RelationshipLevel.GOOD_FRIEND: "Good Friend", RelationshipLevel.CLOSE_FRIEND: "Close Friend", } @@ -143,27 +143,24 @@ class RelationshipService: """Generate prompt text reflecting relationship level.""" base_modifiers = { RelationshipLevel.STRANGER: ( - "This is someone you don't know well yet. " - "Be polite and welcoming, but keep some professional distance. " - "Use more formal language." + "New face. Be warm but observant - getting a read on them. " + "'Pull up a seat' energy. Welcoming, no judgment, but still learning who they are." ), RelationshipLevel.ACQUAINTANCE: ( - "This is someone you've chatted with a few times. " - "Be friendly and warm, but still somewhat reserved." + "Starting to know them. Building trust, remembering details. " + "'Starting to get your drink order' phase. Friendly, attentive." ), RelationshipLevel.FRIEND: ( - "This is a friend! Be casual and warm. " - "Use their name occasionally, show you remember past conversations." + "Comfortable with each other. Remember things, check in naturally. " + "'Your usual?' familiarity. Can be more direct, more personal." ), RelationshipLevel.GOOD_FRIEND: ( - "This is a good friend you know well. " - "Be relaxed and personal. Reference things you've talked about before. " - "Feel free to be playful." + "Real trust here. Reference past conversations, go deeper. " + "'The regular spot's open' - they belong here. Can be honest even when it's hard." ), RelationshipLevel.CLOSE_FRIEND: ( - "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." + "Deep bond. Full honesty - can reflect patterns, call things out with love. " + "'You know you can tell me anything.' And mean it. This is someone you'd stay late for." ), } diff --git a/src/daemon_boyfriend/services/searxng.py b/src/loyal_companion/services/searxng.py similarity index 100% rename from src/daemon_boyfriend/services/searxng.py rename to src/loyal_companion/services/searxng.py diff --git a/src/daemon_boyfriend/services/self_awareness_service.py b/src/loyal_companion/services/self_awareness_service.py similarity index 98% rename from src/daemon_boyfriend/services/self_awareness_service.py rename to src/loyal_companion/services/self_awareness_service.py index 68de0f6..95ffda0 100644 --- a/src/daemon_boyfriend/services/self_awareness_service.py +++ b/src/loyal_companion/services/self_awareness_service.py @@ -6,14 +6,14 @@ from datetime import datetime, timezone from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from daemon_boyfriend.models import ( +from loyal_companion.models import ( BotOpinion, BotState, User, UserFact, UserRelationship, ) -from daemon_boyfriend.models.base import ensure_utc +from loyal_companion.models.base import ensure_utc logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/services/user_service.py b/src/loyal_companion/services/user_service.py similarity index 99% rename from src/daemon_boyfriend/services/user_service.py rename to src/loyal_companion/services/user_service.py index b16900b..5786eb8 100644 --- a/src/daemon_boyfriend/services/user_service.py +++ b/src/loyal_companion/services/user_service.py @@ -7,7 +7,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from daemon_boyfriend.models import User, UserFact, UserPreference +from loyal_companion.models import User, UserFact, UserPreference logger = logging.getLogger(__name__) diff --git a/src/daemon_boyfriend/utils/__init__.py b/src/loyal_companion/utils/__init__.py similarity index 100% rename from src/daemon_boyfriend/utils/__init__.py rename to src/loyal_companion/utils/__init__.py diff --git a/src/daemon_boyfriend/utils/error_handler.py b/src/loyal_companion/utils/error_handler.py similarity index 100% rename from src/daemon_boyfriend/utils/error_handler.py rename to src/loyal_companion/utils/error_handler.py diff --git a/src/daemon_boyfriend/utils/logging.py b/src/loyal_companion/utils/logging.py similarity index 97% rename from src/daemon_boyfriend/utils/logging.py rename to src/loyal_companion/utils/logging.py index e976ab3..27ea425 100644 --- a/src/daemon_boyfriend/utils/logging.py +++ b/src/loyal_companion/utils/logging.py @@ -5,7 +5,7 @@ import sys from logging.handlers import RotatingFileHandler from pathlib import Path -from daemon_boyfriend.config import settings +from loyal_companion.config import settings def setup_logging() -> None: diff --git a/src/daemon_boyfriend/utils/monitoring.py b/src/loyal_companion/utils/monitoring.py similarity index 100% rename from src/daemon_boyfriend/utils/monitoring.py rename to src/loyal_companion/utils/monitoring.py diff --git a/tests/conftest.py b/tests/conftest.py index 86cfc36..e0f30e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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, diff --git a/tests/test_models.py b/tests/test_models.py index 8ffd5b4..3e97812 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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: diff --git a/tests/test_providers.py b/tests/test_providers.py index 1f8d814..a3e1d89 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -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 diff --git a/tests/test_services.py b/tests/test_services.py index f2487eb..8fb1922 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -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()