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

@@ -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>")
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

View File

@@ -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)

View File

@@ -35,4 +35,4 @@ ENV PYTHONUNBUFFERED=1
USER botuser
# Run the bot
CMD ["python", "-m", "daemon_boyfriend"]
CMD ["python", "-m", "loyal_companion"]

201
README.md
View File

@@ -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 <name>` | Set your preferred name |
| `!clearname` | Reset to Discord display name |
| `!remember <fact>` | 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 <name>` | Set name for another user |
| `!teachbot @user <fact>` | 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 <date>` | 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
├── 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

View File

@@ -1,33 +1,33 @@
services:
daemon-boyfriend:
loyal-companion:
build: .
container_name: daemon-boyfriend
container_name: loyal-companion
restart: unless-stopped
env_file:
- .env
environment:
- PYTHONUNBUFFERED=1
- DATABASE_URL=postgresql+asyncpg://daemon:${POSTGRES_PASSWORD:-daemon}@postgres:5432/daemon_boyfriend
- 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
container_name: loyal-companion-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}
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 daemon -d daemon_boyfriend"]
test: ["CMD-SHELL", "pg_isready -U companion -d loyal_companion"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -142,7 +142,7 @@ OPENAI_API_KEY=your_key
### 3. Run
```bash
python -m daemon_boyfriend
python -m loyal_companion
```
### 4. Interact

View File

@@ -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

View File

@@ -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
```
---

View File

@@ -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)

View File

@@ -27,7 +27,7 @@ Practical guides for extending and working with the Daemon Boyfriend codebase.
```bash
# Clone repository
git clone <repository-url>
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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-...",

View File

@@ -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"]

View File

@@ -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 (

View File

@@ -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:

View File

@@ -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}")

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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

View File

@@ -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,

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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

View File

@@ -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__)

View File

@@ -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

View File

@@ -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."
),
}

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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

View File

@@ -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."
),
}

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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:

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()